feat(frontend): Login + Register pages
This commit is contained in:
81
packages/frontend/src/pages/auth/Login.tsx
Normal file
81
packages/frontend/src/pages/auth/Login.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
54
packages/frontend/src/pages/auth/Register.tsx
Normal file
54
packages/frontend/src/pages/auth/Register.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user