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:
Marco Sadjadi
2026-07-08 23:00:25 +02:00
parent 17056d0b30
commit 089074d104
7 changed files with 775 additions and 402 deletions

View File

@@ -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]">