Files
leocrm/frontend/src/pages/PasswordResetConfirm.tsx
T
leocrm-bot 22976abe92 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)
2026-06-29 08:02:15 +02:00

107 lines
3.8 KiB
TypeScript

import React, { useState } from 'react';
import { Link, useSearchParams } 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 { usePasswordResetConfirm } from '@/api/hooks';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
const resetConfirmSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string().min(8),
}).refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword'],
});
type ResetConfirmFormData = z.infer<typeof resetConfirmSchema>;
export function PasswordResetConfirmPage() {
const { t } = useTranslation();
const [searchParams] = useSearchParams();
const token = searchParams.get('token') || '';
const resetMutation = usePasswordResetConfirm();
const [result, setResult] = useState<'success' | 'error' | null>(null);
const {
register,
handleSubmit,
formState: { errors },
} = useForm<ResetConfirmFormData>({
resolver: zodResolver(resetConfirmSchema),
defaultValues: { password: '', confirmPassword: '' },
});
const onSubmit = async (data: ResetConfirmFormData) => {
try {
await resetMutation.mutateAsync({ token, password: data.password });
setResult('success');
} catch {
setResult('error');
}
};
if (result === 'success') {
return (
<div className="min-h-screen flex items-center justify-center bg-secondary-50 px-4" data-testid="reset-confirm-page">
<div className="w-full max-w-md">
<div className="bg-white rounded-lg shadow-md p-8 text-center">
<p className="text-secondary-700 mb-6">{t('auth.resetConfirmSuccess')}</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-confirm-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.resetConfirmTitle')}</h2>
<p className="text-sm text-secondary-500 mb-6">{t('auth.resetConfirmDesc')}</p>
{result === 'error' && (
<div role="alert" className="text-sm text-danger-600 bg-danger-50 rounded-md p-3 mb-4">
{t('auth.resetConfirmFailed')}
</div>
)}
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4" noValidate>
<Input
label={t('auth.newPassword')}
type="password"
autoComplete="new-password"
required
error={errors.password?.message}
helperText={t('auth.passwordTooShort')}
{...register('password')}
/>
<Input
label={t('auth.confirmPassword')}
type="password"
autoComplete="new-password"
required
error={errors.confirmPassword?.message}
{...register('confirmPassword')}
/>
<Button type="submit" fullWidth isLoading={resetMutation.isPending}>
{t('auth.resetConfirmButton')}
</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>
);
}