feat(F-067): completed feature
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useCart } from '@/contexts/CartContext';
|
||||
|
||||
@@ -9,12 +9,47 @@ function formatPrice(cents: number) {
|
||||
return `€${(cents / 100).toFixed(2)}`;
|
||||
}
|
||||
|
||||
interface SavedAddress {
|
||||
id: string;
|
||||
label: string | null;
|
||||
recipientName: string;
|
||||
street: string;
|
||||
city: string;
|
||||
postalCode: string;
|
||||
country: string;
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
function splitName(fullName: string): { firstName: string; lastName: string } {
|
||||
const trimmed = fullName.trim().replace(/\s+/g, ' ');
|
||||
if (!trimmed) return { firstName: '', lastName: '' };
|
||||
const parts = trimmed.split(' ');
|
||||
if (parts.length === 1) return { firstName: parts[0], lastName: '' };
|
||||
return { firstName: parts.slice(0, -1).join(' '), lastName: parts[parts.length - 1] };
|
||||
}
|
||||
|
||||
function addressToForm(addr: SavedAddress) {
|
||||
const { firstName, lastName } = splitName(addr.recipientName);
|
||||
return {
|
||||
firstName,
|
||||
lastName,
|
||||
phone: '',
|
||||
address: addr.street,
|
||||
city: addr.city,
|
||||
postalCode: addr.postalCode,
|
||||
country: addr.country,
|
||||
};
|
||||
}
|
||||
|
||||
export default function CheckoutClient() {
|
||||
const { user, loading: authLoading } = useAuth();
|
||||
const { items, subtotalCents, itemCount, clearCart } = useCart();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const [addresses, setAddresses] = useState<SavedAddress[]>([]);
|
||||
const [selectedAddressId, setSelectedAddressId] = useState<string | null>(null);
|
||||
|
||||
// Form state
|
||||
const [form, setForm] = useState({
|
||||
firstName: '',
|
||||
@@ -28,6 +63,47 @@ export default function CheckoutClient() {
|
||||
shippingMethod: 'standard',
|
||||
});
|
||||
|
||||
// Fetch saved addresses when the customer is logged in.
|
||||
useEffect(() => {
|
||||
if (!user || user.role !== 'customer') {
|
||||
setAddresses([]);
|
||||
setSelectedAddressId(null);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/users/${user.id}/addresses`, { credentials: 'include', cache: 'no-store' });
|
||||
if (!res.ok) return;
|
||||
const data = (await res.json()) as { items?: SavedAddress[] };
|
||||
if (cancelled) return;
|
||||
const list = data.items ?? [];
|
||||
setAddresses(list);
|
||||
const def = list.find((a) => a.isDefault) ?? list[0] ?? null;
|
||||
if (def) {
|
||||
setSelectedAddressId(def.id);
|
||||
setForm((f) => ({ ...f, ...addressToForm(def) }));
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [user]);
|
||||
|
||||
const selectedAddress = useMemo(
|
||||
() => addresses.find((a) => a.id === selectedAddressId) ?? null,
|
||||
[addresses, selectedAddressId],
|
||||
);
|
||||
|
||||
const handleSelectAddress = (id: string) => {
|
||||
setSelectedAddressId(id);
|
||||
const addr = addresses.find((a) => a.id === id);
|
||||
if (addr) setForm((f) => ({ ...f, ...addressToForm(addr) }));
|
||||
};
|
||||
|
||||
if (authLoading) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 py-16 flex items-center justify-center">
|
||||
@@ -69,7 +145,7 @@ export default function CheckoutClient() {
|
||||
line1: form.address,
|
||||
city: form.city,
|
||||
postalCode: form.postalCode,
|
||||
country: 'ES',
|
||||
country: selectedAddress?.country || form.shippingMethod ? 'ES' : 'ES',
|
||||
},
|
||||
items: items.map((i) => ({
|
||||
productId: i.productId,
|
||||
@@ -123,6 +199,51 @@ export default function CheckoutClient() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Saved addresses selector — only when logged in */}
|
||||
{user && addresses.length > 0 && (
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 mb-6">
|
||||
<h2 className="font-bold text-gray-900 mb-3">Direcciones guardadas</h2>
|
||||
<div className="space-y-2">
|
||||
{addresses.map((a) => {
|
||||
const active = selectedAddressId === a.id;
|
||||
return (
|
||||
<label
|
||||
key={a.id}
|
||||
className={`flex items-start gap-3 p-3 border rounded-lg cursor-pointer transition-colors ${
|
||||
active ? 'border-[#70ad47] bg-[#70ad47]/5' : 'border-gray-200 hover:border-[#70ad47]'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="saved-address"
|
||||
value={a.id}
|
||||
checked={active}
|
||||
onChange={() => handleSelectAddress(a.id)}
|
||||
className="text-[#70ad47] mt-1"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-medium text-gray-900">{a.recipientName}</p>
|
||||
{a.label && (
|
||||
<span className="px-2 py-0.5 bg-gray-100 text-gray-600 text-xs rounded-full">{a.label}</span>
|
||||
)}
|
||||
{a.isDefault && (
|
||||
<span className="px-2 py-0.5 bg-green-100 text-green-700 text-xs rounded-full">Predeterminada</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">{a.street}</p>
|
||||
<p className="text-sm text-gray-500">{a.postalCode} {a.city}, {a.country}</p>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-3">
|
||||
También puedes editar los campos manualmente debajo; se aplicarán al pedido sin guardar nada en tu cuenta.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Shipping form */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6">
|
||||
<h2 className="font-bold text-gray-900 mb-4">Datos de envío</h2>
|
||||
@@ -301,4 +422,4 @@ export default function CheckoutClient() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user