Files
dyolink/frontend/src/app/[locale]/(dashboard)/settings/account/page.tsx

189 lines
5.9 KiB
TypeScript

'use client';
import { useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { useTranslations } from 'next-intl';
import { Link, useRouter } from '@/i18n/navigation';
import { useSearchParams } from 'next/navigation';
import { Lock } from 'lucide-react';
import { useAuth } from '@/lib/hooks/useAuth';
import { authApi } from '@/lib/api/auth';
import { Button } from '@/components/ui/shared/Button';
import { Input } from '@/components/ui/shared/Input';
import { Toast } from '@/components/ui/shared/Toast';
type PasswordForm = {
currentPassword: string;
newPassword: string;
confirmPassword: string;
};
export default function AccountSettingsPage() {
const t = useTranslations('settings');
const tAuth = useTranslations('auth');
const tCommon = useTranslations('common');
const tValidation = useTranslations('validation');
const { user, isAuthReady } = useAuth();
const router = useRouter();
const searchParams = useSearchParams();
const isResetFlow = searchParams.get('reset') === '1';
const [error, setError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const passwordSchema = useMemo(
() =>
z
.object({
currentPassword: z.string(),
newPassword: z
.string()
.min(8, tValidation('passwordMinLength'))
.regex(/[A-Z]/, tValidation('passwordUppercase'))
.regex(/[0-9]/, tValidation('passwordNumber')),
confirmPassword: z.string(),
})
.refine((data) => data.newPassword === data.confirmPassword, {
message: tValidation('passwordsDoNotMatch'),
path: ['confirmPassword'],
})
.superRefine((data, ctx) => {
if (!isResetFlow && !data.currentPassword.trim()) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: tValidation('passwordRequired'),
path: ['currentPassword'],
});
}
}),
[isResetFlow, tValidation],
);
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<PasswordForm>({
resolver: zodResolver(passwordSchema),
defaultValues: {
currentPassword: '',
newPassword: '',
confirmPassword: '',
},
});
useEffect(() => {
if (isAuthReady && !user) {
router.replace('/login');
}
}, [isAuthReady, user, router]);
const onSubmit = async (data: PasswordForm) => {
try {
setError(null);
setSuccessMessage(null);
setIsSubmitting(true);
await authApi.changePassword({
...(isResetFlow ? {} : { currentPassword: data.currentPassword }),
newPassword: data.newPassword,
});
reset();
setSuccessMessage(t('passwordChanged'));
router.replace('/login');
} catch (err: unknown) {
const message = err instanceof Error ? err.message : t('passwordChangeFailed');
setError(message || t('passwordChangeFailed'));
} finally {
setIsSubmitting(false);
}
};
const passwordToggleLabels = {
show: tAuth('showPassword'),
hide: tAuth('hidePassword'),
};
if (!isAuthReady || !user) {
return (
<p className="text-text-secondary text-sm">{tCommon('loadingEllipsis')}</p>
);
}
return (
<div className="space-y-6">
<div>
<Link href="/today" className="text-sm text-primary hover:opacity-90">
{tCommon('backToApp')}
</Link>
<h1 className="text-2xl font-semibold text-text-primary mt-4">{t('accountTitle')}</h1>
<p className="text-text-secondary text-sm mt-2">
{isResetFlow ? t('resetPasswordSubtitle') : t('accountSubtitle')}
</p>
</div>
<div className="surface-card p-6 sm:p-8 max-w-lg">
<h2 className="text-lg font-medium text-text-primary mb-1">
{isResetFlow ? t('resetPasswordTitle') : t('changePasswordTitle')}
</h2>
<p className="text-sm text-text-secondary mb-6">
{user.email}
{user.mobile ? ` · ${user.mobile}` : ''}
</p>
<form className="space-y-5" onSubmit={handleSubmit(onSubmit)}>
{!isResetFlow && (
<Input
label={t('currentPassword')}
{...register('currentPassword')}
type="password"
placeholder={tAuth('passwordPlaceholder')}
error={errors.currentPassword?.message}
icon={<Lock className="h-5 w-5 icon-flat" />}
passwordToggleLabels={passwordToggleLabels}
/>
)}
<Input
label={t('newPassword')}
{...register('newPassword')}
type="password"
placeholder={tAuth('passwordPlaceholder')}
error={errors.newPassword?.message}
icon={<Lock className="h-5 w-5 icon-flat" />}
passwordToggleLabels={passwordToggleLabels}
/>
<Input
label={t('confirmNewPassword')}
{...register('confirmPassword')}
type="password"
placeholder={tAuth('passwordPlaceholder')}
error={errors.confirmPassword?.message}
icon={<Lock className="h-5 w-5 icon-flat" />}
passwordToggleLabels={passwordToggleLabels}
/>
{error && (
<div className="p-3 bg-red-950/30 border border-red-600/40 rounded-[var(--radius-md)]">
<p className="text-sm text-red-600">{error}</p>
</div>
)}
<Button type="submit" variant="primary" isLoading={isSubmitting}>
{isResetFlow ? t('setNewPassword') : t('updatePassword')}
</Button>
</form>
</div>
{successMessage && (
<Toast variant="success">{successMessage}</Toast>
)}
</div>
);
}