Files
Clarium/src/auth/LoginForm.tsx
T
mroberts f0efa25f23 feat: add sign-in flow, routing shell, and searchable book list
Pin @testing-library/jest-dom to ~6.9.1 (6.10.0 is npm-deprecated).

Claude-Session: https://claude.ai/code/session_01M9MLit5Ko3X4s7rzC5Kv7X
2026-10-02 14:23:42 -05:00

86 lines
2.8 KiB
TypeScript

import { useState, type FormEvent } from 'react'
import { Navigate, useLocation, type Location } from 'react-router'
import { errorMessage } from '../api'
import { card, inputClass, labelText, primaryButton } from '../ui'
import { useAuth } from './AuthContext'
export function LoginForm() {
const { user, login, register } = useAuth()
const location = useLocation()
const [mode, setMode] = useState<'login' | 'register'>('login')
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [busy, setBusy] = useState(false)
if (user) {
const from = (location.state as { from?: Location } | null)?.from
return <Navigate to={from ? from.pathname + from.search : '/books'} replace />
}
const isLogin = mode === 'login'
async function submit(event: FormEvent) {
event.preventDefault()
setError(null)
setBusy(true)
try {
await (isLogin ? login : register)(username, password)
} catch (err) {
setError(errorMessage(err))
} finally {
setBusy(false)
}
}
return (
<div className="min-h-screen bg-slate-50 px-4 pt-16">
<main className={`mx-auto max-w-sm ${card}`}>
<h1 className="mb-4 text-2xl font-semibold">{isLogin ? 'Sign in' : 'Create account'}</h1>
<form onSubmit={submit} className="space-y-4">
<label className="block">
<span className={labelText}>Username</span>
<input
className={inputClass}
value={username}
onChange={(e) => setUsername(e.target.value)}
autoComplete="username"
required
/>
</label>
<label className="block">
<span className={labelText}>Password</span>
<input
className={inputClass}
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete={isLogin ? 'current-password' : 'new-password'}
minLength={isLogin ? undefined : 12}
required
/>
</label>
{error && (
<p role="alert" className="text-sm text-red-700">
{error}
</p>
)}
<button type="submit" disabled={busy} className={`${primaryButton} w-full`}>
{isLogin ? 'Sign in' : 'Create account'}
</button>
</form>
<button
type="button"
className="mt-4 text-sm text-indigo-700 underline"
onClick={() => {
setMode(isLogin ? 'register' : 'login')
setError(null)
}}
>
{isLogin ? 'Need an account? Register' : 'Have an account? Sign in'}
</button>
</main>
</div>
)
}