feat(conversion): Google-first login, truthful dashboard, SSR marketplace, template seeding
- login: OAuth/email on top, phone collapsed behind 'Sign in with phone instead' (prod providers: google on, email off, sms on — a developer should never see a phone field as the front door) - dashboard: plan card wired to GET /v1/billing/status; calls card shows '—' + pointer to per-server metrics (no user-facing usage endpoint exists; previous card showed invented '0 of 100,000 / Hobby') - templates: server-rendered grid (revalidate 300) via fetchPublicTemplates, client browser hydrates with initial data; inviting empty state with labeled starter ideas instead of 'No templates yet' - servers/new: removed upgrade nag from first analyze wait - scripts/seed-templates.mjs: idempotent dry-run-by-default seeder driving the real preview->create->live->publish flow for 6 first-party templates Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SXUwmPVRTD8AKQtio6gCN5
This commit is contained in:
@@ -203,9 +203,11 @@ export default function LoginPage() {
|
||||
sms: false,
|
||||
email: false,
|
||||
});
|
||||
// Default to SMS — email is off by default until an SMTP/Resend provider
|
||||
// is wired. The effect below flips to 'email' if the backend says it's on.
|
||||
const [method, setMethod] = useState<'email' | 'phone'>('phone');
|
||||
// Phone sign-in is deliberately collapsed behind a text link whenever any
|
||||
// other provider (OAuth or email) is available — a developer evaluating the
|
||||
// product should never see a phone-number field as the front door. It only
|
||||
// renders expanded when SMS is the sole configured provider.
|
||||
const [phoneOpen, setPhoneOpen] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Email magic-link
|
||||
@@ -226,9 +228,9 @@ export default function LoginPage() {
|
||||
)
|
||||
.then((p) => {
|
||||
setProviders(p);
|
||||
// Pick the most-likely method up-front: email if enabled, else SMS.
|
||||
if (p.email) setMethod('email');
|
||||
else if (p.sms) setMethod('phone');
|
||||
// SMS as the only provider → show the phone form directly (no point
|
||||
// hiding the sole sign-in method behind a toggle).
|
||||
if (p.sms && !p.email && !p.google && !p.github) setPhoneOpen(true);
|
||||
})
|
||||
.catch(() => undefined);
|
||||
const err = new URLSearchParams(window.location.search).get('error');
|
||||
@@ -317,7 +319,7 @@ export default function LoginPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasOAuth && (
|
||||
{hasOAuth && (providers.email || providers.sms) && (
|
||||
<div className="my-5 flex items-center gap-3">
|
||||
<span className="h-px flex-1 bg-[--color-border]" />
|
||||
<span className="text-[11px] uppercase tracking-wider text-[--color-fg-subtle]">
|
||||
@@ -327,35 +329,8 @@ export default function LoginPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab toggle only shown when BOTH email and SMS are enabled — if just
|
||||
one is configured, that method's form renders directly without a
|
||||
useless one-tab toggle. */}
|
||||
{providers.sms && providers.email && (
|
||||
<div
|
||||
className={`flex gap-1 rounded-md border border-[--color-border] p-1 ${hasOAuth ? '' : 'mt-7'}`}
|
||||
>
|
||||
{(['email', 'phone'] as const).map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setMethod(m);
|
||||
setError(null);
|
||||
}}
|
||||
className={`h-7 flex-1 rounded text-[12px] font-medium transition-colors ${
|
||||
method === m
|
||||
? 'bg-[--color-bg-subtle] text-[--color-fg]'
|
||||
: 'text-[--color-fg-muted] hover:text-[--color-fg]'
|
||||
}`}
|
||||
>
|
||||
{m === 'email' ? 'Email' : 'Phone'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={providers.sms || providers.email ? 'mt-4' : hasOAuth ? '' : 'mt-7'}>
|
||||
{method === 'email' && providers.email && emailState !== 'sent' && (
|
||||
{providers.email && !phoneOpen && emailState !== 'sent' && (
|
||||
<form onSubmit={sendMagicLink} className="space-y-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
@@ -381,7 +356,7 @@ export default function LoginPage() {
|
||||
</form>
|
||||
)}
|
||||
|
||||
{method === 'email' && providers.email && emailState === 'sent' && (
|
||||
{providers.email && !phoneOpen && emailState === 'sent' && (
|
||||
<div className="panel p-4">
|
||||
<p className="text-[13px]">
|
||||
Magic link sent to <span className="mono">{email}</span>.
|
||||
@@ -392,15 +367,11 @@ export default function LoginPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{method === 'phone' && smsStep === 'phone' && (
|
||||
{providers.sms && phoneOpen && smsStep === 'phone' && (
|
||||
<form onSubmit={requestSmsCode} className="space-y-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="country">Country</Label>
|
||||
<CountryPicker
|
||||
countries={COUNTRIES}
|
||||
value={country}
|
||||
onChange={setCountry}
|
||||
/>
|
||||
<CountryPicker countries={COUNTRIES} value={country} onChange={setCountry} />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="phone" hint={dialFor(country)}>
|
||||
@@ -429,7 +400,7 @@ export default function LoginPage() {
|
||||
</form>
|
||||
)}
|
||||
|
||||
{method === 'phone' && smsStep === 'code' && (
|
||||
{providers.sms && phoneOpen && smsStep === 'code' && (
|
||||
<form onSubmit={verifySmsCode} className="space-y-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="code" hint={`sent to ${sentTo}`}>
|
||||
@@ -471,6 +442,35 @@ export default function LoginPage() {
|
||||
)}
|
||||
|
||||
{error && <p className="mt-3 text-[12px] text-[--color-danger]">{error}</p>}
|
||||
|
||||
{/* Phone sign-in stays available but demoted: expand link when any
|
||||
other provider exists, collapse link to get back. */}
|
||||
{providers.sms && !phoneOpen && (hasOAuth || providers.email) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setPhoneOpen(true);
|
||||
setError(null);
|
||||
}}
|
||||
className="mt-4 w-full text-center text-[12px] text-[--color-fg-muted] underline-offset-2 transition-colors hover:text-[--color-fg] hover:underline"
|
||||
>
|
||||
Sign in with phone number instead
|
||||
</button>
|
||||
)}
|
||||
{providers.sms && phoneOpen && (hasOAuth || providers.email) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setPhoneOpen(false);
|
||||
setSmsStep('phone');
|
||||
setCode('');
|
||||
setError(null);
|
||||
}}
|
||||
className="mt-4 w-full text-center text-[12px] text-[--color-fg-muted] underline-offset-2 transition-colors hover:text-[--color-fg] hover:underline"
|
||||
>
|
||||
← Use another sign-in method
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 text-[12px] text-[--color-fg-subtle]">
|
||||
|
||||
Reference in New Issue
Block a user