202 lines
8.0 KiB
TypeScript
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>
|
|
)
|
|
}
|