'use client' import { useEffect, useState } from 'react' import Link from 'next/link' import { Loader2 } from 'lucide-react' import { formatClock, formatEventDate, formatMoney } from '@/lib/event-types' interface Entry { registration: string registration_status: string payment_status: string amount_due: number amount_paid: number currency: string can_pay: boolean next_action?: string runner: { first_name?: string; full_name?: string } edition: { edition_name?: string; event_date?: string | null; venue?: string; county?: string } race: { category_name?: string; distance_label?: string; start_time?: string | null } bib_number: string | null race_pack_status: string check_in_status: string result: { finish_status: string; chip_time: number | null; gun_time: number | null; overall_rank?: number } | null payments: { date: string; method: string | null; amount: number; status: string; reference: string }[] } const STORE = 'lukenya-my-race-token' function duration(seconds: number | null) { if (!seconds) return null const s = Math.round(seconds) return `${Math.floor(s / 3600)}:${String(Math.floor((s % 3600) / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}` } async function post(url: string, body: unknown): Promise { 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 } export function MyRaceView() { const [token, setToken] = useState(null) const [entry, setEntry] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [email, setEmail] = useState('') const [sent, setSent] = useState(null) const [paying, setPaying] = useState(false) useEffect(() => { // Take the token out of the address bar so it isn't shared by accident, and keep it for this tab. const url = new URL(window.location.href) const fromUrl = url.searchParams.get('token') if (fromUrl) { try { sessionStorage.setItem(STORE, fromUrl) } catch { /* private mode */ } url.searchParams.delete('token') window.history.replaceState(null, '', url.pathname + url.search) } let saved: string | null = fromUrl if (!saved) { try { saved = sessionStorage.getItem(STORE) } catch { saved = null } } setToken(saved) if (!saved) { setLoading(false) return } post('/api/my-race', { token: saved }) .then(setEntry) .catch((err) => { setError((err as Error).message) try { sessionStorage.removeItem(STORE) } catch { /* ignore */ } }) .finally(() => setLoading(false)) }, []) const requestLink = async () => { setError(null) try { const res = await post<{ message: string }>('/api/my-race', { email }) setSent(res.message) } catch (err) { setError((err as Error).message) } } const pay = async () => { if (!entry || !token) return setPaying(true) setError(null) try { const res = await post<{ authorization_url: string }>('/api/payment/start', { registration: entry.registration, payment_token: token, channel: 'mpesa', }) window.location.assign(res.authorization_url) } catch (err) { setError((err as Error).message) setPaying(false) } } const card = 'max-w-[560px] mx-auto bg-white rounded-xl shadow-[0_2px_16px_rgba(0,0,0,0.08)] p-6 md:p-10' if (loading) { return (

Loading your entry…

) } if (!entry) { return (

My Race

Enter the email you registered with and we'll send you a private link to your entry.

{error &&

{error}

} {sent ? (

{sent}

) : (
setEmail(e.target.value)} placeholder="you@example.com" className="flex-1 px-4 h-12 rounded-lg border border-[#D1D5DB] font-sans text-[15px] focus:border-[#006838] focus:ring-2 focus:ring-[rgba(0,104,56,0.12)] outline-none" />
)}
) } const confirmed = entry.registration_status === 'Confirmed' || entry.registration_status === 'Completed' const balance = Math.max(entry.amount_due - entry.amount_paid, 0) const rows: [string, string | null | undefined][] = [ ['Race', [entry.race.distance_label, entry.race.category_name].filter(Boolean).join(' ')], ['Race day', [formatEventDate(entry.edition.event_date), formatClock(entry.race.start_time) && `start ${formatClock(entry.race.start_time)}`].filter(Boolean).join(', ')], ['Venue', [entry.edition.venue, entry.edition.county].filter(Boolean).join(', ')], ['Entry', entry.registration], ['Bib', entry.bib_number || 'Not allocated yet'], ['Race pack', entry.race_pack_status], ['Check-in', entry.check_in_status], ] return (

{entry.edition.edition_name}

{entry.runner.first_name ? `${entry.runner.first_name}, ` : ''} {confirmed ? "you're in." : 'your entry is not confirmed yet.'}

{confirmed ? `Paid ${formatMoney(entry.amount_paid, entry.currency) || ''}.` : balance > 0 ? `${formatMoney(balance, entry.currency)} is still to pay. Your place is confirmed once payment reaches us.` : `Status: ${entry.registration_status}.`}

{entry.next_action && (

Next: {entry.next_action}

)} {entry.can_pay && ( )} {error &&

{error}

}
{rows.map(([label, value]) => (
{label}
{value || '—'}
))} {entry.result && (
Result
{entry.result.finish_status} {duration(entry.result.chip_time || entry.result.gun_time) ? `, ${duration(entry.result.chip_time || entry.result.gun_time)}` : ''} {entry.result.overall_rank ? `, position ${entry.result.overall_rank}` : ''}
)}
{entry.payments.length > 0 && (

Payments

    {entry.payments.map((p) => (
  • {formatEventDate(p.date)} · {p.method || 'Online'} {formatMoney(p.amount, entry.currency)} · {p.status}
  • ))}
)}

This page is private to you. Don't share the link from your email.{' '} Race information

) }