Initial commit – Cursa de la Cirera 2026 e-commerce

Next.js 14 + Prisma + Stripe + Google Sheets + Nodemailer
This commit is contained in:
2026-06-17 12:02:28 +02:00
commit 60f6e9d2e1
45 changed files with 9974 additions and 0 deletions
+785
View File
@@ -0,0 +1,785 @@
'use client'
import { useState, useRef, useEffect } from 'react'
import Image from 'next/image'
import { useSearchParams } from 'next/navigation'
import { Suspense } from 'react'
import { Ruler, ChevronDown, Check, AlertCircle, Loader2 } from 'lucide-react'
import Header from '@/components/Header'
import Footer from '@/components/Footer'
import CountdownBanner from '@/components/CountdownBanner'
import TrustSection from '@/components/TrustSection'
import SizeGuideModal from '@/components/SizeGuideModal'
import clsx from 'clsx'
// ─── Config ───────────────────────────────────────────────────────────────────
const PRODUCTS = {
samarreta: {
id: 'samarreta',
name: 'Samarreta',
fullName: 'Samarreta Cursa de la Cirera 2026',
price: 10.0,
image: '/assets/Samarreta.jpeg',
features: ['Teixit tècnic Dry Fit', 'Running Mesh Yarn', 'Samarreta Fullprint', 'Mànica recta'],
hasTshirt: true,
hasSocks: false,
badge: null,
},
mitjons: {
id: 'mitjons',
name: 'Mitjons',
fullName: 'Mitjons Cursa de la Cirera 2026',
price: 10.0,
image: '/assets/Mitjons.jpeg',
features: ['Blood Bros Socks', 'Alta qualitat', 'Disseny exclusiu', 'Talles XXSXL'],
hasTshirt: false,
hasSocks: true,
badge: null,
},
pack: {
id: 'pack',
name: 'Pack Complet',
fullName: 'Pack Samarreta + Mitjons',
price: 18.5,
image: '/assets/Pack Samarreta+Mitjons.jpeg',
features: ['Samarreta + Mitjons', 'Estalvia 1,50€', 'Tot l\'equipament', 'Millor preu'],
hasTshirt: true,
hasSocks: true,
badge: 'OFERTA',
},
} as const
type ProductId = keyof typeof PRODUCTS
const TSHIRT_SIZES = ['S', 'M', 'L', 'XL', 'XXL', '3XL']
const SOCK_SIZES = [
{ label: 'XXS', range: '32-34' },
{ label: 'XS', range: '35-37' },
{ label: 'S', range: '38-39' },
{ label: 'M', range: '40-42' },
{ label: 'L', range: '43-44' },
{ label: 'XL', range: '45-47' },
]
const PROVINCES = [
'Lleida', 'Barcelona', 'Girona', 'Tarragona',
'Aragó', 'Madrid', 'Valencia', 'Altres',
]
interface FormData {
nom: string
cognoms: string
telefon: string
email: string
adreca: string
codiPostal: string
poblacio: string
provincia: string
}
const EMPTY_FORM: FormData = {
nom: '', cognoms: '', telefon: '', email: '',
adreca: '', codiPostal: '', poblacio: '', provincia: '',
}
// ─── Cancelled Banner ─────────────────────────────────────────────────────────
function CancelledBanner() {
const params = useSearchParams()
const [visible, setVisible] = useState(false)
useEffect(() => {
if (params.get('cancelled') === 'true') setVisible(true)
}, [params])
if (!visible) return null
return (
<div className="max-w-3xl mx-auto px-4 sm:px-6 mt-6">
<div className="flex items-start gap-3 bg-yellow-500/10 border border-yellow-500/30 rounded-xl p-4">
<AlertCircle className="text-yellow-400 shrink-0 mt-0.5" size={18} />
<div>
<p className="text-yellow-300 font-semibold text-sm">Pagament cancel·lat</p>
<p className="text-yellow-400/70 text-xs mt-0.5">
Has cancel·lat el pagament. Torna a omplir el formulari quan vulguis per completar la teva comanda.
</p>
</div>
<button
onClick={() => setVisible(false)}
className="ml-auto text-yellow-500 hover:text-yellow-300 transition-colors shrink-0"
>
</button>
</div>
</div>
)
}
// ─── Main Page ────────────────────────────────────────────────────────────────
export default function HomePage() {
const [selectedProduct, setSelectedProduct] = useState<ProductId | null>(null)
const [sizeTshirt, setSizeTshirt] = useState('')
const [sizeSocks, setSizeSocks] = useState('')
const [formData, setFormData] = useState<FormData>(EMPTY_FORM)
const [shipping, setShipping] = useState<'correos' | 'recollida' | null>(null)
const [sizeGuide, setSizeGuide] = useState<'samarreta' | 'mitjons' | null>(null)
const [loading, setLoading] = useState(false)
const [errors, setErrors] = useState<Partial<Record<keyof FormData | 'product' | 'sizes' | 'shipping', string>>>({})
const sizesRef = useRef<HTMLDivElement>(null)
const formRef = useRef<HTMLDivElement>(null)
const shippingRef = useRef<HTMLDivElement>(null)
const summaryRef = useRef<HTMLDivElement>(null)
const product = selectedProduct ? PRODUCTS[selectedProduct] : null
const shippingCost = shipping === 'correos' ? 7.99 : 0
const total = product ? product.price + shippingCost : 0
function scrollTo(ref: React.RefObject<HTMLDivElement>) {
setTimeout(() => ref.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 100)
}
function handleSelectProduct(id: ProductId) {
setSelectedProduct(id)
setSizeTshirt('')
setSizeSocks('')
setShipping(null)
setErrors({})
scrollTo(sizesRef)
}
function handleSizeTshirt(s: string) {
setSizeTshirt(s)
if (!product?.hasSocks) scrollTo(formRef)
else if (sizeSocks) scrollTo(formRef)
}
function handleSizeSocks(s: string) {
setSizeSocks(s)
if (!product?.hasTshirt) scrollTo(formRef)
else if (sizeTshirt) scrollTo(formRef)
}
function handleField(key: keyof FormData, value: string) {
setFormData(prev => ({ ...prev, [key]: value }))
if (errors[key]) setErrors(prev => ({ ...prev, [key]: '' }))
}
function handleShipping(v: 'correos' | 'recollida') {
setShipping(v)
scrollTo(summaryRef)
}
function validate(): boolean {
const newErrors: typeof errors = {}
if (!selectedProduct) newErrors.product = 'Selecciona un producte'
if (product?.hasTshirt && !sizeTshirt) newErrors.sizes = 'Selecciona la talla de la samarreta'
if (product?.hasSocks && !sizeSocks) newErrors.sizes = (newErrors.sizes ? newErrors.sizes + ' i els ' : '') + 'Selecciona la talla dels mitjons'
if (!formData.nom.trim()) newErrors.nom = 'El nom és obligatori'
if (!formData.cognoms.trim()) newErrors.cognoms = 'Els cognoms són obligatoris'
if (!formData.telefon.trim()) newErrors.telefon = 'El telèfon és obligatori'
if (!formData.email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email))
newErrors.email = 'Introdueix un email vàlid'
if (!shipping) newErrors.shipping = 'Selecciona una opció de lliurament'
if (shipping === 'correos') {
if (!formData.adreca.trim()) newErrors.adreca = "L'adreça és obligatòria per a enviament"
if (!formData.codiPostal.trim()) newErrors.codiPostal = 'El codi postal és obligatori'
if (!formData.poblacio.trim()) newErrors.poblacio = 'La població és obligatòria'
if (!formData.provincia.trim()) newErrors.provincia = 'La província és obligatòria'
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
async function handlePay() {
if (!validate()) {
const firstError = document.querySelector('[data-error]')
firstError?.scrollIntoView({ behavior: 'smooth', block: 'center' })
return
}
setLoading(true)
try {
const res = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
product: selectedProduct,
sizeTshirt: sizeTshirt || null,
sizeSocks: sizeSocks || null,
shipping,
...formData,
}),
})
const data = await res.json()
if (!res.ok) throw new Error(data.error ?? 'Error desconegut')
window.location.href = data.url
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Error inesperat. Torna-ho a intentar.'
setErrors({ product: message })
setLoading(false)
}
}
const needsTshirtSize = product?.hasTshirt ?? false
const needsSockSize = product?.hasSocks ?? false
const sizesComplete = (!needsTshirtSize || sizeTshirt) && (!needsSockSize || sizeSocks)
const formComplete =
formData.nom && formData.cognoms && formData.telefon && formData.email &&
(shipping !== 'correos' || (formData.adreca && formData.codiPostal && formData.poblacio && formData.provincia))
const canPay = selectedProduct && sizesComplete && formComplete && shipping
return (
<>
<Header />
<CountdownBanner />
<main className="min-h-screen">
<Suspense fallback={null}>
<CancelledBanner />
</Suspense>
{/* ─── Hero ─────────────────────────────────────────────── */}
<section className="relative overflow-hidden">
<div
className="absolute inset-0 pointer-events-none"
style={{
background:
'radial-gradient(ellipse 80% 60% at 70% 40%, rgba(0,200,220,0.07) 0%, transparent 60%), radial-gradient(ellipse 60% 50% at 30% 60%, rgba(232,64,64,0.05) 0%, transparent 50%)',
}}
/>
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-12 sm:py-20 grid lg:grid-cols-2 gap-10 items-center">
{/* Text side */}
<div className="order-2 lg:order-1">
<p className="section-title">Blood Bros Sport presenta</p>
<h1 className="text-5xl sm:text-6xl lg:text-7xl font-black text-white leading-none mb-4">
Cursa<br />
<span className="text-teal">de la</span><br />
Cirera
</h1>
<p className="text-xl sm:text-2xl font-black text-slate-400 mb-6">
Corbins 2026
</p>
<p className="text-slate-500 text-base mb-8 max-w-md leading-relaxed">
L'equipament oficial de la Cursa. Samarreta tècnica Fullprint i mitjons exclusius Blood Bros Socks.
Edició limitada. Comandes fins al 25 de juny.
</p>
<div className="flex flex-wrap gap-3">
<button
onClick={() => document.getElementById('productes')?.scrollIntoView({ behavior: 'smooth' })}
className="btn-pay w-auto px-8 py-3 text-base"
>
Compra ara
</button>
<div className="flex items-center gap-2 px-4 py-3 bg-dark-card border border-dark-border rounded-xl">
<Check size={16} className="text-teal" />
<span className="text-sm text-slate-400">Pagament segur via Stripe</span>
</div>
</div>
</div>
{/* Image side */}
<div className="order-1 lg:order-2 flex justify-center">
<div className="relative w-72 sm:w-96">
<div
className="absolute inset-0 rounded-3xl blur-3xl opacity-30"
style={{ background: 'linear-gradient(135deg, #00C8DC, #E84040)' }}
/>
<Image
src="/assets/Pack Samarreta+Mitjons.jpeg"
alt="Pack Samarreta + Mitjons Cursa de la Cirera 2026"
width={420}
height={560}
className="relative z-10 float-anim object-contain w-full"
priority
/>
</div>
</div>
</div>
</section>
{/* ─── STEP 1: PRODUCTES ────────────────────────────────── */}
<section id="productes" className="max-w-6xl mx-auto px-4 sm:px-6 pb-16">
<div className="mb-8">
<p className="section-title">Pas 1 de 4 · Producte</p>
<h2 className="text-3xl sm:text-4xl font-black text-white">
Quin equipament vols?
</h2>
{errors.product && (
<p data-error className="text-cherry text-sm mt-2 flex items-center gap-1">
<AlertCircle size={14} /> {errors.product}
</p>
)}
</div>
<div className="grid sm:grid-cols-3 gap-5">
{(Object.values(PRODUCTS) as typeof PRODUCTS[keyof typeof PRODUCTS][]).map((p) => (
<button
key={p.id}
onClick={() => handleSelectProduct(p.id as ProductId)}
className={clsx('product-card text-left', { selected: selectedProduct === p.id })}
>
{p.badge && (
<div className="absolute top-4 right-4">
<span className="badge badge-red">{p.badge}</span>
</div>
)}
<div className="relative mb-4 rounded-xl overflow-hidden bg-dark aspect-square">
<Image
src={p.image}
alt={p.fullName}
fill
className="object-cover transition-transform duration-500 group-hover:scale-105"
sizes="(max-width: 640px) 90vw, 30vw"
/>
{selectedProduct === p.id && (
<div className="absolute inset-0 bg-teal/10 flex items-center justify-center">
<div className="w-10 h-10 rounded-full bg-teal flex items-center justify-center">
<Check size={20} className="text-dark" style={{ color: '#000' }} />
</div>
</div>
)}
</div>
<h3 className="text-white font-black text-lg mb-1">{p.name}</h3>
<div className="text-2xl font-black text-teal mb-3">
{p.price.toFixed(2).replace('.', ',')}€
</div>
{p.id === 'pack' && (
<p className="text-slate-500 text-xs mb-2 line-through">vs 20,00€ per separat</p>
)}
<ul className="space-y-1">
{p.features.map((f) => (
<li key={f} className="flex items-center gap-2 text-slate-500 text-xs">
<span className="w-1 h-1 rounded-full bg-teal shrink-0" />
{f}
</li>
))}
</ul>
</button>
))}
</div>
</section>
{/* ─── STEP 2: TALLES ───────────────────────────────────── */}
<div ref={sizesRef} />
{selectedProduct && (
<section className="max-w-6xl mx-auto px-4 sm:px-6 pb-16 animate-fade-up">
<div className="mb-8">
<p className="section-title">Pas 2 de 4 · Talles</p>
<h2 className="text-3xl sm:text-4xl font-black text-white">
La teva talla
</h2>
{errors.sizes && (
<p data-error className="text-cherry text-sm mt-2 flex items-center gap-1">
<AlertCircle size={14} /> {errors.sizes}
</p>
)}
</div>
<div className={clsx('grid gap-8', product?.hasTshirt && product?.hasSocks ? 'sm:grid-cols-2' : 'sm:grid-cols-1 max-w-md')}>
{/* Tshirt sizes */}
{product?.hasTshirt && (
<div>
<div className="flex items-center justify-between mb-4">
<h3 className="text-white font-bold">Samarreta</h3>
<button
onClick={() => setSizeGuide('samarreta')}
className="flex items-center gap-1.5 text-teal text-xs hover:underline"
>
<Ruler size={14} />
Guia de talles
</button>
</div>
<div className="flex flex-wrap gap-2">
{TSHIRT_SIZES.map((s) => (
<button
key={s}
onClick={() => handleSizeTshirt(s)}
className={clsx('size-btn', { selected: sizeTshirt === s })}
>
{s}
</button>
))}
</div>
{sizeTshirt && (
<p className="text-teal text-xs mt-3 flex items-center gap-1">
<Check size={12} /> Talla {sizeTshirt} seleccionada
</p>
)}
</div>
)}
{/* Socks sizes */}
{product?.hasSocks && (
<div>
<div className="flex items-center justify-between mb-4">
<h3 className="text-white font-bold">Mitjons</h3>
<button
onClick={() => setSizeGuide('mitjons')}
className="flex items-center gap-1.5 text-teal text-xs hover:underline"
>
<Ruler size={14} />
Guia de talles
</button>
</div>
<div className="flex flex-wrap gap-2">
{SOCK_SIZES.map(({ label, range }) => (
<button
key={label}
onClick={() => handleSizeSocks(label)}
className={clsx('size-btn flex flex-col items-center', { selected: sizeSocks === label })}
>
<span>{label}</span>
<span className="text-[10px] opacity-60">{range}</span>
</button>
))}
</div>
{sizeSocks && (
<p className="text-teal text-xs mt-3 flex items-center gap-1">
<Check size={12} /> Talla {sizeSocks} seleccionada
</p>
)}
</div>
)}
</div>
</section>
)}
{/* ─── STEP 3: DADES ────────────────────────────────────── */}
<div ref={formRef} />
{selectedProduct && sizesComplete && (
<section className="max-w-6xl mx-auto px-4 sm:px-6 pb-16 animate-fade-up">
<div className="mb-8">
<p className="section-title">Pas 3 de 4 · Les teves dades</p>
<h2 className="text-3xl sm:text-4xl font-black text-white">
On t'enviem la comanda?
</h2>
</div>
<div className="max-w-2xl">
<div className="grid sm:grid-cols-2 gap-4">
{/* Nom */}
<div>
<label className="form-label">Nom *</label>
<input
type="text"
className={clsx('input-field', errors.nom && 'border-cherry')}
placeholder="El teu nom"
value={formData.nom}
onChange={(e) => handleField('nom', e.target.value)}
autoComplete="given-name"
/>
{errors.nom && (
<p data-error className="text-cherry text-xs mt-1">{errors.nom}</p>
)}
</div>
{/* Cognoms */}
<div>
<label className="form-label">Cognoms *</label>
<input
type="text"
className={clsx('input-field', errors.cognoms && 'border-cherry')}
placeholder="Els teus cognoms"
value={formData.cognoms}
onChange={(e) => handleField('cognoms', e.target.value)}
autoComplete="family-name"
/>
{errors.cognoms && (
<p data-error className="text-cherry text-xs mt-1">{errors.cognoms}</p>
)}
</div>
{/* Telèfon */}
<div>
<label className="form-label">Telèfon mòbil *</label>
<input
type="tel"
className={clsx('input-field', errors.telefon && 'border-cherry')}
placeholder="6XX XXX XXX"
value={formData.telefon}
onChange={(e) => handleField('telefon', e.target.value)}
autoComplete="tel"
/>
{errors.telefon && (
<p data-error className="text-cherry text-xs mt-1">{errors.telefon}</p>
)}
</div>
{/* Email */}
<div>
<label className="form-label">Correu electrònic *</label>
<input
type="email"
className={clsx('input-field', errors.email && 'border-cherry')}
placeholder="tu@exemple.com"
value={formData.email}
onChange={(e) => handleField('email', e.target.value)}
autoComplete="email"
/>
{errors.email && (
<p data-error className="text-cherry text-xs mt-1">{errors.email}</p>
)}
</div>
{/* Adreça */}
<div className="sm:col-span-2">
<label className="form-label">
Adreça postal
<span className="text-slate-600 normal-case font-normal ml-1">(per enviament a domicili)</span>
</label>
<input
type="text"
className={clsx('input-field', errors.adreca && 'border-cherry')}
placeholder="Carrer, número, pis..."
value={formData.adreca}
onChange={(e) => handleField('adreca', e.target.value)}
autoComplete="street-address"
/>
{errors.adreca && (
<p data-error className="text-cherry text-xs mt-1">{errors.adreca}</p>
)}
</div>
{/* CP */}
<div>
<label className="form-label">Codi postal</label>
<input
type="text"
className={clsx('input-field', errors.codiPostal && 'border-cherry')}
placeholder="25XXX"
value={formData.codiPostal}
onChange={(e) => handleField('codiPostal', e.target.value)}
maxLength={5}
autoComplete="postal-code"
/>
{errors.codiPostal && (
<p data-error className="text-cherry text-xs mt-1">{errors.codiPostal}</p>
)}
</div>
{/* Població */}
<div>
<label className="form-label">Població</label>
<input
type="text"
className={clsx('input-field', errors.poblacio && 'border-cherry')}
placeholder="Corbins, Lleida..."
value={formData.poblacio}
onChange={(e) => handleField('poblacio', e.target.value)}
autoComplete="address-level2"
/>
{errors.poblacio && (
<p data-error className="text-cherry text-xs mt-1">{errors.poblacio}</p>
)}
</div>
{/* Província */}
<div className="sm:col-span-2">
<label className="form-label">Província</label>
<select
className={clsx('input-field', errors.provincia && 'border-cherry')}
value={formData.provincia}
onChange={(e) => handleField('provincia', e.target.value)}
>
<option value="">Selecciona una província...</option>
{PROVINCES.map((p) => (
<option key={p} value={p}>{p}</option>
))}
</select>
{errors.provincia && (
<p data-error className="text-cherry text-xs mt-1">{errors.provincia}</p>
)}
</div>
</div>
{formComplete && (
<div className="mt-4 flex items-center gap-2 text-teal text-sm">
<Check size={16} />
<span>Dades correctes. Ara selecciona com vols rebre la comanda.</span>
</div>
)}
</div>
</section>
)}
{/* ─── STEP 4: ENVIAMENT ────────────────────────────────── */}
<div ref={shippingRef} />
{selectedProduct && sizesComplete && formData.nom && (
<section className="max-w-6xl mx-auto px-4 sm:px-6 pb-16 animate-fade-up">
<div className="mb-8">
<p className="section-title">Pas 4 de 4 · Lliurament</p>
<h2 className="text-3xl sm:text-4xl font-black text-white">
Com vols rebre-ho?
</h2>
{errors.shipping && (
<p data-error className="text-cherry text-sm mt-2 flex items-center gap-1">
<AlertCircle size={14} /> {errors.shipping}
</p>
)}
</div>
<div className="flex flex-col sm:flex-row gap-4 max-w-2xl">
{/* Correos Express */}
<button
onClick={() => handleShipping('correos')}
className={clsx('shipping-card text-left', { selected: shipping === 'correos' })}
>
<div className="flex items-start justify-between mb-3">
<div className="text-2xl">🚚</div>
{shipping === 'correos' && (
<div className="w-5 h-5 rounded-full bg-teal flex items-center justify-center shrink-0">
<Check size={12} style={{ color: '#000' }} />
</div>
)}
</div>
<h3 className="text-white font-bold mb-1">Correos Express</h3>
<p className="text-slate-500 text-sm mb-3">Enviament a casa teva. Lliurament en 24-48h hàbils.</p>
<div className="text-cherry font-black text-xl">+7,99</div>
<p className="text-slate-600 text-xs mt-1">Lliurament previst: 29 Juny 3 Juliol</p>
</button>
{/* Recollida */}
<button
onClick={() => handleShipping('recollida')}
className={clsx('shipping-card text-left', { selected: shipping === 'recollida' })}
>
<div className="flex items-start justify-between mb-3">
<div className="text-2xl">📍</div>
{shipping === 'recollida' && (
<div className="w-5 h-5 rounded-full bg-teal flex items-center justify-center shrink-0">
<Check size={12} style={{ color: '#000' }} />
</div>
)}
</div>
<h3 className="text-white font-bold mb-1">Recollida Corbins / Lleida</h3>
<p className="text-slate-500 text-sm mb-3">Vés a buscar la comanda. Ens posem en contacte amb tu per coordinar.</p>
<div className="text-teal font-black text-xl">Gratuït</div>
<p className="text-slate-600 text-xs mt-1">Lliurament previst: 29 Juny 3 Juliol</p>
</button>
</div>
</section>
)}
{/* ─── RESUM I PAGAMENT ─────────────────────────────────── */}
<div ref={summaryRef} />
{selectedProduct && sizesComplete && formData.nom && shipping && (
<section className="max-w-6xl mx-auto px-4 sm:px-6 pb-20 animate-fade-up">
<div className="mb-8">
<p className="section-title">Resum de la comanda</p>
<h2 className="text-3xl sm:text-4xl font-black text-white">
Llest per pagar?
</h2>
</div>
<div className="max-w-lg">
<div className="bg-dark-card border border-dark-border rounded-2xl overflow-hidden">
{/* Summary items */}
<div className="p-6 space-y-4">
<div className="flex items-center gap-4">
<div className="w-16 h-16 rounded-xl overflow-hidden bg-dark shrink-0">
<Image
src={product!.image}
alt={product!.fullName}
width={64}
height={64}
className="w-full h-full object-cover"
/>
</div>
<div className="flex-1">
<div className="text-white font-bold text-sm">{product!.fullName}</div>
<div className="text-slate-500 text-xs mt-0.5">
{sizeTshirt && `Samarreta: ${sizeTshirt}`}
{sizeTshirt && sizeSocks && ' · '}
{sizeSocks && `Mitjons: ${sizeSocks}`}
</div>
</div>
<div className="text-white font-bold">
{product!.price.toFixed(2).replace('.', ',')}
</div>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-slate-400">
{shipping === 'correos' ? 'Correos Express' : 'Recollida Corbins/Lleida'}
</span>
<span className={shipping === 'correos' ? 'text-white' : 'text-teal'}>
{shipping === 'correos' ? '7,99€' : 'Gratuït'}
</span>
</div>
<div className="border-t border-dark-border pt-4 flex items-center justify-between">
<span className="text-white font-black text-lg">TOTAL</span>
<span className="text-teal font-black text-3xl">
{total.toFixed(2).replace('.', ',')}
</span>
</div>
</div>
{/* Client summary */}
<div className="border-t border-dark-border px-6 py-4 bg-dark/40">
<p className="text-slate-500 text-xs mb-2 uppercase tracking-wider">Comanda per a</p>
<p className="text-white text-sm font-semibold">{formData.nom} {formData.cognoms}</p>
<p className="text-slate-500 text-xs">{formData.email} · {formData.telefon}</p>
{shipping === 'correos' && formData.adreca && (
<p className="text-slate-500 text-xs mt-1">
{formData.adreca}, {formData.codiPostal} {formData.poblacio}
</p>
)}
</div>
{/* Pay button */}
<div className="p-6 border-t border-dark-border">
<button
onClick={handlePay}
disabled={!canPay || loading}
className="btn-pay"
>
{loading ? (
<span className="flex items-center justify-center gap-2">
<Loader2 size={20} className="animate-spin" />
Preparant pagament...
</span>
) : (
<span className="flex items-center justify-center gap-2">
Pagar {total.toFixed(2).replace('.', ',')} amb Stripe
<svg viewBox="0 0 60 25" fill="currentColor" className="h-5 opacity-80">
<path d="M59.64 14.28h-8.06c.19 1.93 1.6 2.55 3.2 2.55 1.64 0 2.96-.37 4.05-.95v3.32a12.08 12.08 0 0 1-4.56.83c-4.06 0-6.8-2.22-6.8-7 0-4.6 2.7-7.1 6.3-7.1 3.54 0 5.96 2.38 5.96 7.35zm-8.12-2.3h4.12c-.1-1.81-.98-2.59-2-2.59-1.01 0-1.99.68-2.12 2.59zM44.9 25l1.59-8.6a13.83 13.83 0 0 1-3.5.48c-3.36 0-4.76-2.28-4.76-6.31 0-4.48 2.45-7.45 6.43-7.45 1.32 0 2.47.23 3.45.65l.6-3.15A17.1 17.1 0 0 0 44.5 0C39.29 0 33.5 3.71 33.5 10.72c0 5.72 2.91 9.06 8.42 9.06a14 14 0 0 0 3.81-.52L44.9 25zm-25.08-3.84c.79 0 1.5-.18 2.12-.54L20.6 25c-.74.28-1.74.48-2.86.48C13.38 25.48 11 22.9 11 18.7c0-5.5 3.1-8.05 7.09-8.05 2.9 0 5.1 1.3 6.1 3.67L22 13.2l.54-2.86H18.3L15.4 25h4.42zm-3-7.03c0 2.14 1.07 3.09 2.76 3.09.4 0 .78-.1 1.12-.26L20.5 14h-.21c-1.82 0-3.47 1-3.47 4.13zM10.04 7.75a3.6 3.6 0 0 1-1.4-.28L7 18.73H2.86L5.9 3h4.32l-.53 2.83c.74-.87 1.83-2.28 3.26-2.83h.3l-1.3 5.31c-.5-.39-1.25-.56-1.91-.56z" />
</svg>
</span>
)}
</button>
<p className="text-center text-slate-600 text-xs mt-3">
Seràs redirigit a Stripe per completar el pagament de forma segura
</p>
</div>
</div>
</div>
</section>
)}
<TrustSection />
{/* CTA if nothing selected yet */}
{!selectedProduct && (
<div className="text-center py-8 pb-16">
<button
onClick={() => document.getElementById('productes')?.scrollIntoView({ behavior: 'smooth' })}
className="flex items-center gap-2 mx-auto text-teal hover:text-white transition-colors text-sm"
>
<ChevronDown size={18} className="animate-bounce" />
Veure productes
</button>
</div>
)}
</main>
<Footer />
{/* Size guide modal */}
{sizeGuide && (
<SizeGuideModal type={sizeGuide} onClose={() => setSizeGuide(null)} />
)}
</>
)
}