feat(F-099): completed feature

This commit is contained in:
chattie
2026-08-21 07:29:07 +02:00
parent 3f1d08382f
commit 5177a851aa
41 changed files with 922 additions and 131 deletions

View 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>;
}