feat: auth hook and login page (Task 9)

- useAuth: session token from URL/localStorage, /api/auth/me validation, logout
- LoginPage: email form with POST /api/auth/request, error handling
- App.tsx: use useAuth hook, LoginPage, 'sent' confirmation route
- Fix: auto-route to game covers both 'login' and 'sent' routes when authenticated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-30 02:10:38 +00:00
parent 2dd6402c86
commit 7c9386f125
3 changed files with 144 additions and 52 deletions
+64
View File
@@ -0,0 +1,64 @@
import { useState } from 'react'
type LoginPageProps = { onLoginSent: () => void }
export function LoginPage({ onLoginSent }: LoginPageProps) {
const [email, setEmail] = useState('')
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (!email.includes('@') || !email.includes('.')) {
setError('Please enter a valid email address.')
return
}
setSubmitting(true)
setError(null)
try {
const r = await fetch('/api/auth/request', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
})
if (!r.ok) {
const body = await r.json().catch(() => ({}))
throw new Error(body.error || 'Something went wrong')
}
onLoginSent()
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Something went wrong')
} finally {
setSubmitting(false)
}
}
return (
<div style={{ maxWidth: 420, margin: '4rem auto 0' }}>
<h1 style={{ fontFamily: 'var(--font-header)', marginBottom: '0.5rem' }}>MorseQuest</h1>
<p style={{ marginBottom: '2rem', color: 'var(--color-brown)' }}>
Learn Morse code, adventurer. Enter your email to begin your quest.
</p>
<form onSubmit={handleSubmit}>
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 600 }}>
Email address
</label>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="you@example.com"
disabled={submitting}
required
style={{ marginBottom: '1rem' }}
/>
{error && (
<p style={{ color: 'crimson', marginBottom: '1rem', fontSize: '0.9rem' }}>{error}</p>
)}
<button type="submit" className="btn" disabled={submitting} style={{ width: '100%' }}>
{submitting ? 'Sending...' : 'Send Magic Link'}
</button>
</form>
</div>
)
}