Files
finance/frontend/src/pages/ResetPasswordPage.tsx
T
2026-09-22 08:29:49 +05:00

130 lines
4.1 KiB
TypeScript

import { useMemo, useState } from 'react'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
import { z } from 'zod'
import { AuthLayout } from '@/components/layout/AuthLayout'
import { Button } from '@/components/ui/Button'
import { TextField } from '@/components/ui/fields'
import { useConfirmPasswordReset, useRequestPasswordReset } from '@/hooks/useAuth'
import { describeApiError } from '@/lib/apiErrors'
interface ResetValues {
email: string
password: string
confirm: string
}
export function ResetPasswordPage() {
const { t } = useTranslation()
const [searchParams] = useSearchParams()
const token = searchParams.get('token') ?? ''
const requestReset = useRequestPasswordReset()
const confirmReset = useConfirmPasswordReset()
const navigate = useNavigate()
const [sent, setSent] = useState(false)
const [apiError, setApiError] = useState<string | null>(null)
const schema = useMemo(
() =>
token
? z
.object({
email: z.string(),
password: z.string().min(12, t('auth.passwordHint')),
confirm: z.string().min(1, t('common.requiredField')),
})
.refine((values) => values.password === values.confirm, {
path: ['confirm'],
message: t('auth.passwordMismatch'),
})
: z.object({
email: z.string().min(1, t('common.requiredField')).email(t('auth.invalidEmail')),
password: z.string(),
confirm: z.string(),
}),
[t, token],
)
const {
register,
handleSubmit,
formState: { errors },
} = useForm<ResetValues>({
resolver: zodResolver(schema),
defaultValues: { email: '', password: '', confirm: '' },
})
const pending = requestReset.isPending || confirmReset.isPending
const submit = handleSubmit((values) => {
setApiError(null)
if (!token) {
requestReset.mutate(values.email, {
onSuccess: () => setSent(true),
onError: (error) => setApiError(describeApiError(error, t)),
})
return
}
confirmReset.mutate(
{ token, password: values.password },
{
onSuccess: () => navigate('/accounts', { replace: true }),
onError: (error) => setApiError(describeApiError(error, t)),
},
)
})
return (
<AuthLayout
title={t('auth.resetTitle')}
subtitle={t('auth.resetSubtitle')}
context={t('authShell.reset')}
>
{apiError ? (
<div className="banner banner-danger" role="alert">
{apiError}
</div>
) : null}
{sent ? (
<div className="banner banner-success" role="status">
{t('auth.resetSent')}
</div>
) : (
<form onSubmit={submit} noValidate className="form-grid">
{token ? (
<>
<TextField
label={t('auth.password')}
type="password"
autoComplete="new-password"
hint={t('auth.passwordHint')}
error={errors.password?.message}
{...register('password')}
/>
<TextField
label={t('auth.passwordConfirm')}
type="password"
autoComplete="new-password"
error={errors.confirm?.message}
{...register('confirm')}
/>
</>
) : (
<TextField
label={t('auth.email')}
type="email"
autoComplete="email"
error={errors.email?.message}
{...register('email')}
/>
)}
<Button type="submit" disabled={pending}>
{token ? t('auth.resetSubmit') : t('auth.resetSend')}
</Button>
</form>
)}
<p className="auth-alt">
<Link to="/login">{t('auth.backToLogin')}</Link>
</p>
</AuthLayout>
)
}