feat(F-099): completed feature
This commit is contained in:
73
project/frontend/src/app/cuenta/restablecer/page.tsx
Normal file
73
project/frontend/src/app/cuenta/restablecer/page.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
'use client';
|
||||
|
||||
import { Suspense, useState } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
function ResetForm() {
|
||||
const router = useRouter();
|
||||
const params = useSearchParams();
|
||||
const token = params.get('token') ?? '';
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirm, setConfirm] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [done, setDone] = useState(false);
|
||||
|
||||
if (!token) return <Message>El enlace de recuperación no es válido.</Message>;
|
||||
if (done) return <Message success>Contraseña actualizada. Redirigiendo al inicio de sesión...</Message>;
|
||||
|
||||
const submit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
setError('');
|
||||
if (password.length < 8) return setError('La contraseña debe tener al menos 8 caracteres.');
|
||||
if (password !== confirm) return setError('Las contraseñas no coinciden.');
|
||||
setSaving(true);
|
||||
try {
|
||||
const response = await fetch('/api/auth/password-reset/confirm', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token, password }),
|
||||
});
|
||||
const data = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
setError(data?.error?.message ?? data?.message ?? 'El enlace es inválido o ha caducado.');
|
||||
return;
|
||||
}
|
||||
setDone(true);
|
||||
window.setTimeout(() => router.push('/auth/login'), 2200);
|
||||
} catch {
|
||||
setError('No se pudo conectar con el servidor. Inténtalo de nuevo.');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={submit} className="w-full max-w-md space-y-5 rounded-2xl border border-gray-200 bg-white p-8 shadow-sm">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Restablecer contraseña</h1>
|
||||
<p className="mt-1 text-sm text-gray-500">Elige una contraseña nueva para tu cuenta.</p>
|
||||
</div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Nueva contraseña
|
||||
<input type="password" minLength={8} required value={password} onChange={(e) => setPassword(e.target.value)} className="mt-1 w-full rounded-xl border border-gray-300 px-4 py-3 outline-none focus:ring-2 focus:ring-[#70ad47]" />
|
||||
</label>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Repetir contraseña
|
||||
<input type="password" minLength={8} required value={confirm} onChange={(e) => setConfirm(e.target.value)} className="mt-1 w-full rounded-xl border border-gray-300 px-4 py-3 outline-none focus:ring-2 focus:ring-[#70ad47]" />
|
||||
</label>
|
||||
{error && <p className="rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700">{error}</p>}
|
||||
<button disabled={saving} className="w-full rounded-xl bg-[#70ad47] py-3 font-semibold text-white hover:bg-[#5a9040] disabled:opacity-60">
|
||||
{saving ? 'Guardando...' : 'Cambiar contraseña'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function Message({ children, success = false }: { children: React.ReactNode; success?: boolean }) {
|
||||
return <div className={`w-full max-w-md rounded-2xl border p-8 text-sm ${success ? 'border-green-200 bg-green-50 text-green-700' : 'border-gray-200 bg-white text-gray-700'}`}>{children}</div>;
|
||||
}
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
return <main className="flex min-h-[70vh] items-center justify-center bg-gray-50 px-4 py-12"><Suspense fallback={<Message>Cargando...</Message>}><ResetForm /></Suspense></main>;
|
||||
}
|
||||
Reference in New Issue
Block a user