feat: Phase 1 – Plattform-Grundgerüst (Docker, NestJS, React, Auth)
This commit is contained in:
104
apps/platform-frontend/src/features/auth/login-page.tsx
Normal file
104
apps/platform-frontend/src/features/auth/login-page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user