feat(F-099): completed feature
This commit is contained in:
@@ -4,7 +4,12 @@ import { useState, useEffect, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
|
||||
const API = process.env.NEXT_PUBLIC_API_URL ?? 'http://127.0.0.1:3000';
|
||||
interface Profile {
|
||||
id: string;
|
||||
email: string;
|
||||
displayName: string | null;
|
||||
phone: string | null;
|
||||
}
|
||||
|
||||
interface Address {
|
||||
id: string;
|
||||
@@ -62,8 +67,8 @@ function AddressForm({
|
||||
setError('');
|
||||
try {
|
||||
const url = initial
|
||||
? `${API}/users/${userId}/addresses/${initial.id}`
|
||||
: `${API}/users/${userId}/addresses`;
|
||||
? `/api/users/${userId}/addresses/${initial.id}`
|
||||
: `/api/users/${userId}/addresses`;
|
||||
const method = initial ? 'PATCH' : 'POST';
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
@@ -187,6 +192,14 @@ export default function AccountPage() {
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editing, setEditing] = useState<Address | null>(null);
|
||||
const [msg, setMsg] = useState('');
|
||||
const [profile, setProfile] = useState<Profile | null>(null);
|
||||
const [profileForm, setProfileForm] = useState({ displayName: '', phone: '' });
|
||||
const [profileSaving, setProfileSaving] = useState(false);
|
||||
const [profileError, setProfileError] = useState('');
|
||||
const [passwordForm, setPasswordForm] = useState({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
||||
const [passwordSaving, setPasswordSaving] = useState(false);
|
||||
const [passwordMessage, setPasswordMessage] = useState('');
|
||||
const [passwordError, setPasswordError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && !user) {
|
||||
@@ -198,7 +211,7 @@ export default function AccountPage() {
|
||||
if (!user) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`${API}/users/${user.id}/addresses`, { credentials: 'include' });
|
||||
const res = await fetch(`/api/users/${user.id}/addresses`, { credentials: 'include' });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setAddresses(data.items ?? []);
|
||||
@@ -211,16 +224,58 @@ export default function AccountPage() {
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(() => {
|
||||
void load();
|
||||
}, 0);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [load]);
|
||||
if (!user) return;
|
||||
fetch(`/api/users/${user.id}`, { credentials: 'include' })
|
||||
.then((res) => res.ok ? res.json() : null)
|
||||
.then((data: Profile | null) => {
|
||||
if (!data) return;
|
||||
setProfile(data);
|
||||
setProfileForm({ displayName: data.displayName ?? '', phone: data.phone ?? '' });
|
||||
})
|
||||
.catch(() => setProfileError('No se pudieron cargar tus datos personales'));
|
||||
}, [user]);
|
||||
|
||||
const saveProfile = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!user) return;
|
||||
setProfileSaving(true); setProfileError('');
|
||||
try {
|
||||
const res = await fetch(`/api/users/${user.id}`, {
|
||||
method: 'PATCH', headers: { 'Content-Type': 'application/json' }, credentials: 'include',
|
||||
body: JSON.stringify({ displayName: profileForm.displayName, phone: profileForm.phone }),
|
||||
});
|
||||
const data = await res.json().catch(() => null);
|
||||
if (!res.ok) throw new Error(data?.error?.message ?? 'No se pudieron guardar los datos');
|
||||
setProfile(data); setMsg('Datos personales actualizados');
|
||||
} catch (error) {
|
||||
setProfileError(error instanceof Error ? error.message : 'No se pudieron guardar los datos');
|
||||
} finally { setProfileSaving(false); }
|
||||
};
|
||||
|
||||
const changePassword = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
setPasswordError(''); setPasswordMessage('');
|
||||
if (passwordForm.newPassword.length < 8) { setPasswordError('La nueva contraseña debe tener al menos 8 caracteres.'); return; }
|
||||
if (passwordForm.newPassword !== passwordForm.confirmPassword) { setPasswordError('Las contraseñas nuevas no coinciden.'); return; }
|
||||
setPasswordSaving(true);
|
||||
try {
|
||||
const res = await fetch('/api/auth/me/password', {
|
||||
method: 'PATCH', headers: { 'Content-Type': 'application/json' }, credentials: 'include',
|
||||
body: JSON.stringify({ currentPassword: passwordForm.currentPassword, newPassword: passwordForm.newPassword }),
|
||||
});
|
||||
const data = await res.json().catch(() => null);
|
||||
if (!res.ok) throw new Error(data?.error?.message ?? 'No se pudo cambiar la contraseña');
|
||||
setPasswordForm({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
||||
setPasswordMessage('Contraseña actualizada correctamente');
|
||||
} catch (error) {
|
||||
setPasswordError(error instanceof Error ? error.message : 'No se pudo cambiar la contraseña');
|
||||
} finally { setPasswordSaving(false); }
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!user || !confirm('¿Eliminar esta dirección?')) return;
|
||||
try {
|
||||
await fetch(`${API}/users/${user.id}/addresses/${id}`, { method: 'DELETE', credentials: 'include' });
|
||||
await fetch(`/api/users/${user.id}/addresses/${id}`, { method: 'DELETE', credentials: 'include' });
|
||||
setMsg('Dirección eliminada');
|
||||
setTimeout(() => setMsg(''), 3000);
|
||||
load();
|
||||
@@ -247,6 +302,45 @@ export default function AccountPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Personal data */}
|
||||
<section className="rounded-xl border border-stone-200 bg-white p-5">
|
||||
<h2 className="mb-4 text-lg font-semibold text-stone-900">Datos personales</h2>
|
||||
<form onSubmit={saveProfile} className="grid gap-4 sm:grid-cols-2">
|
||||
<label className="text-sm font-medium text-stone-700">Nombre
|
||||
<input value={profileForm.displayName} onChange={(e) => setProfileForm(f => ({ ...f, displayName: e.target.value }))} required className="mt-1 w-full rounded-lg border border-stone-300 px-3 py-2 font-normal outline-none focus:ring-2 focus:ring-[#2D6A4F]" />
|
||||
</label>
|
||||
<label className="text-sm font-medium text-stone-700">Teléfono
|
||||
<input value={profileForm.phone} onChange={(e) => setProfileForm(f => ({ ...f, phone: e.target.value }))} className="mt-1 w-full rounded-lg border border-stone-300 px-3 py-2 font-normal outline-none focus:ring-2 focus:ring-[#2D6A4F]" />
|
||||
</label>
|
||||
{profileError && <p className="sm:col-span-2 text-sm text-red-600">{profileError}</p>}
|
||||
<button disabled={profileSaving} className="w-fit rounded-lg bg-[#2D6A4F] px-5 py-2 text-sm font-semibold text-white disabled:opacity-50">{profileSaving ? 'Guardando...' : 'Guardar datos'}</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{/* Password */}
|
||||
<section className="rounded-xl border border-stone-200 bg-white p-5">
|
||||
<div className="mb-4 flex items-center justify-between gap-4">
|
||||
<h2 className="text-lg font-semibold text-stone-900">Contraseña</h2>
|
||||
<a href="/auth/recuperar" className="text-sm text-[#2D6A4F] hover:underline">¿Has olvidado tu contraseña?</a>
|
||||
</div>
|
||||
<form onSubmit={changePassword} className="max-w-xl space-y-4">
|
||||
<label className="block text-sm font-medium text-stone-700">Contraseña actual
|
||||
<input type="password" required value={passwordForm.currentPassword} onChange={(e) => setPasswordForm(f => ({ ...f, currentPassword: e.target.value }))} className="mt-1 w-full rounded-lg border border-stone-300 px-3 py-2 font-normal outline-none focus:ring-2 focus:ring-[#2D6A4F]" />
|
||||
</label>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<label className="text-sm font-medium text-stone-700">Nueva contraseña
|
||||
<input type="password" minLength={8} required value={passwordForm.newPassword} onChange={(e) => setPasswordForm(f => ({ ...f, newPassword: e.target.value }))} className="mt-1 w-full rounded-lg border border-stone-300 px-3 py-2 font-normal outline-none focus:ring-2 focus:ring-[#2D6A4F]" />
|
||||
</label>
|
||||
<label className="text-sm font-medium text-stone-700">Repetir contraseña
|
||||
<input type="password" minLength={8} required value={passwordForm.confirmPassword} onChange={(e) => setPasswordForm(f => ({ ...f, confirmPassword: e.target.value }))} className="mt-1 w-full rounded-lg border border-stone-300 px-3 py-2 font-normal outline-none focus:ring-2 focus:ring-[#2D6A4F]" />
|
||||
</label>
|
||||
</div>
|
||||
{passwordError && <p className="text-sm text-red-600">{passwordError}</p>}
|
||||
{passwordMessage && <p className="text-sm text-green-700">{passwordMessage}</p>}
|
||||
<button disabled={passwordSaving} className="rounded-lg bg-[#2D6A4F] px-5 py-2 text-sm font-semibold text-white disabled:opacity-50">{passwordSaving ? 'Actualizando...' : 'Cambiar contraseña'}</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{/* Addresses */}
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
|
||||
Reference in New Issue
Block a user