c1922140a9
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
786 lines
34 KiB
TypeScript
786 lines
34 KiB
TypeScript
'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 XXS–XL'],
|
||
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 en 24-48h des del moment que t'ho enviem. Primer haurem de fabricar-te la comanda.</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">Ens posarem en contacte amb tu per coordinar l'entrega. Primer haurem de fabricar-te la comanda.</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)} />
|
||
)}
|
||
</>
|
||
)
|
||
}
|