108 lines
3.5 KiB
TypeScript
108 lines
3.5 KiB
TypeScript
import { type FormEvent, type ReactNode, useState } from 'react';
|
|
import { useLocation, useNavigate } from 'react-router-dom';
|
|
import { useAuth } from './auth-context';
|
|
import { Button } from '../../components/ui/button';
|
|
import { Card } from '../../components/ui/card';
|
|
import { Input } from '../../components/ui/input';
|
|
import { ApiError } from '../../lib/api-client';
|
|
import { loginSchema } from '../../lib/schemas';
|
|
|
|
/** Zentrale Anmeldeseite der Plattform. */
|
|
export function LoginPage(): ReactNode {
|
|
const { login } = useAuth();
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
|
|
const [username, setUsername] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
const from = (location.state as { from?: string } | null)?.from ?? '/';
|
|
|
|
async function handleSubmit(event: FormEvent<HTMLFormElement>): Promise<void> {
|
|
event.preventDefault();
|
|
setError(null);
|
|
setFieldErrors({});
|
|
|
|
const parsed = loginSchema.safeParse({ username, password });
|
|
if (!parsed.success) {
|
|
const errors: Record<string, string> = {};
|
|
for (const issue of parsed.error.issues) {
|
|
const key = String(issue.path[0]);
|
|
if (!errors[key]) errors[key] = issue.message;
|
|
}
|
|
setFieldErrors(errors);
|
|
return;
|
|
}
|
|
|
|
setSubmitting(true);
|
|
try {
|
|
await login(parsed.data);
|
|
navigate(from, { replace: true });
|
|
} catch (caughtError) {
|
|
if (caughtError instanceof ApiError) {
|
|
setError(caughtError.message);
|
|
} else {
|
|
setError('Anmeldung fehlgeschlagen. Bitte später erneut versuchen.');
|
|
}
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="flex min-h-full items-center justify-center bg-slate-50 px-4">
|
|
<div className="w-full max-w-sm">
|
|
<div className="mb-8 text-center">
|
|
<span className="mx-auto flex h-12 w-12 items-center justify-center rounded-xl bg-brand-600 text-lg font-bold text-white">
|
|
M
|
|
</span>
|
|
<h1 className="mt-4 text-2xl font-bold text-slate-900">MPM</h1>
|
|
<p className="mt-1 text-sm text-slate-500">
|
|
Modulare Management-Plattform
|
|
</p>
|
|
</div>
|
|
|
|
<Card asChild className="rounded-xl p-6">
|
|
<form
|
|
onSubmit={(event) => void handleSubmit(event)}
|
|
className=""
|
|
noValidate
|
|
>
|
|
<div className="space-y-4">
|
|
<Input
|
|
label="Benutzername"
|
|
autoComplete="username"
|
|
value={username}
|
|
onChange={(event) => setUsername(event.target.value)}
|
|
error={fieldErrors.username}
|
|
autoFocus
|
|
/>
|
|
<Input
|
|
label="Passwort"
|
|
type="password"
|
|
autoComplete="current-password"
|
|
value={password}
|
|
onChange={(event) => setPassword(event.target.value)}
|
|
error={fieldErrors.password}
|
|
/>
|
|
</div>
|
|
|
|
{error && (
|
|
<p role="alert" className="mt-4 rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700">
|
|
{error}
|
|
</p>
|
|
)}
|
|
|
|
<Button type="submit" loading={submitting} className="mt-6 w-full">
|
|
Anmelden
|
|
</Button>
|
|
</form>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|