'use client'; import { Input, Label } from '@/components/input'; import { Logo } from '@/components/logo'; import { Button } from '@/components/ui/button'; import { apiFetch, apiUrl } from '@/lib/api'; import Link from 'next/link'; import { useEffect, useState } from 'react'; const ERROR_COPY: Record = { google_failed: 'Google sign-in could not be completed. Please try again.', google_state: 'Google sign-in expired or was interrupted. Please try again.', github_failed: 'GitHub sign-in could not be completed. Please try again.', github_state: 'GitHub sign-in expired or was interrupted. Please try again.', invalid_phone: 'Enter your number in international format, e.g. +41 79 123 45 67.', rate_limited: 'Too many requests. Wait a few minutes and try again.', sms_request_failed: 'Could not send the SMS. Check the number and try again.', invalid_or_expired_code: 'That code has expired. Request a new one.', invalid_code: 'Wrong code. Check the SMS and try again.', too_many_attempts: 'Too many wrong attempts. Request a new code.', sms_verify_failed: 'Could not verify the code. Try again.', }; function errCode(err: unknown): string { const detail = (err as { detail?: { error?: string } }).detail; return detail?.error ?? (err as Error).message ?? 'unknown'; } export default function LoginPage() { const [providers, setProviders] = useState({ google: false, github: false, sms: false }); const [method, setMethod] = useState<'email' | 'phone'>('email'); const [error, setError] = useState(null); // Email magic-link const [email, setEmail] = useState(''); const [emailState, setEmailState] = useState<'idle' | 'sending' | 'sent'>('idle'); // SMS one-time code const [phone, setPhone] = useState(''); const [code, setCode] = useState(''); const [smsStep, setSmsStep] = useState<'phone' | 'code'>('phone'); const [smsBusy, setSmsBusy] = useState(false); useEffect(() => { apiFetch<{ google: boolean; github: boolean; sms: boolean }>('/v1/auth/providers') .then(setProviders) .catch(() => undefined); const err = new URLSearchParams(window.location.search).get('error'); if (err) setError(ERROR_COPY[err] ?? 'Sign-in failed. Please try again.'); }, []); async function sendMagicLink(e: React.FormEvent) { e.preventDefault(); setEmailState('sending'); setError(null); try { await apiFetch('/v1/auth/magic-link', { method: 'POST', body: JSON.stringify({ email }) }); setEmailState('sent'); } catch (err) { setEmailState('idle'); setError(ERROR_COPY[errCode(err)] ?? 'Could not send the link.'); } } async function requestSmsCode(e: React.FormEvent) { e.preventDefault(); setSmsBusy(true); setError(null); try { await apiFetch('/v1/auth/sms/request', { method: 'POST', body: JSON.stringify({ phone }) }); setSmsStep('code'); } catch (err) { setError(ERROR_COPY[errCode(err)] ?? 'Could not send the SMS.'); } finally { setSmsBusy(false); } } async function verifySmsCode(e: React.FormEvent) { e.preventDefault(); setSmsBusy(true); setError(null); try { await apiFetch('/v1/auth/sms/verify', { method: 'POST', body: JSON.stringify({ phone, code }), }); window.location.href = '/dashboard'; } catch (err) { setError(ERROR_COPY[errCode(err)] ?? 'Could not verify the code.'); setSmsBusy(false); } } const hasOAuth = providers.google || providers.github; return (

Sign in to your workspace

Passwordless — pick whichever is easiest.

{hasOAuth && (
{providers.google && ( Continue with Google )} {providers.github && ( Continue with GitHub )}
)} {hasOAuth && (
or
)} {providers.sms && (
{(['email', 'phone'] as const).map((m) => ( ))}
)}
{method === 'email' && emailState !== 'sent' && (
setEmail(e.target.value)} placeholder="you@company.com" />
)} {method === 'email' && emailState === 'sent' && (

Magic link sent to {email}.

Open it on this device to finish signing in.

)} {method === 'phone' && smsStep === 'phone' && (
setPhone(e.target.value)} placeholder="+41 79 123 45 67" />
)} {method === 'phone' && smsStep === 'code' && (
setCode(e.target.value.replace(/\D/g, ''))} placeholder="123456" className="mono tracking-[0.3em]" />
)} {error &&

{error}

}
← Back to home
); } function GoogleIcon() { return ( ); } function GitHubIcon() { return ( ); }