100 lines
3.2 KiB
TypeScript
100 lines
3.2 KiB
TypeScript
import { useState, type FormEvent } from 'react'
|
|
import { Link, useNavigate } from 'react-router'
|
|
import AppLayout from '../components/layout/AppLayout'
|
|
import Button from '../components/ui/Button'
|
|
import FieldError from '../components/ui/FieldError'
|
|
import Input from '../components/ui/Input'
|
|
import Panel from '../components/ui/Panel'
|
|
import { api } from '../lib/api'
|
|
import { parseFieldErrors, type FieldErrors } from '../lib/fieldErrors'
|
|
|
|
export default function RegisterPage() {
|
|
const navigate = useNavigate()
|
|
const [email, setEmail] = useState('')
|
|
const [password, setPassword] = useState('')
|
|
const [fieldErrors, setFieldErrors] = useState<FieldErrors>({})
|
|
const [formError, setFormError] = useState<string | null>(null)
|
|
const [pending, setPending] = useState(false)
|
|
|
|
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
|
event.preventDefault()
|
|
setFieldErrors({})
|
|
setFormError(null)
|
|
setPending(true)
|
|
|
|
try {
|
|
await api.postJson('/register', { email, password })
|
|
// Register does not establish a session — send the user to sign in.
|
|
navigate('/login')
|
|
} catch (err: unknown) {
|
|
const errors = parseFieldErrors(err)
|
|
if (errors !== null) {
|
|
setFieldErrors(errors)
|
|
} else {
|
|
setFormError('Registration failed. Please try again.')
|
|
}
|
|
} finally {
|
|
setPending(false)
|
|
}
|
|
}
|
|
|
|
const emailErrors = fieldErrors.email
|
|
const passwordErrors = fieldErrors.password
|
|
|
|
return (
|
|
<AppLayout>
|
|
<Panel className="mx-auto max-w-sm space-y-4 mt-32">
|
|
<h2 className="text-primary font-bold uppercase tracking-wider">
|
|
Register
|
|
</h2>
|
|
|
|
<form className="space-y-4" onSubmit={handleSubmit}>
|
|
<div className="space-y-1">
|
|
<Input
|
|
label="Email"
|
|
type="email"
|
|
autoComplete="email"
|
|
required
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
aria-invalid={emailErrors !== undefined}
|
|
aria-describedby={emailErrors ? 'email-errors' : undefined}
|
|
/>
|
|
<FieldError id="email-errors" messages={emailErrors} />
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<Input
|
|
label="Password"
|
|
type="password"
|
|
autoComplete="new-password"
|
|
required
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
aria-invalid={passwordErrors !== undefined}
|
|
aria-describedby={passwordErrors ? 'password-errors' : undefined}
|
|
/>
|
|
<FieldError id="password-errors" messages={passwordErrors} />
|
|
</div>
|
|
|
|
{formError !== null && (
|
|
<p role="alert" className="text-destructive font-mono text-sm">
|
|
{formError}
|
|
</p>
|
|
)}
|
|
|
|
<Button type="submit" glow disabled={pending} className="w-full">
|
|
{pending ? 'Registering…' : 'Register'}
|
|
</Button>
|
|
</form>
|
|
|
|
<p className="text-muted-foreground font-mono text-sm">
|
|
Already have an account?{' '}
|
|
<Link to="/login" className="text-primary underline">
|
|
Log In
|
|
</Link>
|
|
</p>
|
|
</Panel>
|
|
</AppLayout>
|
|
)
|
|
}
|