109 lines
3.9 KiB
TypeScript
109 lines
3.9 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { Navigate, useLocation, useNavigate } from 'react-router-dom';
|
|
import { useForm } from 'react-hook-form';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { Gift, Loader2 } from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
import { loginSchema, type LoginInput } from '@family-wishlist/shared';
|
|
import { Button } from '@/components/ui/Button';
|
|
import { Input } from '@/components/ui/Input';
|
|
import { Label } from '@/components/ui/Label';
|
|
import { useAuthStore } from '@/features/auth/authStore';
|
|
import { ApiError } from '@/lib/api';
|
|
import { Footer } from '@/components/Layout/Footer';
|
|
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
|
|
import { translateValidation, useI18n } from '@/i18n/i18n';
|
|
|
|
export function LoginPage() {
|
|
const { t } = useI18n();
|
|
const { user, login } = useAuthStore();
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const from = (location.state as { from?: { pathname: string } } | null)?.from?.pathname ?? '/';
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
formState: { errors, isSubmitting },
|
|
} = useForm<LoginInput>({
|
|
resolver: zodResolver(loginSchema),
|
|
defaultValues: { username: '', password: '' },
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (user) navigate(from, { replace: true });
|
|
}, [user, from, navigate]);
|
|
|
|
if (user) return <Navigate to={from} replace />;
|
|
|
|
const submit = handleSubmit(async (values) => {
|
|
try {
|
|
await login(values.username, values.password);
|
|
navigate(from, { replace: true });
|
|
} catch (err) {
|
|
if (err instanceof ApiError) toast.error(err.message);
|
|
else toast.error(t('login.failed'));
|
|
}
|
|
});
|
|
|
|
return (
|
|
<div className="app-shell">
|
|
<div className="public-profile__toolbar">
|
|
<LanguageSwitcher />
|
|
</div>
|
|
<div className="app-shell__main flex items-center justify-center py-12">
|
|
<div className="w-full max-w-md animate-fade-in-up">
|
|
<div className="mb-6 flex items-center justify-center gap-2">
|
|
<span className="inline-flex h-11 w-11 items-center justify-center rounded-md bg-primary text-primary-foreground shadow-card">
|
|
<Gift className="h-5 w-5" />
|
|
</span>
|
|
<h1 className="font-display text-3xl">{t('app.name')}</h1>
|
|
</div>
|
|
|
|
<div className="profile-form p-6 sm:p-8">
|
|
<h2 className="mb-1 text-xl font-semibold">{t('login.title')}</h2>
|
|
<p className="mb-6 text-sm text-muted">
|
|
{t('login.description')}
|
|
</p>
|
|
<form className="grid gap-4" onSubmit={submit}>
|
|
<div className="field">
|
|
<Label htmlFor="username">{t('login.username')}</Label>
|
|
<Input
|
|
id="username"
|
|
autoComplete="username"
|
|
autoFocus
|
|
{...register('username')}
|
|
/>
|
|
{errors.username && (
|
|
<span className="field__error">
|
|
{translateValidation(t, errors.username.message)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="field">
|
|
<Label htmlFor="password">{t('login.password')}</Label>
|
|
<Input
|
|
id="password"
|
|
type="password"
|
|
autoComplete="current-password"
|
|
{...register('password')}
|
|
/>
|
|
{errors.password && (
|
|
<span className="field__error">
|
|
{translateValidation(t, errors.password.message)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<Button type="submit" size="lg" disabled={isSubmitting}>
|
|
{isSubmitting && <Loader2 className="h-4 w-4 animate-spin" />}
|
|
{t('login.submit')}
|
|
</Button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Footer />
|
|
</div>
|
|
);
|
|
}
|