305 lines
14 KiB
TypeScript
305 lines
14 KiB
TypeScript
'use client';
|
|
import Link from 'next/link';
|
|
import Image from 'next/image';
|
|
import { useState } from 'react';
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
import { useCart } from '@/contexts/CartContext';
|
|
|
|
function formatPrice(cents: number) {
|
|
return `€${(cents / 100).toFixed(2)}`;
|
|
}
|
|
|
|
export default function CheckoutClient() {
|
|
const { user, loading: authLoading } = useAuth();
|
|
const { items, subtotalCents, itemCount, clearCart } = useCart();
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [error, setError] = useState('');
|
|
|
|
// Form state
|
|
const [form, setForm] = useState({
|
|
firstName: '',
|
|
lastName: '',
|
|
email: user?.email ?? '',
|
|
phone: '',
|
|
address: '',
|
|
city: '',
|
|
postalCode: '',
|
|
notes: '',
|
|
shippingMethod: 'standard',
|
|
});
|
|
|
|
if (authLoading) {
|
|
return (
|
|
<div className="max-w-7xl mx-auto px-4 py-16 flex items-center justify-center">
|
|
<div className="text-gray-500">Cargando...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (items.length === 0) {
|
|
return (
|
|
<div className="max-w-2xl mx-auto px-4 py-16 text-center">
|
|
<div className="text-6xl mb-4">🛒</div>
|
|
<h1 className="text-2xl font-bold text-gray-900 mb-2">Tu carrito está vacío</h1>
|
|
<p className="text-gray-500 mb-8">Añade productos antes de hacer el pedido.</p>
|
|
<Link href="/products" className="inline-flex px-6 py-3 bg-[#70ad47] hover:bg-[#5a9040] text-white font-semibold rounded-xl transition-colors">
|
|
Ver productos
|
|
</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const shippingCost = form.shippingMethod === 'express' ? 899 : 499;
|
|
const totalCents = subtotalCents + shippingCost;
|
|
|
|
const handlePlaceOrder = async () => {
|
|
setSubmitting(true);
|
|
setError('');
|
|
try {
|
|
const res = await fetch('/api/checkout', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify({
|
|
shippingAddress: {
|
|
firstName: form.firstName,
|
|
lastName: form.lastName,
|
|
email: form.email,
|
|
phone: form.phone,
|
|
line1: form.address,
|
|
city: form.city,
|
|
postalCode: form.postalCode,
|
|
country: 'ES',
|
|
},
|
|
items: items.map((i) => ({
|
|
productId: i.productId,
|
|
variantId: i.variantId,
|
|
quantity: i.quantity,
|
|
})),
|
|
shippingMethod: form.shippingMethod,
|
|
notes: form.notes,
|
|
}),
|
|
});
|
|
if (res.status === 401) {
|
|
setError('Debes iniciar sesión para finalizar el pedido.');
|
|
return;
|
|
}
|
|
if (!res.ok) {
|
|
const data = await res.json().catch(() => ({}));
|
|
throw new Error(data.error?.message || 'Error al procesar el pedido');
|
|
}
|
|
const { orderId } = await res.json();
|
|
clearCart();
|
|
window.location.href = `/order-confirmation?orderId=${orderId}`;
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Error al procesar el pedido');
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<h1 className="text-3xl font-bold text-gray-900 mb-8" style={{ fontFamily: 'var(--font-heading)' }}>
|
|
Finalizar pedido
|
|
</h1>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
|
{/* Form */}
|
|
<div>
|
|
{/* Login prompt — solo si NO está autenticado */}
|
|
{!user && (
|
|
<div className="bg-[#70ad47]/5 border border-[#70ad47]/20 rounded-xl p-6 mb-6">
|
|
<h2 className="font-bold text-gray-900 mb-1">¿Ya tienes cuenta?</h2>
|
|
<p className="text-sm text-gray-600 mb-3">Inicia sesión para una experiencia más rápida.</p>
|
|
<div className="flex gap-3">
|
|
<Link href="/auth/login" className="px-4 py-2 bg-[#70ad47] hover:bg-[#5a9040] text-white text-sm font-semibold rounded-lg transition-colors">
|
|
Iniciar sesión
|
|
</Link>
|
|
<Link href="/auth/register" className="px-4 py-2 border border-gray-300 hover:border-[#70ad47] text-gray-700 text-sm font-semibold rounded-lg transition-colors">
|
|
Crear cuenta
|
|
</Link>
|
|
</div>
|
|
</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>
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Nombre *</label>
|
|
<input
|
|
type="text" required placeholder="María"
|
|
value={form.firstName}
|
|
onChange={e => setForm(f => ({ ...f, firstName: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#70ad47] focus:border-transparent outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Apellidos *</label>
|
|
<input
|
|
type="text" required placeholder="García López"
|
|
value={form.lastName}
|
|
onChange={e => setForm(f => ({ ...f, lastName: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#70ad47] focus:border-transparent outline-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Email *</label>
|
|
<input
|
|
type="email" required placeholder="maria@ejemplo.com"
|
|
value={form.email}
|
|
onChange={e => setForm(f => ({ ...f, email: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#70ad47] focus:border-transparent outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Teléfono *</label>
|
|
<input
|
|
type="tel" required placeholder="+34 600 000 000"
|
|
value={form.phone}
|
|
onChange={e => setForm(f => ({ ...f, phone: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#70ad47] focus:border-transparent outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Dirección *</label>
|
|
<input
|
|
type="text" required placeholder="Calle Gran Vía 42"
|
|
value={form.address}
|
|
onChange={e => setForm(f => ({ ...f, address: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#70ad47] focus:border-transparent outline-none"
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Ciudad *</label>
|
|
<input
|
|
type="text" required placeholder="Madrid"
|
|
value={form.city}
|
|
onChange={e => setForm(f => ({ ...f, city: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#70ad47] focus:border-transparent outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">CP *</label>
|
|
<input
|
|
type="text" required placeholder="28013"
|
|
value={form.postalCode}
|
|
onChange={e => setForm(f => ({ ...f, postalCode: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#70ad47] focus:border-transparent outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">País</label>
|
|
<input type="text" disabled value="España"
|
|
className="w-full px-3 py-2 border border-gray-200 rounded-lg bg-gray-50 text-gray-500" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border-t border-gray-200 pt-4 mt-4">
|
|
<h3 className="font-semibold text-gray-900 mb-3">Método de envío</h3>
|
|
<div className="space-y-2">
|
|
{[
|
|
{ id: 'standard', name: 'Estándar', desc: 'Entrega 3-5 días laborables', price: '€4.99' },
|
|
{ id: 'express', name: 'Express 24h', desc: 'Entrega al día siguiente', price: '€8.99' },
|
|
].map(opt => (
|
|
<label key={opt.id}
|
|
className={`flex items-center gap-3 p-3 border rounded-lg cursor-pointer transition-colors ${form.shippingMethod === opt.id ? 'border-[#70ad47] bg-[#70ad47]/5' : 'border-gray-200 hover:border-[#70ad47]'}`}>
|
|
<input type="radio" name="shipping" value={opt.id} checked={form.shippingMethod === opt.id}
|
|
onChange={e => setForm(f => ({ ...f, shippingMethod: e.target.value }))}
|
|
className="text-[#70ad47]" />
|
|
<div className="flex-1">
|
|
<p className="font-medium text-gray-900">{opt.name}</p>
|
|
<p className="text-sm text-gray-500">{opt.desc}</p>
|
|
</div>
|
|
<span className="font-semibold text-[#70ad47]">{opt.price}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border-t border-gray-200 pt-4">
|
|
<h3 className="font-semibold text-gray-900 mb-3">Notas del pedido</h3>
|
|
<textarea rows={3} placeholder="Observaciones, instrucciones de entrega..."
|
|
value={form.notes}
|
|
onChange={e => setForm(f => ({ ...f, notes: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#70ad47] focus:border-transparent outline-none resize-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Order summary */}
|
|
<div>
|
|
<div className="bg-gray-50 rounded-xl border border-gray-200 p-6 sticky top-24">
|
|
<h2 className="font-bold text-gray-900 mb-4">Tu pedido ({itemCount})</h2>
|
|
|
|
<div className="space-y-3 max-h-80 overflow-y-auto mb-4">
|
|
{items.map(item => (
|
|
<div key={item.variantId} className="flex gap-3">
|
|
<div className="w-12 h-12 bg-white rounded-lg overflow-hidden flex-shrink-0 flex items-center justify-center">
|
|
{item.imageUrl
|
|
? <Image src={item.imageUrl} alt={item.productName} width={48} height={48} className="object-cover" />
|
|
: <span className="text-2xl">🌿</span>
|
|
}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium text-gray-900 line-clamp-1">{item.productName}</p>
|
|
<p className="text-xs text-gray-500">Cantidad: {item.quantity}</p>
|
|
</div>
|
|
<p className="text-sm font-bold text-gray-900 flex-shrink-0">
|
|
{formatPrice(item.priceCents * item.quantity)}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="border-t border-gray-200 pt-4 space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Subtotal</span>
|
|
<span className="font-medium">{formatPrice(subtotalCents)}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Envío</span>
|
|
<span className="text-gray-500">{form.shippingMethod === 'express' ? '€8.99' : '€4.99'}</span>
|
|
</div>
|
|
<div className="flex justify-between text-gray-600">
|
|
<span>IVA</span>
|
|
<span>Incluido</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border-t border-gray-200 mt-4 pt-4 flex justify-between items-center">
|
|
<span className="font-bold text-gray-900">Total</span>
|
|
<span className="text-2xl font-bold text-[#70ad47]">{formatPrice(totalCents)}</span>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="mt-3 bg-red-50 border border-red-200 text-red-700 text-sm px-4 py-2.5 rounded-xl">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
onClick={handlePlaceOrder}
|
|
disabled={submitting || !form.firstName || !form.email || !form.address || !form.city || !form.postalCode}
|
|
className="mt-4 w-full py-3.5 bg-[#70ad47] hover:bg-[#5a9040] disabled:bg-gray-300 disabled:text-gray-500 text-white font-semibold rounded-xl transition-colors cursor-pointer"
|
|
>
|
|
{submitting ? 'Procesando...' : 'Finalizar pedido'}
|
|
</button>
|
|
|
|
<Link href="/cart" className="mt-3 w-full block text-center text-sm text-gray-500 hover:text-[#70ad47] transition-colors">
|
|
← Volver al carrito
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|