T07a: frontend core SPA — shell + auth + routing + i18n + UI library + a11y
- React 18 + Vite + TypeScript + Tailwind CSS setup - AppShell with Sidebar (plugin menu) + TopBar (tenant switcher, search, notifications, user menu) - Auth pages: Login, PasswordResetRequest, PasswordResetConfirm - Protected routes with auth guard - API client (axios with interceptors: session cookie, 401 redirect, 422 validation) - TanStack Query hooks for auth, users, companies, contacts, notifications - Zustand stores: authStore, uiStore - i18n setup (de/en locales) with react-i18next - UI component library: Button, Input, Select, Modal, Toast, Table, Card, Badge, Avatar, Pagination, EmptyState, Skeleton, ConfirmDialog - Accessibility: ARIA labels, 44px touch targets, keyboard nav, reduced-motion, sr-only - Design tokens from prototype as CSS custom properties - 111 tests passing across 20 test files - tsc --noEmit: 0 errors - npm run build: success (471KB JS, 24KB CSS)
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { usePasswordResetRequest } from '@/api/hooks';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
|
||||
const resetRequestSchema = z.object({
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
type ResetRequestFormData = z.infer<typeof resetRequestSchema>;
|
||||
|
||||
export function PasswordResetRequestPage() {
|
||||
const { t } = useTranslation();
|
||||
const resetMutation = usePasswordResetRequest();
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<ResetRequestFormData>({
|
||||
resolver: zodResolver(resetRequestSchema),
|
||||
defaultValues: { email: '' },
|
||||
});
|
||||
|
||||
const onSubmit = async (data: ResetRequestFormData) => {
|
||||
try {
|
||||
await resetMutation.mutateAsync(data);
|
||||
setSubmitted(true);
|
||||
} catch {
|
||||
setSubmitted(true);
|
||||
}
|
||||
};
|
||||
|
||||
if (submitted) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-secondary-50 px-4" data-testid="reset-request-page">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="bg-white rounded-lg shadow-md p-8 text-center">
|
||||
<div className="mb-4 w-12 h-12 mx-auto rounded-full bg-success-100 flex items-center justify-center" aria-hidden="true">
|
||||
<svg className="w-6 h-6 text-success-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-secondary-700 mb-6">{t('auth.resetRequestSuccess')}</p>
|
||||
<Link to="/login" className="text-sm text-primary-600 hover:text-primary-700">
|
||||
{t('auth.backToLogin')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-secondary-50 px-4" data-testid="reset-request-page">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-primary-600">leocrm</h1>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow-md p-8">
|
||||
<h2 className="text-xl font-semibold text-secondary-900 mb-2">{t('auth.resetRequestTitle')}</h2>
|
||||
<p className="text-sm text-secondary-500 mb-6">{t('auth.resetRequestDesc')}</p>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4" noValidate>
|
||||
<Input
|
||||
label={t('auth.email')}
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
error={errors.email?.message}
|
||||
{...register('email')}
|
||||
/>
|
||||
<Button type="submit" fullWidth isLoading={resetMutation.isPending}>
|
||||
{t('auth.resetRequestButton')}
|
||||
</Button>
|
||||
</form>
|
||||
<div className="mt-4 text-center">
|
||||
<Link to="/login" className="text-sm text-primary-600 hover:text-primary-700">
|
||||
{t('auth.backToLogin')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user