feat(frontend): Login + Register pages

This commit is contained in:
2026-05-20 23:07:19 +02:00
parent 6f921ae44e
commit 4e15d8b59d
2 changed files with 135 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
import { useState } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { authApi } from '../../api/auth.js';
import { ApiClientError } from '../../api/client.js';
import { useAuth } from '../../stores/authStore.js';
export function LoginPage() {
const [searchParams] = useSearchParams();
const next = searchParams.get('next') ?? '/';
const navigate = useNavigate();
const login = useAuth((s) => s.login);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [needsVerification, setNeedsVerification] = useState(false);
async function submit(e: React.FormEvent) {
e.preventDefault();
setBusy(true); setError(null); setNeedsVerification(false);
try {
await login(email, password);
navigate(next, { replace: true });
} catch (err) {
if (err instanceof ApiClientError) {
if (err.code === 'EMAIL_NOT_VERIFIED') setNeedsVerification(true);
else setError(err.message);
} else setError('Onbekende fout');
} finally { setBusy(false); }
}
async function resend() {
try { await authApi.resendVerification({ email }); setError('Bevestigingsmail opnieuw verstuurd.'); }
catch { setError('Kon mail niet versturen.'); }
}
return (
<AuthLayout title="Inloggen">
<form onSubmit={submit} className="space-y-4">
<Field label="E-mailadres">
<input type="email" className="input-field" required value={email} onChange={(e) => setEmail(e.target.value)} autoComplete="email" />
</Field>
<Field label="Wachtwoord">
<input type="password" className="input-field" required value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="current-password" />
</Field>
{error && <p className="rounded-xl bg-danger-50 p-3 text-sm text-danger-700 dark:bg-danger-400/10 dark:text-danger-400">{error}</p>}
{needsVerification && (
<div className="rounded-xl bg-amber-50 p-3 text-sm text-amber-800 dark:bg-amber-900/30 dark:text-amber-200">
Je e-mailadres is nog niet bevestigd.
<button type="button" className="ml-2 font-semibold underline" onClick={resend}>Stuur opnieuw</button>
</div>
)}
<button type="submit" className="btn-primary w-full py-3" disabled={busy}>{busy ? 'Bezig…' : 'Inloggen'}</button>
</form>
<div className="mt-6 flex justify-between text-sm">
<Link to="/forgot-password" className="text-brand-600 hover:underline">Wachtwoord vergeten?</Link>
<Link to="/register" className="text-brand-600 hover:underline">Account aanmaken</Link>
</div>
</AuthLayout>
);
}
export function AuthLayout({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="mx-auto max-w-md p-6">
<div className="surface p-8">
<h1 className="mb-6 font-display text-2xl font-bold">{title}</h1>
{children}
</div>
</div>
);
}
export function Field({ label, children }: { label: string; children: React.ReactNode }) {
return (
<label className="block text-sm">
<span className="mb-1 block font-medium text-slate-700 dark:text-slate-200">{label}</span>
{children}
</label>
);
}

View File

@@ -0,0 +1,54 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { authApi } from '../../api/auth.js';
import { ApiClientError } from '../../api/client.js';
import { AuthLayout, Field } from './Login.js';
export function RegisterPage() {
const [email, setEmail] = useState('');
const [displayName, setDisplayName] = useState('');
const [password, setPassword] = useState('');
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [done, setDone] = useState(false);
async function submit(e: React.FormEvent) {
e.preventDefault();
setBusy(true); setError(null);
try {
await authApi.register({ email, displayName, password });
setDone(true);
} catch (err) {
if (err instanceof ApiClientError) setError(err.message);
else setError('Onbekende fout');
} finally { setBusy(false); }
}
if (done) {
return (
<AuthLayout title="Bijna klaar 📬">
<p className="text-sm">
We hebben een bevestigingsmail gestuurd naar <strong>{email}</strong>.
Klik op de link om je account te activeren.
</p>
<Link to="/login" className="mt-6 block text-sm text-brand-600 hover:underline">Terug naar inloggen</Link>
</AuthLayout>
);
}
return (
<AuthLayout title="Registreren">
<form onSubmit={submit} className="space-y-4">
<Field label="Naam"><input className="input-field" required minLength={1} value={displayName} onChange={(e) => setDisplayName(e.target.value)} autoComplete="name" /></Field>
<Field label="E-mailadres"><input type="email" className="input-field" required value={email} onChange={(e) => setEmail(e.target.value)} autoComplete="email" /></Field>
<Field label="Wachtwoord (min. 8 tekens)"><input type="password" className="input-field" required minLength={8} value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="new-password" /></Field>
{error && <p className="rounded-xl bg-danger-50 p-3 text-sm text-danger-700 dark:bg-danger-400/10 dark:text-danger-400">{error}</p>}
<button type="submit" className="btn-primary w-full py-3" disabled={busy}>{busy ? 'Bezig…' : 'Account aanmaken'}</button>
<p className="text-xs text-slate-500">De eerste registratie wordt automatisch beheerder.</p>
</form>
<div className="mt-6 text-sm">
<Link to="/login" className="text-brand-600 hover:underline">Heb je al een account? Inloggen</Link>
</div>
</AuthLayout>
);
}