feat: Phase 1 – Plattform-Grundgerüst (Docker, NestJS, React, Auth)

This commit is contained in:
MPM Dev
2026-10-06 14:22:11 +02:00
commit a7e1c421f2
85 changed files with 17701 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
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 { 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>
<form
onSubmit={(event) => void handleSubmit(event)}
className="rounded-xl border border-slate-200 bg-white p-6 shadow-sm"
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>
</div>
</div>
);
}