Files
carmanagement/apps/dashboard/src/app/reset-password/page.tsx
T
2026-05-09 21:10:38 -04:00

202 lines
8.0 KiB
TypeScript

'use client'
import Link from 'next/link'
import { useState, Suspense } from 'react'
import { useSearchParams } from 'next/navigation'
import { useDashboardI18n } from '@/components/I18nProvider'
import { API_BASE } from '@/lib/api'
export default function ResetPasswordPage() {
return (
<Suspense>
<ResetPasswordContent />
</Suspense>
)
}
function ResetPasswordContent() {
const { language } = useDashboardI18n()
const dict = {
en: {
title: 'Set new password',
subtitle: 'Choose a strong password for your workspace account.',
newPassword: 'New password',
confirmPassword: 'Confirm password',
submit: 'Reset password',
submitting: 'Resetting…',
backToLogin: 'Back to sign in',
successTitle: 'Password updated',
successBody: 'Your password has been reset. You can now sign in with your new password.',
signIn: 'Sign in',
errorMismatch: 'Passwords do not match.',
errorShort: 'Password must be at least 8 characters.',
errorInvalidToken: 'This reset link is invalid or has expired. Please request a new one.',
errorGeneric: 'Something went wrong. Please try again.',
noToken: 'No reset token found. Please request a new password reset.',
requestNew: 'Request new reset link',
},
fr: {
title: 'Nouveau mot de passe',
subtitle: "Choisissez un mot de passe fort pour votre compte.",
newPassword: 'Nouveau mot de passe',
confirmPassword: 'Confirmer le mot de passe',
submit: 'Réinitialiser',
submitting: 'Traitement…',
backToLogin: 'Retour à la connexion',
successTitle: 'Mot de passe mis à jour',
successBody: 'Votre mot de passe a été réinitialisé. Vous pouvez maintenant vous connecter.',
signIn: 'Se connecter',
errorMismatch: 'Les mots de passe ne correspondent pas.',
errorShort: 'Le mot de passe doit contenir au moins 8 caractères.',
errorInvalidToken: 'Ce lien est invalide ou a expiré. Veuillez en demander un nouveau.',
errorGeneric: 'Une erreur est survenue. Veuillez réessayer.',
noToken: 'Aucun jeton trouvé. Veuillez demander une réinitialisation.',
requestNew: 'Demander un nouveau lien',
},
ar: {
title: 'تعيين كلمة مرور جديدة',
subtitle: 'اختر كلمة مرور قوية لحساب مساحة العمل.',
newPassword: 'كلمة المرور الجديدة',
confirmPassword: 'تأكيد كلمة المرور',
submit: 'إعادة تعيين',
submitting: 'جارٍ المعالجة…',
backToLogin: 'العودة إلى تسجيل الدخول',
successTitle: 'تم تحديث كلمة المرور',
successBody: 'تم إعادة تعيين كلمة المرور. يمكنك الآن تسجيل الدخول.',
signIn: 'تسجيل الدخول',
errorMismatch: 'كلمتا المرور غير متطابقتين.',
errorShort: 'يجب أن تتكون كلمة المرور من 8 أحرف على الأقل.',
errorInvalidToken: 'رابط إعادة التعيين غير صالح أو منتهي الصلاحية.',
errorGeneric: 'حدث خطأ ما. يرجى المحاولة مرة أخرى.',
noToken: 'لم يتم العثور على رمز. يرجى طلب إعادة تعيين.',
requestNew: 'طلب رابط جديد',
},
}[language]
const searchParams = useSearchParams()
const token = searchParams.get('token')
const [password, setPassword] = useState('')
const [confirm, setConfirm] = useState('')
const [loading, setLoading] = useState(false)
const [done, setDone] = useState(false)
const [error, setError] = useState<string | null>(null)
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
if (password.length < 8) { setError(dict.errorShort); return }
if (password !== confirm) { setError(dict.errorMismatch); return }
setLoading(true)
setError(null)
try {
const res = await fetch(`${API_BASE}/auth/employee/reset-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, password }),
})
const json = await res.json()
if (!res.ok) {
if (json?.error === 'invalid_token') throw new Error(dict.errorInvalidToken)
throw new Error(dict.errorGeneric)
}
setDone(true)
} catch (err: any) {
setError(err.message ?? dict.errorGeneric)
} finally {
setLoading(false)
}
}
if (!token) {
return (
<ResetShell title={dict.title}>
<p className="text-sm text-slate-500 text-center">{dict.noToken}</p>
<div className="mt-4 text-center">
<Link href="/forgot-password" className="btn-primary inline-flex">{dict.requestNew}</Link>
</div>
</ResetShell>
)
}
if (done) {
return (
<ResetShell title={dict.title}>
<div className="space-y-4 text-center">
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-green-100">
<svg className="h-7 w-7 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 className="text-xl font-bold text-slate-900">{dict.successTitle}</h2>
<p className="text-sm text-slate-500">{dict.successBody}</p>
<Link href="/sign-in" className="btn-primary inline-flex justify-center">{dict.signIn}</Link>
</div>
</ResetShell>
)
}
return (
<ResetShell title={dict.title} subtitle={dict.subtitle}>
<form onSubmit={handleSubmit} className="space-y-5">
{error && (
<div className="rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error}</div>
)}
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{dict.newPassword}</label>
<input
type="password"
required
minLength={8}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
className="input-field"
/>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-slate-700">{dict.confirmPassword}</label>
<input
type="password"
required
minLength={8}
value={confirm}
onChange={(e) => setConfirm(e.target.value)}
placeholder="••••••••"
className="input-field"
/>
</div>
<button
type="submit"
disabled={loading}
className="btn-primary w-full justify-center disabled:cursor-not-allowed disabled:opacity-60"
>
{loading ? dict.submitting : dict.submit}
</button>
</form>
<div className="mt-6 border-t border-slate-200 pt-6 text-center text-sm text-slate-500">
<Link href="/sign-in" className="font-semibold text-slate-900 underline decoration-slate-300 underline-offset-4">
{dict.backToLogin}
</Link>
</div>
</ResetShell>
)
}
function ResetShell({ title, subtitle, children }: { title: string; subtitle?: string; children: React.ReactNode }) {
return (
<main className="flex min-h-screen items-center justify-center bg-[radial-gradient(circle_at_top,#dbeafe,transparent_35%),linear-gradient(180deg,#f8fafc,white)] px-4 py-12">
<div className="w-full max-w-md">
<div className="mb-8 text-center">
<span className="text-xs font-semibold uppercase tracking-[0.24em] text-blue-600">RentalDriveGo</span>
<h1 className="mt-3 text-3xl font-black tracking-tight text-slate-900">{title}</h1>
{subtitle && <p className="mt-2 text-sm text-slate-500">{subtitle}</p>}
</div>
<section className="rounded-[2rem] border border-slate-200 bg-white p-8 shadow-sm">
{children}
</section>
</div>
</main>
)
}