Restructures frontend to a feature-based folder layout
Continuous Integration / config (pull_request) Successful in 11s
Continuous Integration / changes (pull_request) Successful in 21s
Continuous Integration / backend-build (pull_request) Skipped
Continuous Integration / backend-test (pull_request) Skipped
Continuous Integration / vulnerability-scan (pull_request) Skipped
Continuous Integration / frontend-prepare (pull_request) Successful in 1m37s
Continuous Integration / frontend-build (pull_request) Successful in 2m12s
Continuous Integration / frontend-test (pull_request) Successful in 4m42s
Continuous Integration / frontend-lint (pull_request) Successful in 2m2s
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Continuous Integration / publish-test (pull_request) Successful in 6m51s
Continuous Integration / deploy-test (pull_request) Skipped

Groups auth, setup, invitation, profile, users, cms, availability and
system code (services/hooks, components, schemas, mocks, pages) under
src/features/<name> instead of splitting by technical layer (api/,
components/, lib/schemas/, mocks/, pages/). Renames the old
connection-oriented `api` layer to `services` per feature, and splits
the monolithic api/types.ts into per-feature types.ts files (with
ProblemDetails/ApiResult merged into lib/api-client.ts as shared
infra). Layout-agnostic code (ui primitives, app shell, i18n, test
utils, lib) stays at the top level.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 22:30:37 +02:00
co-authored by Claude Sonnet 5
parent 650cf09cfa
commit 11ec08aac3
83 changed files with 249 additions and 254 deletions
@@ -0,0 +1,121 @@
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useNavigate, useSearch } from '@tanstack/react-router';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useAuth } from '@/features/auth/context/auth-context';
import { NetworkError, ProblemDetailsError } from '@/lib/api-client';
import { loginFormSchema, type LoginFormData } from '@/features/auth/schemas/auth';
export function LoginPage() {
const { t } = useTranslation();
const { login } = useAuth();
const navigate = useNavigate();
const search = useSearch({ strict: false }) as { redirect?: string };
const [serverError, setServerError] = useState<string | null>(null);
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<LoginFormData>({
resolver: zodResolver(loginFormSchema),
mode: 'onTouched',
defaultValues: { email: '', password: '' },
});
const onSubmit = handleSubmit(async (values) => {
setServerError(null);
try {
await login(values.email, values.password);
await navigate({ to: (search.redirect as string | undefined) ?? '/dashboard' });
} catch (err) {
if (err instanceof ProblemDetailsError && err.status === 401) {
setServerError(t('login.errors.invalidCredentials'));
} else if (err instanceof NetworkError) {
setServerError(t('errors.network'));
} else {
setServerError(t('login.errors.generic'));
}
}
});
return (
<div className="flex min-h-svh items-center justify-center bg-secondary/40 p-4">
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>{t('login.title')}</CardTitle>
<CardDescription>{t('login.subtitle')}</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={onSubmit} noValidate className="space-y-4">
{serverError !== null && (
<div
role="alert"
data-testid="login-error"
className="rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive"
>
{serverError}
</div>
)}
<div className="space-y-2">
<Label htmlFor="email">{t('login.email')}</Label>
<Input
id="email"
type="email"
autoComplete="email"
placeholder={t('login.emailPlaceholder')}
data-testid="login-email-input"
aria-invalid={errors.email !== undefined}
{...register('email')}
/>
{errors.email && (
<p
className="text-sm text-destructive"
data-testid="login-email-error"
>
{errors.email.message}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="password">{t('login.password')}</Label>
<Input
id="password"
type="password"
placeholder="••••••••"
autoComplete="current-password"
data-testid="login-password-input"
aria-invalid={errors.password !== undefined}
{...register('password')}
/>
{errors.password && (
<p
className="text-sm text-destructive"
data-testid="login-password-error"
>
{errors.password.message}
</p>
)}
</div>
<Button
type="submit"
className="w-full"
disabled={isSubmitting}
data-testid="login-form-submit-button"
>
{isSubmitting ? t('login.submitting') : t('login.submit')}
</Button>
</form>
</CardContent>
</Card>
</div>
);
}