Files
KabundiNjuki ff743a1397 Initial commit
2026-10-05 16:40:36 +03:00

1359 lines
70 KiB
TypeScript

'use client'
import { useState, useEffect, useRef, useMemo } from 'react'
import Link from 'next/link'
import { Navigation } from '@/components/navigation'
import { Footer } from '@/components/footer'
import { Check, TreePine, Globe, ChevronDown, ChevronUp, Loader2 } from 'lucide-react'
import { useEvent } from '@/components/event-provider'
import {
displayPrice,
editionLabel,
formatClock,
formatDateTime,
formatEventDate,
formatMoney,
venueLine,
type PublicCategory,
type Residency,
} from '@/lib/event-types'
interface LegalDoc {
name: string
document_type: string
slug: string
version: string
title: string
summary: string | null
body: string
applies_to: string
required: boolean
}
// Used only if the backend can't be reached; the server still checks the current versions.
const FALLBACK_INDEMNITY: LegalDoc = {
name: '', document_type: 'Indemnity', slug: 'indemnity', version: '1', title: 'Indemnity Declaration', summary: null,
applies_to: 'All runners', required: true,
body: indemnityTextFallback(),
}
function indemnityTextFallback() {
return '<p>I, the undersigned, hereby declare that I am physically fit and have trained sufficiently to participate in the Lukenya University Marathon. I understand that participating in a marathon involves inherent risks including, but not limited to, physical exertion, weather conditions, and contact with other participants. I voluntarily assume all risks associated with my participation in this event.</p><p>I release and hold harmless Lukenya University, the event organizers, sponsors, volunteers, and all affiliated parties from any and all claims, damages, or liabilities that may arise from my participation in this event. I consent to receive medical treatment that may be deemed necessary in the event of injury or illness during the race.</p><p>I confirm that all information provided in this registration is accurate and complete.</p>'
}
// Turns the stored document HTML into plain paragraphs for the scroll box.
function paragraphs(html: string): string[] {
return (html || '')
.replace(/<\/(p|div|li)>/gi, '\n')
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<[^>]+>/g, '')
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.split('\n')
.map((x) => x.trim())
.filter(Boolean)
}
const RELATIONSHIPS = ['Parent', 'Spouse or partner', 'Sibling', 'Child', 'Other relative', 'Friend', 'Colleague', 'Other']
const STORE_KEY = 'lm-registration-draft'
interface RaceOption {
id: string
distance: string
name: string
description: string
inclusions: string[]
hasPrizeMoney: boolean
isVirtual: boolean
soldOut: boolean
closed: boolean
category: PublicCategory
}
function toRaceOption(c: PublicCategory): RaceOption {
const start = formatClock(c.start_time)
return {
id: c.name,
distance: c.distance_label,
name: c.category_name,
description: c.short_description || (c.distance_km ? `${c.distance_km} kilometres${start ? `, starts ${start}` : ''}` : c.discipline),
inclusions: c.inclusions,
hasPrizeMoney: c.prize_eligible,
isVirtual: c.discipline === 'Virtual',
soldOut: c.sold_out || Boolean(c.closed),
closed: Boolean(c.closed),
category: c,
}
}
function ageOn(dob: string, onDate: string | null | undefined): number | null {
if (!dob) return null
const birth = new Date(`${dob}T00:00:00`)
const ref = onDate ? new Date(`${onDate.slice(0, 10)}T00:00:00`) : new Date()
if (Number.isNaN(birth.getTime()) || Number.isNaN(ref.getTime())) return null
let age = ref.getFullYear() - birth.getFullYear()
const m = ref.getMonth() - birth.getMonth()
if (m < 0 || (m === 0 && ref.getDate() < birth.getDate())) age--
return age
}
async function postJSON<T>(url: string, body: unknown): Promise<T> {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const data = await res.json().catch(() => ({}))
if (!res.ok) throw new Error(data.error || 'Something went wrong. Please try again.')
return data as T
}
const tshirtSizes = ['XS', 'S', 'M', 'L', 'XL', 'XXL']
export default function RegistrationPage() {
const { event, registrationEnabled } = useEvent()
const edition = event.edition
const races = useMemo(() => event.categories.map(toRaceOption), [event.categories])
const isOpen = edition?.registration_state === 'open'
const canPay = registrationEnabled && isOpen
const label = editionLabel(edition)
const eventDate = formatEventDate(edition?.event_date)
const venue = venueLine(edition)
const trees = edition?.trees_commitment_per_entry || 0
const treesText = trees > 0 ? `${trees} ${trees === 1 ? 'tree' : 'trees'} planted in your name` : null
const [currentStep, setCurrentStep] = useState(1)
const [selectedRace, setSelectedRace] = useState<string | null>(null)
const [entryCategory, setEntryCategory] = useState<Residency>('resident')
const [paymentMethod, setPaymentMethod] = useState<'mpesa' | 'card' | null>(null)
const [submitting, setSubmitting] = useState(false)
const [submitError, setSubmitError] = useState<string | null>(null)
// Set once the entry exists, so a failed payment start can be retried without re-registering.
const [savedEntry, setSavedEntry] = useState<{ registration: string; amount_due: number; currency: string; payment_token?: string } | null>(null)
const [legalDocs, setLegalDocs] = useState<LegalDoc[]>([FALLBACK_INDEMNITY])
const [consents, setConsents] = useState<Record<string, boolean>>({})
const [mobileSummaryOpen, setMobileSummaryOpen] = useState(false)
const [errors, setErrors] = useState<Record<string, string>>({})
const formRef = useRef<HTMLDivElement>(null)
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
idNumber: '',
dob: '',
gender: '',
nationality: 'Kenya',
email: '',
phone: '',
whatsapp: '',
tshirtSize: '',
emergencyName: '',
emergencyPhone: '',
emergencyRelationship: '',
supportRequest: '',
guardianName: '',
guardianPhone: '',
})
const runnerAge = ageOn(formData.dob, edition?.event_date)
const isMinor = runnerAge !== null && runnerAge < 18
useEffect(() => {
fetch('/api/legal')
.then((r) => (r.ok ? r.json() : null))
.then((docs: LegalDoc[] | null) => {
if (Array.isArray(docs) && docs.length) setLegalDocs(docs)
})
.catch(() => { /* keep the built-in indemnity */ })
}, [])
// Keep what the runner typed if they navigate away and come back in this tab.
useEffect(() => {
try {
const saved = JSON.parse(window.sessionStorage.getItem(STORE_KEY) || 'null')
if (saved?.formData) setFormData((prev) => ({ ...prev, ...saved.formData }))
if (saved?.entryCategory) setEntryCategory(saved.entryCategory)
if (saved?.selectedRace) setSelectedRace(saved.selectedRace)
} catch { /* storage unavailable */ }
}, [])
useEffect(() => {
try {
window.sessionStorage.setItem(STORE_KEY, JSON.stringify({ formData, entryCategory, selectedRace }))
} catch { /* storage unavailable */ }
}, [formData, entryCategory, selectedRace])
// Documents that apply to this runner: under-18-only ones appear when the date of birth says so.
const applicableDocs = legalDocs.filter((d) => d.applies_to !== 'Under 18 only' || isMinor)
const requiredDocs = applicableDocs.filter((d) => d.required)
const optionalDocs = applicableDocs.filter((d) => !d.required && d.name)
const indemnityDoc = legalDocs.find((d) => d.document_type === 'Indemnity') || FALLBACK_INDEMNITY
const docKey = (d: LegalDoc) => d.name || d.document_type
const allRequiredAccepted = requiredDocs.every((d) => consents[docKey(d)])
// Pre-select a race passed from another page, e.g. /registration?race=42KM
useEffect(() => {
const wanted = new URLSearchParams(window.location.search).get('race')
if (wanted && races.some((r) => r.id === wanted && !r.soldOut)) setSelectedRace(wanted)
}, [races])
const selectedRaceData = races.find(r => r.id === selectedRace)
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const { name, value } = e.target
setFormData(prev => ({ ...prev, [name]: value }))
// Clear error when user types
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }))
}
}
const scrollToForm = () => {
formRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
const handleRaceRowClick = (raceId: string) => {
setSelectedRace(raceId)
scrollToForm()
}
const validateStep1 = () => {
return selectedRace !== null && !selectedRaceData?.soldOut
}
const validateStep2 = () => {
const newErrors: Record<string, string> = {}
if (!formData.firstName.trim()) newErrors.firstName = 'First name is required'
if (!formData.lastName.trim()) newErrors.lastName = 'Last name is required'
if (!formData.idNumber.trim()) newErrors.idNumber = 'ID/Passport number is required'
if (!formData.dob) newErrors.dob = 'Date of birth is required'
if (!formData.gender) newErrors.gender = 'Gender is required'
if (!formData.nationality) newErrors.nationality = 'Nationality is required'
if (!formData.email.trim()) newErrors.email = 'Email address is required'
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) newErrors.email = 'Please enter a valid email'
if (!formData.phone.trim()) newErrors.phone = 'Phone number is required'
if (!formData.tshirtSize) newErrors.tshirtSize = 'T-shirt size is required'
if (!formData.emergencyName.trim()) newErrors.emergencyName = 'Emergency contact name is required'
if (!formData.emergencyPhone.trim()) newErrors.emergencyPhone = 'Emergency contact phone is required'
if (!formData.emergencyRelationship) newErrors.emergencyRelationship = 'Choose how this person is related to you'
const race = selectedRaceData?.category
if (race?.min_age && runnerAge !== null && runnerAge < race.min_age) {
newErrors.dob = `${race.category_name} is for runners aged ${race.min_age} and over on race day`
}
if (isMinor) {
if (!formData.guardianName.trim()) newErrors.guardianName = 'A parent or guardian must be named for runners under 18'
if (!formData.guardianPhone.trim()) newErrors.guardianPhone = 'Parent or guardian phone is required'
}
if (!allRequiredAccepted) newErrors.indemnity = 'Please tick each required box to continue'
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleContinueToStep2 = () => {
if (validateStep1()) {
setCurrentStep(2)
scrollToForm()
}
}
const handleContinueToStep3 = () => {
if (validateStep2()) {
setCurrentStep(3)
setSubmitError(null)
scrollToForm()
}
}
const startPayment = async (entry: NonNullable<typeof savedEntry>) => {
const pay = await postJSON<{ authorization_url: string }>('/api/payment/start', {
registration: entry.registration,
payment_token: entry.payment_token,
channel: paymentMethod,
})
// Off to the secure payment page. The payment is confirmed by the server, not by this page.
window.location.assign(pay.authorization_url)
}
const handleSubmit = async () => {
if (!paymentMethod || !selectedRaceData || !edition && event.source !== 'fallback') return
if (!canPay) {
setSubmitError('Online payment is not open yet. Your details have not been sent.')
return
}
setSubmitting(true)
setSubmitError(null)
try {
let entry = savedEntry
if (!entry) {
entry = await postJSON<NonNullable<typeof savedEntry>>('/api/register', {
event_edition: selectedRaceData.category && edition ? edition.name : undefined,
race_category: selectedRaceData.id,
residency: entryCategory === 'resident' ? 'Resident' : 'Non-resident',
first_name: formData.firstName.trim(),
last_name: formData.lastName.trim(),
id_number: formData.idNumber.trim(),
date_of_birth: formData.dob,
gender: formData.gender === 'male' ? 'Male' : formData.gender === 'female' ? 'Female' : formData.gender,
nationality: formData.nationality,
email: formData.email.trim(),
phone: formData.phone.trim(),
whatsapp: formData.whatsapp.trim(),
shirt_size: formData.tshirtSize,
emergency_contact_name: formData.emergencyName.trim(),
emergency_contact_phone: formData.emergencyPhone.trim(),
emergency_contact_relationship: formData.emergencyRelationship,
support_request: formData.supportRequest.trim() || undefined,
guardian_name: isMinor ? formData.guardianName.trim() : undefined,
guardian_phone: isMinor ? formData.guardianPhone.trim() : undefined,
indemnity_accepted: allRequiredAccepted,
consents: applicableDocs.filter((d) => d.name).length
? applicableDocs.filter((d) => d.name).map((d) => ({ name: d.name, accepted: Boolean(consents[docKey(d)]) }))
: undefined,
preferred_payment: paymentMethod,
})
setSavedEntry(entry)
}
await startPayment(entry)
} catch (err) {
setSubmitError((err as Error).message)
setSubmitting(false)
}
}
// Price display component
const PriceDisplay = ({ race, residency = entryCategory, className = '' }: { race?: RaceOption; residency?: Residency; className?: string }) => {
const price = race ? formatMoney(displayPrice(race.category, residency), event.currency) : null
if (race?.soldOut) {
return (
<span className={`inline-flex items-center px-2.5 py-1 rounded bg-[#E5E7EB] text-[#111518] font-heading font-bold text-[11px] uppercase tracking-wide ${className}`}>
{race.closed ? 'Entries closed' : 'Sold out'}
</span>
)
}
if (price) {
return <span className={`font-heading font-bold text-[#006838] text-[15px] ${className}`}>{price}</span>
}
return (
<span className={`inline-flex items-center px-2.5 py-1 rounded bg-[#FFF100] text-[#714508] font-heading font-bold text-[11px] uppercase tracking-wide ${className}`}>
Coming Soon
</span>
)
}
// Summary Panel Content
const SummaryContent = () => (
<>
{!selectedRaceData ? (
<div className="border-2 border-dashed border-[#E5E7EB] rounded-lg p-6 text-center">
<p className="font-sans text-[#687279] text-sm">
Select your race above to see your entry summary.
</p>
</div>
) : (
<>
{/* Selected Race */}
<div className="mb-4">
<span className="font-display font-bold text-[#006838] text-[32px] leading-none">
{selectedRaceData.distance}
</span>
<p className="font-heading font-bold text-[#111518] text-base mt-1">
{selectedRaceData.name}
</p>
<p className="font-sans text-[#687279] text-sm">
{entryCategory === 'resident' ? 'Kenyan resident' : 'Non-resident'}
</p>
</div>
<div className="border-t border-[#E5E7EB] pt-4 mb-4">
<p className="font-heading font-semibold text-[11px] text-[#687279] uppercase tracking-wide mb-2">
Entry Fee
</p>
<PriceDisplay race={selectedRaceData} className="text-[16px]" />
<p className="font-sans text-[#687279] text-[13px] mt-2">
Includes: {selectedRaceData.inclusions.join(' · ')}
</p>
</div>
<div className="border-t border-[#E5E7EB] pt-4 mb-4">
<p className="font-heading font-semibold text-[11px] text-[#687279] uppercase tracking-wide mb-3">
What You Get
</p>
<ul className="space-y-2">
{selectedRaceData.inclusions.map((item, i) => (
<li key={i} className="flex items-center gap-2 font-sans text-[#111518] text-[13px]">
<Check className="w-3.5 h-3.5 text-[#3BB34A] flex-shrink-0" />
{item}
</li>
))}
{treesText && (
<li className="flex items-center gap-2 font-sans text-[#111518] text-[13px]">
<Check className="w-3.5 h-3.5 text-[#3BB34A] flex-shrink-0" />
{treesText} in ASAL Kenya
</li>
)}
</ul>
</div>
<div className="border-t border-[#E5E7EB] pt-4 mb-4">
<div className="bg-[#F0FFF4] rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<TreePine className="w-[18px] h-[18px] text-[#006838]" />
<span className="font-heading font-bold text-[#006838] text-[13px]">
Your Environmental Contribution
</span>
</div>
<p className="font-sans text-[#687279] text-[13px] leading-relaxed mb-3">
{treesText ? `Your entry: ${treesText}. ` : ''}Trees are GPS-tagged and planted in the Lukenya Conservation Area, Machakos County.
</p>
<div>
<div className="flex justify-between items-center mb-1">
<span className="font-sans text-[#687279] text-[12px]">10M Trees Goal</span>
</div>
<div className="h-1.5 bg-[#E5E7EB] rounded-full overflow-hidden">
<div className="h-full bg-[#3BB34A] rounded-full" style={{ width: '0.1%' }} />
</div>
<p className="font-sans text-[#687279] text-[12px] mt-1">10,000 planted so far</p>
</div>
</div>
</div>
<div className="border-t border-[#E5E7EB] pt-4 text-center">
<p className="font-sans text-[#687279] text-[13px] mb-2">
Questions about registration?
</p>
<a
href="mailto:marathon@lukenyauniversity.ac.ke"
className="font-heading font-semibold text-[#006838] text-[13px] hover:underline block mb-1"
>
Email us
</a>
<a
href="https://wa.me/254700000000"
className="font-heading font-semibold text-[#006838] text-[13px] hover:underline block"
>
WhatsApp us
</a>
</div>
</>
)}
</>
)
return (
<div className="min-h-screen bg-white">
<Navigation />
{/* SECTION 1: PAGE HERO */}
<section className="bg-[#006838] pt-20">
<div className="max-w-[1200px] mx-auto px-4 h-[280px] flex items-center">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between w-full">
{/* Left content */}
<div>
<span className="font-heading font-semibold text-[11px] text-[#3BB34A] uppercase tracking-[0.12em] mb-3 block">
{(label || 'Next edition').toUpperCase()}
</span>
<div className="w-8 h-0.5 bg-[#F0C000] mb-4" />
<h1 className="font-display font-bold text-white text-[36px] md:text-[52px] leading-[1.1] mb-4">
REGISTER FOR THE RACE
</h1>
<p className="font-sans font-medium text-white/75 text-base">
{[eventDate, venue, isOpen ? 'Entries open now' : edition?.registration_state === 'closed' ? 'Entries closed' : 'Entries open soon'].filter(Boolean).join(' · ')}
</p>
</div>
{/* Right trust pills - desktop only */}
<div className="hidden lg:flex items-center gap-3 mt-6 lg:mt-0">
{['Sporthive Certified Timing', 'M-PESA & Card Accepted', 'Instant Confirmation'].map((pill) => (
<span
key={pill}
className="inline-flex items-center gap-1.5 px-3 py-2 border border-white/40 rounded-full font-heading font-semibold text-white text-[12px]"
>
<Check className="w-3.5 h-3.5" />
{pill}
</span>
))}
</div>
</div>
</div>
</section>
{/* SECTION 2: PRICING TABLE */}
<section className="bg-white py-16">
<div className="max-w-[1200px] mx-auto px-4">
<div className="text-center mb-10">
<span className="font-heading font-semibold text-[11px] text-[#3BB34A] uppercase tracking-[0.12em] mb-3 block">
ENTRY FEES
</span>
<div className="w-8 h-0.5 bg-[#F0C000] mx-auto mb-4" />
<h2 className="font-heading font-bold text-[#111518] text-[32px] md:text-[40px] mb-3">
Choose Your Race.
</h2>
<p className="font-sans font-medium text-[#687279] text-base max-w-2xl mx-auto">
Pricing shown in Kenyan Shillings ({event.currency}).{' '}
{races.some((r) => r.category.prices.early_bird_active) && formatDateTime(edition?.early_bird_ends)
? `Early-bird rates apply until ${formatDateTime(edition?.early_bird_ends)}. `
: ''}
Non-residents pay non-resident rates.
</p>
</div>
{/* Desktop Table */}
<div className="hidden md:block overflow-hidden rounded-xl shadow-[0_2px_16px_rgba(0,0,0,0.08)]">
<table className="w-full border-collapse">
<thead>
<tr className="bg-[#006838]">
<th className="text-left py-4 px-5 font-heading font-bold text-white text-sm">Race</th>
<th className="text-left py-4 px-5 font-heading font-bold text-white text-sm">Distance</th>
<th className="text-left py-4 px-5 font-heading font-bold text-white text-sm">Kenyan resident</th>
<th className="text-left py-4 px-5 font-heading font-bold text-white text-sm">Non-resident</th>
<th className="text-left py-4 px-5 font-heading font-bold text-white text-sm">{"What's Included"}</th>
</tr>
</thead>
<tbody>
{races.map((race, index) => (
<tr
key={race.id}
onClick={() => handleRaceRowClick(race.id)}
className={`cursor-pointer transition-colors hover:bg-[rgba(0,104,56,0.04)] ${
index % 2 === 0 ? 'bg-white' : 'bg-[#F9FAFB]'
} ${selectedRace === race.id ? 'bg-[rgba(0,104,56,0.04)]' : ''}`}
>
<td className="py-4 px-5">
<div className="flex items-center gap-2">
<span className="font-heading font-bold text-[#111518] text-[15px]">
{race.distance} {race.name}
</span>
{race.hasPrizeMoney && (
<span className="inline-flex px-2 py-0.5 rounded bg-[#F0C000] text-[#714508] font-heading font-bold text-[10px] uppercase">
Prize Money
</span>
)}
</div>
<p className="font-sans text-[#687279] text-[13px]">{race.description}</p>
</td>
<td className="py-4 px-5 font-sans text-[#111518] text-[13px] whitespace-nowrap">
{race.category.distance_km ? `${race.category.distance_km} km` : race.distance}
</td>
<td className="py-4 px-5"><PriceDisplay race={race} residency="resident" /></td>
<td className="py-4 px-5"><PriceDisplay race={race} residency="non-resident" /></td>
<td className="py-4 px-5 font-sans text-[#687279] text-[13px]">
{race.inclusions.join(' · ')}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile Cards */}
<div className="md:hidden space-y-3">
{races.map((race) => (
<div
key={race.id}
onClick={() => handleRaceRowClick(race.id)}
className={`bg-white rounded-xl p-4 shadow-[0_2px_8px_rgba(0,0,0,0.06)] cursor-pointer transition-all ${
selectedRace === race.id ? 'ring-2 ring-[#006838]' : ''
}`}
>
<div className="flex justify-between items-start mb-2">
<div>
<div className="flex items-center gap-2">
<span className="font-display font-bold text-[#006838] text-xl">{race.distance}</span>
{race.hasPrizeMoney && (
<span className="inline-flex px-2 py-0.5 rounded bg-[#F0C000] text-[#714508] font-heading font-bold text-[10px] uppercase">
Prize Money
</span>
)}
</div>
<p className="font-heading font-bold text-[#111518] text-sm">{race.name}</p>
</div>
<PriceDisplay race={race} />
</div>
<p className="font-sans text-[#687279] text-xs">
{race.inclusions.join(' · ')}
</p>
</div>
))}
</div>
<p className="text-center font-sans text-[#687279] text-[13px] mt-6 max-w-3xl mx-auto">
{treesText ? `Every entry includes ${treesText.replace(' planted in your name', '')} planted in the ASAL conservation area. ` : ''}Payment accepted via M-PESA and card. Contact us for group and corporate relay pricing.
</p>
<div className="text-center mt-6">
<button
onClick={scrollToForm}
className="inline-flex items-center gap-2 px-8 py-4 bg-[#FFF100] hover:bg-[#3BB34A] text-[#714508] hover:text-white font-heading font-bold text-sm uppercase tracking-wide rounded-lg transition-colors"
>
Register Now
<span>→</span>
</button>
</div>
</div>
</section>
{/* SECTION 3: TWO-COLUMN FORM LAYOUT */}
<section ref={formRef} id="registration-form" className="bg-[#F9FAFB] py-20">
<div className="max-w-[1200px] mx-auto px-4">
{(
<div className="flex flex-col lg:flex-row gap-12">
{/* Mobile Summary Accordion */}
<div className="lg:hidden">
<button
onClick={() => setMobileSummaryOpen(!mobileSummaryOpen)}
className="w-full bg-white rounded-xl p-4 shadow-[0_2px_16px_rgba(0,0,0,0.08)] flex items-center justify-between"
>
<span className="font-heading font-bold text-[#111518] text-base">Your Entry Summary</span>
{mobileSummaryOpen ? <ChevronUp className="w-5 h-5" /> : <ChevronDown className="w-5 h-5" />}
</button>
{mobileSummaryOpen && (
<div className="bg-white rounded-b-xl p-6 shadow-[0_2px_16px_rgba(0,0,0,0.08)] -mt-2">
<SummaryContent />
</div>
)}
</div>
{/* LEFT: FORM */}
<div className="flex-1 lg:w-[60%]">
<div className="bg-white rounded-xl shadow-[0_2px_16px_rgba(0,0,0,0.08)] p-6 md:p-12">
{/* Step Indicator */}
<div className="flex items-center justify-between mb-10">
{[
{ num: 1, label: 'Choose Race' },
{ num: 2, label: 'Your Details' },
{ num: 3, label: 'Payment' },
].map((step, index) => (
<div key={step.num} className="flex items-center">
<div className="flex flex-col items-center">
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-heading font-bold text-sm ${
currentStep > step.num
? 'bg-[#3BB34A] text-white'
: currentStep === step.num
? 'bg-[#006838] text-white'
: 'bg-[#E5E7EB] text-[#687279]'
}`}>
{currentStep > step.num ? <Check className="w-5 h-5" /> : step.num}
</div>
<span className={`font-heading font-bold text-xs mt-2 hidden sm:block ${
currentStep >= step.num ? 'text-[#006838]' : 'text-[#687279]'
}`}>
{step.label}
</span>
</div>
{index < 2 && (
<div className={`w-16 sm:w-24 h-0.5 mx-2 ${
currentStep > step.num ? 'bg-[#006838]' : 'bg-[#E5E7EB]'
}`} />
)}
</div>
))}
</div>
{/* STEP 1: CHOOSE RACE */}
{currentStep === 1 && (
<div>
<h3 className="font-heading font-bold text-[#111518] text-[22px] mb-2">
Which race are you entering?
</h3>
<p className="font-sans text-[#687279] text-sm mb-6">
Select your distance. Your entry fee will appear in the summary panel.
</p>
{/* Race Cards */}
<div className="space-y-3 mb-6">
{races.length === 0 && (
<p className="font-sans text-[#687279] text-sm border-2 border-dashed border-[#E5E7EB] rounded-[10px] p-5">
Races for the next edition haven&apos;t been published yet. Join the waitlist below and we&apos;ll email you when entries open.
</p>
)}
{races.map((race) => (
<div
key={race.id}
onClick={() => !race.soldOut && setSelectedRace(race.id)}
className={`relative border-2 rounded-[10px] p-5 cursor-pointer transition-all ${
selectedRace === race.id
? 'border-[#006838] bg-[rgba(0,104,56,0.04)]'
: 'border-[#E5E7EB] bg-white hover:border-[#006838]/40'
} ${race.hasPrizeMoney ? 'border-l-4 border-l-[#F0C000]' : ''}`}
>
{selectedRace === race.id && (
<div className="absolute top-4 right-4">
<Check className="w-5 h-5 text-[#006838]" />
</div>
)}
{race.hasPrizeMoney && (
<span className="absolute top-4 right-4 inline-flex px-2 py-0.5 rounded bg-[#F0C000] text-[#714508] font-heading font-bold text-[10px] uppercase">
Prize Money
</span>
)}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
{race.isVirtual && <Globe className="w-[18px] h-[18px] text-[#3BB34A]" />}
<span className="font-display font-bold text-[#006838] text-[28px]">
{race.distance}
</span>
</div>
<span className="font-heading font-bold text-[#111518] text-[15px]">
{race.name}
</span>
</div>
<div className="text-right">
<p className="font-heading font-semibold text-[10px] text-[#687279] uppercase">
{entryCategory === 'resident' ? 'Kenyan resident' : 'Non-resident'}
</p>
<PriceDisplay race={race} />
</div>
</div>
<p className="font-sans text-[#687279] text-[13px] mt-2">
{race.inclusions.join(' · ')}
</p>
</div>
))}
</div>
{/* Entry Category Toggle */}
<div className="mb-6">
<label className="font-heading font-bold text-[#111518] text-[15px] block mb-1">
Entry Category
</label>
<p className="font-sans text-[#687279] text-[13px] mb-3">
This affects your entry fee. The final amount is confirmed before you pay.
</p>
<div className="flex gap-2">
<button
type="button"
onClick={() => setEntryCategory('resident')}
className={`flex-1 py-3 px-4 rounded-lg font-heading font-bold text-sm transition-colors ${
entryCategory === 'resident'
? 'bg-[#006838] text-white'
: 'bg-white border border-[#E5E7EB] text-[#687279] hover:border-[#006838]'
}`}
>
Kenyan resident
</button>
<button
type="button"
onClick={() => setEntryCategory('non-resident')}
className={`flex-1 py-3 px-4 rounded-lg font-heading font-bold text-sm transition-colors ${
entryCategory === 'non-resident'
? 'bg-[#006838] text-white'
: 'bg-white border border-[#E5E7EB] text-[#687279] hover:border-[#006838]'
}`}
>
Non-resident
</button>
</div>
</div>
<button
onClick={handleContinueToStep2}
disabled={!validateStep1()}
className={`w-full py-4 rounded-lg font-heading font-bold text-sm uppercase tracking-wide transition-colors ${
validateStep1()
? 'bg-[#FFF100] hover:bg-[#3BB34A] text-[#714508] hover:text-white'
: 'bg-[#E5E7EB] text-[#687279] cursor-not-allowed'
}`}
>
Continue to Your Details →
</button>
</div>
)}
{/* STEP 2: YOUR DETAILS */}
{currentStep === 2 && (
<div>
<h3 className="font-heading font-bold text-[#111518] text-[22px] mb-2">
Tell us about yourself.
</h3>
<p className="font-sans text-[#687279] text-sm mb-6">
Your details are used for your race bib, chip timing, and post-race certificate.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5 mb-6">
{/* First Name */}
<div>
<label className="font-heading font-semibold text-[13px] text-[#111518] mb-1.5 block">
First Name <span className="text-[#c0392b]">*</span>
</label>
<input
type="text"
name="firstName"
value={formData.firstName}
onChange={handleInputChange}
className={`w-full px-4 py-3 h-12 rounded-lg border font-sans text-[15px] text-[#111518] outline-none transition-all ${
errors.firstName ? 'border-[#c0392b]' : 'border-[#D1D5DB] focus:border-[#006838] focus:ring-2 focus:ring-[rgba(0,104,56,0.12)]'
}`}
placeholder="Enter your first name"
/>
{errors.firstName && <p className="font-sans text-[#c0392b] text-[12px] mt-1">{errors.firstName}</p>}
</div>
{/* Last Name */}
<div>
<label className="font-heading font-semibold text-[13px] text-[#111518] mb-1.5 block">
Last Name <span className="text-[#c0392b]">*</span>
</label>
<input
type="text"
name="lastName"
value={formData.lastName}
onChange={handleInputChange}
className={`w-full px-4 py-3 h-12 rounded-lg border font-sans text-[15px] text-[#111518] outline-none transition-all ${
errors.lastName ? 'border-[#c0392b]' : 'border-[#D1D5DB] focus:border-[#006838] focus:ring-2 focus:ring-[rgba(0,104,56,0.12)]'
}`}
placeholder="Enter your last name"
/>
{errors.lastName && <p className="font-sans text-[#c0392b] text-[12px] mt-1">{errors.lastName}</p>}
</div>
{/* ID/Passport */}
<div>
<label className="font-heading font-semibold text-[13px] text-[#111518] mb-1.5 block">
ID / Passport Number <span className="text-[#c0392b]">*</span>
</label>
<input
type="text"
name="idNumber"
value={formData.idNumber}
onChange={handleInputChange}
className={`w-full px-4 py-3 h-12 rounded-lg border font-sans text-[15px] text-[#111518] outline-none transition-all ${
errors.idNumber ? 'border-[#c0392b]' : 'border-[#D1D5DB] focus:border-[#006838] focus:ring-2 focus:ring-[rgba(0,104,56,0.12)]'
}`}
placeholder="Enter ID or passport number"
/>
{errors.idNumber && <p className="font-sans text-[#c0392b] text-[12px] mt-1">{errors.idNumber}</p>}
<p className="font-sans text-[#687279] text-[12px] mt-1">Checked when you collect your race pack and for prize payments.</p>
</div>
{/* Date of Birth */}
<div>
<label className="font-heading font-semibold text-[13px] text-[#111518] mb-1.5 block">
Date of Birth <span className="text-[#c0392b]">*</span>
</label>
<input
type="date"
name="dob"
value={formData.dob}
onChange={handleInputChange}
className={`w-full px-4 py-3 h-12 rounded-lg border font-sans text-[15px] text-[#111518] outline-none transition-all ${
errors.dob ? 'border-[#c0392b]' : 'border-[#D1D5DB] focus:border-[#006838] focus:ring-2 focus:ring-[rgba(0,104,56,0.12)]'
}`}
/>
{errors.dob && <p className="font-sans text-[#c0392b] text-[12px] mt-1">{errors.dob}</p>}
<p className="font-sans text-[#687279] text-[12px] mt-1">Used for race age limits and age-group results.</p>
</div>
{/* Gender */}
<div>
<label className="font-heading font-semibold text-[13px] text-[#111518] mb-1.5 block">
Gender <span className="text-[#c0392b]">*</span>
</label>
<div className="flex gap-2">
<button
type="button"
onClick={() => {
setFormData(prev => ({ ...prev, gender: 'male' }))
if (errors.gender) setErrors(prev => ({ ...prev, gender: '' }))
}}
className={`flex-1 py-3 px-4 rounded-lg font-heading font-bold text-sm transition-colors ${
formData.gender === 'male'
? 'bg-[#006838] text-white'
: 'bg-white border border-[#D1D5DB] text-[#687279]'
}`}
>
Male
</button>
<button
type="button"
onClick={() => {
setFormData(prev => ({ ...prev, gender: 'female' }))
if (errors.gender) setErrors(prev => ({ ...prev, gender: '' }))
}}
className={`flex-1 py-3 px-4 rounded-lg font-heading font-bold text-sm transition-colors ${
formData.gender === 'female'
? 'bg-[#006838] text-white'
: 'bg-white border border-[#D1D5DB] text-[#687279]'
}`}
>
Female
</button>
</div>
<p className="font-sans text-[#687279] text-[12px] mt-1">Results and prizes are ranked by gender.</p>
{errors.gender && <p className="font-sans text-[#c0392b] text-[12px] mt-1">{errors.gender}</p>}
</div>
{/* Nationality */}
<div>
<label className="font-heading font-semibold text-[13px] text-[#111518] mb-1.5 block">
Nationality <span className="text-[#c0392b]">*</span>
</label>
<select
name="nationality"
value={formData.nationality}
onChange={handleInputChange}
className={`w-full px-4 py-3 h-12 rounded-lg border font-sans text-[15px] text-[#111518] outline-none transition-all ${
errors.nationality ? 'border-[#c0392b]' : 'border-[#D1D5DB] focus:border-[#006838] focus:ring-2 focus:ring-[rgba(0,104,56,0.12)]'
}`}
>
<option value="Kenya">Kenya</option>
<option value="Uganda">Uganda</option>
<option value="Tanzania">Tanzania</option>
<option value="Ethiopia">Ethiopia</option>
<option value="Rwanda">Rwanda</option>
<option value="Other">Other</option>
</select>
{errors.nationality && <p className="font-sans text-[#c0392b] text-[12px] mt-1">{errors.nationality}</p>}
</div>
{/* Email - Full Width */}
<div className="md:col-span-2">
<label className="font-heading font-semibold text-[13px] text-[#111518] mb-1.5 block">
Email Address <span className="text-[#c0392b]">*</span>
</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
className={`w-full px-4 py-3 h-12 rounded-lg border font-sans text-[15px] text-[#111518] outline-none transition-all ${
errors.email ? 'border-[#c0392b]' : 'border-[#D1D5DB] focus:border-[#006838] focus:ring-2 focus:ring-[rgba(0,104,56,0.12)]'
}`}
placeholder="you@email.com"
/>
{errors.email && <p className="font-sans text-[#c0392b] text-[12px] mt-1">{errors.email}</p>}
</div>
{/* Phone */}
<div>
<label className="font-heading font-semibold text-[13px] text-[#111518] mb-1.5 block">
Phone Number <span className="text-[#c0392b]">*</span>
</label>
<input
type="tel"
name="phone"
value={formData.phone}
onChange={handleInputChange}
className={`w-full px-4 py-3 h-12 rounded-lg border font-sans text-[15px] text-[#111518] outline-none transition-all ${
errors.phone ? 'border-[#c0392b]' : 'border-[#D1D5DB] focus:border-[#006838] focus:ring-2 focus:ring-[rgba(0,104,56,0.12)]'
}`}
placeholder="+254"
/>
{errors.phone && <p className="font-sans text-[#c0392b] text-[12px] mt-1">{errors.phone}</p>}
</div>
{/* WhatsApp */}
<div>
<label className="font-heading font-semibold text-[13px] text-[#111518] mb-1.5 block">
WhatsApp <span className="text-[#687279]">(optional)</span>
</label>
<input
type="tel"
name="whatsapp"
value={formData.whatsapp}
onChange={handleInputChange}
className="w-full px-4 py-3 h-12 rounded-lg border border-[#D1D5DB] font-sans text-[15px] text-[#111518] focus:border-[#006838] focus:ring-2 focus:ring-[rgba(0,104,56,0.12)] outline-none transition-all"
placeholder="+254"
/>
</div>
{/* T-Shirt Size - Full Width */}
<div className="md:col-span-2">
<label className="font-heading font-semibold text-[13px] text-[#111518] mb-1.5 block">
T-Shirt Size <span className="text-[#c0392b]">*</span>
</label>
<select
name="tshirtSize"
value={formData.tshirtSize}
onChange={handleInputChange}
className={`w-full px-4 py-3 h-12 rounded-lg border font-sans text-[15px] text-[#111518] outline-none transition-all ${
errors.tshirtSize ? 'border-[#c0392b]' : 'border-[#D1D5DB] focus:border-[#006838] focus:ring-2 focus:ring-[rgba(0,104,56,0.12)]'
}`}
>
<option value="">Select size</option>
{tshirtSizes.map((size) => (
<option key={size} value={size}>{size}</option>
))}
</select>
{errors.tshirtSize && <p className="font-sans text-[#c0392b] text-[12px] mt-1">{errors.tshirtSize}</p>}
</div>
{/* Emergency contact */}
<div className="md:col-span-2 pt-2">
<p className="font-heading font-bold text-[#111518] text-[15px]">Emergency contact</p>
<p className="font-sans text-[#687279] text-[13px]">Someone we can call on race day if you need help. Not a runner in the same race, ideally.</p>
</div>
<div>
<label htmlFor="emergencyName" className="font-heading font-semibold text-[13px] text-[#111518] mb-1.5 block">
Contact name <span className="text-[#c0392b]">*</span>
</label>
<input
id="emergencyName"
type="text"
name="emergencyName"
value={formData.emergencyName}
onChange={handleInputChange}
autoComplete="off"
className={`w-full px-4 py-3 h-12 rounded-lg border font-sans text-[15px] text-[#111518] outline-none transition-all ${
errors.emergencyName ? 'border-[#c0392b]' : 'border-[#D1D5DB] focus:border-[#006838] focus:ring-2 focus:ring-[rgba(0,104,56,0.12)]'
}`}
placeholder="Full name"
/>
{errors.emergencyName && <p className="font-sans text-[#c0392b] text-[12px] mt-1">{errors.emergencyName}</p>}
</div>
<div>
<label htmlFor="emergencyPhone" className="font-heading font-semibold text-[13px] text-[#111518] mb-1.5 block">
Contact phone <span className="text-[#c0392b]">*</span>
</label>
<input
id="emergencyPhone"
type="tel"
name="emergencyPhone"
value={formData.emergencyPhone}
onChange={handleInputChange}
autoComplete="off"
className={`w-full px-4 py-3 h-12 rounded-lg border font-sans text-[15px] text-[#111518] outline-none transition-all ${
errors.emergencyPhone ? 'border-[#c0392b]' : 'border-[#D1D5DB] focus:border-[#006838] focus:ring-2 focus:ring-[rgba(0,104,56,0.12)]'
}`}
placeholder="+254"
/>
{errors.emergencyPhone && <p className="font-sans text-[#c0392b] text-[12px] mt-1">{errors.emergencyPhone}</p>}
</div>
<div>
<label htmlFor="emergencyRelationship" className="font-heading font-semibold text-[13px] text-[#111518] mb-1.5 block">
Relationship to you <span className="text-[#c0392b]">*</span>
</label>
<select
id="emergencyRelationship"
name="emergencyRelationship"
value={formData.emergencyRelationship}
onChange={handleInputChange}
className={`w-full px-4 py-3 h-12 rounded-lg border font-sans text-[15px] text-[#111518] bg-white outline-none transition-all ${
errors.emergencyRelationship ? 'border-[#c0392b]' : 'border-[#D1D5DB] focus:border-[#006838] focus:ring-2 focus:ring-[rgba(0,104,56,0.12)]'
}`}
>
<option value="">Choose…</option>
{RELATIONSHIPS.map((r) => <option key={r} value={r}>{r}</option>)}
</select>
{errors.emergencyRelationship && <p className="font-sans text-[#c0392b] text-[12px] mt-1">{errors.emergencyRelationship}</p>}
</div>
<div className="md:col-span-2">
<label htmlFor="supportRequest" className="font-heading font-semibold text-[13px] text-[#111518] mb-1.5 block">
Accessibility or support needs <span className="font-sans font-normal text-[#687279]">(optional, private)</span>
</label>
<textarea
id="supportRequest"
name="supportRequest"
value={formData.supportRequest}
onChange={handleInputChange}
maxLength={1000}
rows={3}
className="w-full px-4 py-3 rounded-lg border border-[#D1D5DB] font-sans text-[15px] text-[#111518] outline-none focus:border-[#006838] focus:ring-2 focus:ring-[rgba(0,104,56,0.12)]"
placeholder="For example: wheelchair access, a guide runner, a medical condition our team should know about."
/>
<p className="font-sans text-[#687279] text-[12px] mt-1">Only our registration and race-day staff see this.</p>
</div>
{/* Parent or guardian, only for runners under 18 on race day */}
{isMinor && (
<>
<div className="md:col-span-2 pt-2">
<p className="font-heading font-bold text-[#111518] text-[15px]">Parent or guardian</p>
<p className="font-sans text-[#687279] text-[13px]">
You&apos;ll be {runnerAge} on race day, so a parent or guardian must consent to this entry.
</p>
</div>
<div>
<label htmlFor="guardianName" className="font-heading font-semibold text-[13px] text-[#111518] mb-1.5 block">
Parent or guardian name <span className="text-[#c0392b]">*</span>
</label>
<input
id="guardianName"
type="text"
name="guardianName"
value={formData.guardianName}
onChange={handleInputChange}
autoComplete="off"
className={`w-full px-4 py-3 h-12 rounded-lg border font-sans text-[15px] text-[#111518] outline-none transition-all ${
errors.guardianName ? 'border-[#c0392b]' : 'border-[#D1D5DB] focus:border-[#006838] focus:ring-2 focus:ring-[rgba(0,104,56,0.12)]'
}`}
placeholder="Full name"
/>
{errors.guardianName && <p className="font-sans text-[#c0392b] text-[12px] mt-1">{errors.guardianName}</p>}
</div>
<div>
<label htmlFor="guardianPhone" className="font-heading font-semibold text-[13px] text-[#111518] mb-1.5 block">
Parent or guardian phone <span className="text-[#c0392b]">*</span>
</label>
<input
id="guardianPhone"
type="tel"
name="guardianPhone"
value={formData.guardianPhone}
onChange={handleInputChange}
autoComplete="off"
className={`w-full px-4 py-3 h-12 rounded-lg border font-sans text-[15px] text-[#111518] outline-none transition-all ${
errors.guardianPhone ? 'border-[#c0392b]' : 'border-[#D1D5DB] focus:border-[#006838] focus:ring-2 focus:ring-[rgba(0,104,56,0.12)]'
}`}
placeholder="+254"
/>
{errors.guardianPhone && <p className="font-sans text-[#c0392b] text-[12px] mt-1">{errors.guardianPhone}</p>}
</div>
</>
)}
</div>
{/* Consent: one tick box per document, optional ones clearly optional */}
<fieldset className="bg-[#F9FAFB] rounded-lg p-5 border border-[#E5E7EB] mb-6">
<legend className="font-heading font-bold text-[#111518] text-sm px-1">{indemnityDoc.title}</legend>
<div
className="max-h-[160px] overflow-y-auto mb-4 p-3 bg-white rounded border border-[#E5E7EB] space-y-2"
tabIndex={0}
aria-label={`${indemnityDoc.title} text`}
>
{paragraphs(indemnityDoc.body).map((para, i) => (
<p key={i} className="font-sans text-[#687279] text-[13px] leading-relaxed">{para}</p>
))}
</div>
<div className="space-y-3">
{[...requiredDocs, ...optionalDocs].map((doc) => {
const key = docKey(doc)
const id = `consent-${doc.slug || key}`
return (
<div key={key} className="flex items-start gap-3">
<input
id={id}
type="checkbox"
checked={Boolean(consents[key])}
onChange={(e) => {
const checked = e.target.checked
setConsents((prev) => ({ ...prev, [key]: checked }))
if (errors.indemnity) setErrors((prev) => ({ ...prev, indemnity: '' }))
}}
aria-required={doc.required}
className="mt-0.5 h-5 w-5 flex-shrink-0 rounded border-[#D1D5DB] accent-[#006838] cursor-pointer"
/>
<label htmlFor={id} className="font-sans text-[#111518] text-sm cursor-pointer">
{doc.document_type === 'Indemnity'
? 'I have read and accept the indemnity declaration above'
: doc.required
? `I accept the ${doc.title}`
: doc.title}
{!doc.required && <span className="text-[#687279]"> (optional)</span>}
{!doc.required && doc.summary && (
<span className="block text-[12px] text-[#687279]">{doc.summary.replace(/^Optional:\s*/i, '')}</span>
)}
{doc.document_type !== 'Indemnity' && doc.slug && (
<>
{' '}
<a href={`/legal#${doc.slug}`} target="_blank" rel="noopener noreferrer"
className="font-heading font-semibold text-[#006838] underline">
Read it
</a>
</>
)}
{doc.version && <span className="text-[#9CA3AF] text-[11px]"> · version {doc.version}</span>}
</label>
</div>
)
})}
</div>
<p className="font-sans text-[#687279] text-[12px] mt-3">
<a href="/legal" target="_blank" rel="noopener noreferrer" className="underline">All terms and our privacy notice</a>
</p>
{errors.indemnity && <p role="alert" className="font-sans text-[#c0392b] text-[12px] mt-2">{errors.indemnity}</p>}
</fieldset>
{/* Navigation */}
<div className="flex items-center justify-between">
<button
onClick={() => setCurrentStep(1)}
className="font-heading font-semibold text-[#687279] text-sm hover:text-[#111518] transition-colors"
>
← Back
</button>
<button
onClick={handleContinueToStep3}
className="px-8 py-4 bg-[#FFF100] hover:bg-[#3BB34A] text-[#714508] hover:text-white font-heading font-bold text-sm uppercase tracking-wide rounded-lg transition-colors"
>
Continue to Payment →
</button>
</div>
</div>
)}
{/* STEP 3: PAYMENT */}
{currentStep === 3 && (
<div>
<h3 className="font-heading font-bold text-[#111518] text-[22px] mb-2">
Complete your entry.
</h3>
<p className="font-sans text-[#687279] text-sm mb-6">
Choose how you&apos;d like to pay. You&apos;ll finish on a secure payment page, and your place is confirmed once the payment reaches us.
</p>
{/* Order Summary */}
<div className="bg-[#006838] rounded-xl p-6 mb-6">
<p className="font-heading font-semibold text-[11px] text-[#3BB34A] uppercase tracking-wide mb-2">
Your Order
</p>
<p className="font-heading font-bold text-white text-lg">
{selectedRaceData?.distance} {selectedRaceData?.name}
</p>
<p className="font-sans text-white/70 text-sm mb-4">
{entryCategory === 'resident' ? 'Kenyan resident' : 'Non-resident'}
</p>
<div className="border-t border-white/15 pt-4">
<p className="font-heading font-semibold text-[11px] text-white/60 uppercase mb-1">
Entry Fee
</p>
<PriceDisplay race={selectedRaceData} className="text-[18px] !text-white" />
{treesText && (
<div className="flex items-center gap-2 mt-3">
<TreePine className="w-3.5 h-3.5 text-[#3BB34A]" />
<span className="font-sans text-white/60 text-[13px]">{treesText}</span>
</div>
)}
</div>
</div>
{/* Payment Methods */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div
onClick={() => setPaymentMethod('mpesa')}
className={`border-2 rounded-xl p-5 cursor-pointer transition-all ${
paymentMethod === 'mpesa' ? 'border-[#006838] bg-[rgba(0,104,56,0.04)]' : 'border-[#E5E7EB]'
}`}
>
<div className="flex items-center gap-2 mb-2">
<span className="text-lg">🇰🇪</span>
<span className="font-heading font-bold text-[#006838] text-lg">M-PESA</span>
</div>
<p className="font-sans text-[#687279] text-[13px]">
Approve the payment with your M-PESA PIN when the prompt arrives on your phone.
</p>
</div>
<div
onClick={() => setPaymentMethod('card')}
className={`border-2 rounded-xl p-5 cursor-pointer transition-all ${
paymentMethod === 'card' ? 'border-[#006838] bg-[rgba(0,104,56,0.04)]' : 'border-[#E5E7EB]'
}`}
>
<span className="font-heading font-bold text-[#111518] text-lg block mb-2">
CARD PAYMENT
</span>
<p className="font-sans text-[#687279] text-[13px] mb-2">
Visa, Mastercard accepted. Secure 3D authentication.
</p>
<span className="font-heading font-semibold text-[#687279] text-[11px]">
VISA · MASTERCARD
</span>
</div>
</div>
{submitError && (
<div role="alert" className="mb-4 rounded-lg border border-[#c0392b]/30 bg-[#c0392b]/5 p-4 font-sans text-[13px] text-[#c0392b]">
{savedEntry ? `Your entry ${savedEntry.registration} is saved, but payment didn't start: ${submitError}` : submitError}
</div>
)}
{!canPay && (
<p className="mb-4 font-sans text-[13px] text-[#687279] text-center">
Online payment opens soon. Check back here, or join the waitlist below to be told first.
</p>
)}
{/* Submit */}
<button
onClick={handleSubmit}
disabled={!paymentMethod || submitting || !canPay}
className={`w-full py-4 rounded-lg font-heading font-bold text-[15px] uppercase tracking-wide transition-colors inline-flex items-center justify-center gap-2 ${
paymentMethod && !submitting && canPay
? 'bg-[#FFF100] hover:bg-[#3BB34A] text-[#714508] hover:text-white'
: 'bg-[#E5E7EB] text-[#687279] cursor-not-allowed'
}`}
>
{submitting && <Loader2 className="w-4 h-4 animate-spin" />}
{submitting ? 'Opening payment…' : savedEntry ? 'Try payment again' : 'Continue to payment'}
</button>
<p className="font-sans text-[#687279] text-[12px] text-center mt-4">
🔒 Payments are processed securely by Paystack. You&apos;ll get a confirmation email once your payment is verified.
</p>
{/* Back Link */}
<button
onClick={() => setCurrentStep(2)}
className="font-heading font-semibold text-[#687279] text-sm hover:text-[#111518] transition-colors mt-6 block mx-auto"
>
← Back to Your Details
</button>
</div>
)}
</div>
</div>
{/* RIGHT: STICKY SUMMARY PANEL (Desktop) */}
<div className="hidden lg:block lg:w-[40%]">
<div className="sticky top-[100px]">
<div className="bg-white rounded-xl shadow-[0_2px_16px_rgba(0,0,0,0.08)] overflow-hidden">
{/* Panel Header */}
<div className="bg-[#006838] px-6 py-5">
<p className="font-heading font-semibold text-[11px] text-[#3BB34A] uppercase tracking-wide">
Your Entry Summary
</p>
<p className="font-sans text-white/65 text-[13px]">
{[label, eventDate].filter(Boolean).join(' · ')}
</p>
</div>
{/* Panel Content */}
<div className="p-6">
<SummaryContent />
</div>
</div>
</div>
</div>
</div>
)}
</div>
</section>
{/* SECTION 5: WAITLIST */}
<section id="waitlist" className="bg-[#111518] py-16">
<div className="max-w-[1200px] mx-auto px-4 text-center">
<span className="font-heading font-semibold text-[11px] text-[#F0C000] uppercase tracking-[0.12em] mb-3 block">
{"CAN'T MAKE IT THIS YEAR?"}
</span>
<div className="w-8 h-0.5 bg-[#F0C000] mx-auto mb-4" />
<h2 className="font-heading font-bold text-white text-[28px] md:text-[36px] mb-3">
Join the Waitlist.
</h2>
<p className="font-sans text-white/70 text-base mb-8 max-w-xl mx-auto">
Be first to know when entries open for the next edition. No spam. One email when registration opens.
</p>
<div className="max-w-[560px] mx-auto">
<div className="flex">
<input
type="email"
placeholder="Enter your email"
className="flex-1 px-5 py-4 h-[52px] rounded-l-xl font-sans text-[15px] text-[#111518] outline-none"
/>
<button className="px-6 h-[52px] bg-[#FFF100] text-[#714508] font-heading font-bold text-[13px] uppercase tracking-wide rounded-r-xl hover:bg-[#3BB34A] hover:text-white transition-colors">
Notify Me
</button>
</div>
<p className="font-sans text-white/40 text-[12px] mt-3">
We respect your privacy. Unsubscribe any time.
</p>
</div>
</div>
</section>
<Footer />
</div>
)
}