Initial commit
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
import Link from 'next/link'
|
||||
import { TreePine, MapPin, Users, Target, Heart, Globe, GraduationCap } from 'lucide-react'
|
||||
import { Navigation } from '@/components/navigation'
|
||||
import { Footer } from '@/components/footer'
|
||||
|
||||
const missionStats = [
|
||||
{ value: '10,000,000', label: 'Trees Committed', icon: TreePine },
|
||||
{ value: 'ASAL', label: 'Dryland Regions', icon: MapPin },
|
||||
{ value: '780+', label: 'Runners So Far', icon: Users },
|
||||
]
|
||||
|
||||
const timeline = [
|
||||
{
|
||||
year: '2022',
|
||||
title: 'The Beginning',
|
||||
description: 'Lukenya University Marathon launches with 150 runners and a bold mission: plant 10 million trees in Kenya\'s ASAL regions.',
|
||||
},
|
||||
{
|
||||
year: '2023',
|
||||
title: 'Growing Momentum',
|
||||
description: '280 runners join the cause. Corporate relay category introduced. First Environmental Partner signs on.',
|
||||
},
|
||||
{
|
||||
year: '2024',
|
||||
title: 'Record Participation',
|
||||
description: '350+ finishers. Wheelchair category sees record entries. Partnership with local reforestation NGOs expands.',
|
||||
},
|
||||
{
|
||||
year: '2025',
|
||||
title: 'The Future',
|
||||
description: 'Targeting 500+ participants. New university partnerships. Expanded tree-planting verification programme.',
|
||||
},
|
||||
]
|
||||
|
||||
const asalInfo = [
|
||||
{
|
||||
icon: Globe,
|
||||
title: 'What are ASAL Regions?',
|
||||
description: 'Arid and Semi-Arid Lands cover over 80% of Kenya\'s land mass. These dryland ecosystems are home to millions of Kenyans and are most vulnerable to climate change.',
|
||||
},
|
||||
{
|
||||
icon: TreePine,
|
||||
title: 'Why Trees Matter',
|
||||
description: 'Reforestation in ASAL regions combats desertification, improves soil quality, provides sustainable livelihoods, and sequesters carbon dioxide from the atmosphere.',
|
||||
},
|
||||
{
|
||||
icon: Target,
|
||||
title: 'Our Approach',
|
||||
description: 'We partner with verified reforestation NGOs to plant indigenous tree species. Each tree is GPS-tracked and monitored for survival over 3 years.',
|
||||
},
|
||||
]
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Navigation />
|
||||
|
||||
{/* Page Hero */}
|
||||
<section className="bg-[#006838] pt-20">
|
||||
<div className="max-w-5xl mx-auto px-4 text-center py-16 md:py-24">
|
||||
<span className="text-[#ffcd05] font-semibold text-xs uppercase tracking-[0.12em] mb-4 block">
|
||||
Our Story
|
||||
</span>
|
||||
<h1 className="text-3xl md:text-4xl lg:text-5xl font-extrabold text-white mb-4">
|
||||
Kenya's Only Environmental Marathon
|
||||
</h1>
|
||||
<p className="text-white/80 text-lg max-w-2xl mx-auto">
|
||||
More than a race. A movement to restore Kenya's dryland ecosystems, one entry at a time.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Mission Stats */}
|
||||
<section className="bg-[#006838] py-12">
|
||||
<div className="max-w-5xl mx-auto px-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
||||
{missionStats.map((stat, index) => (
|
||||
<div key={index} className="text-center">
|
||||
<stat.icon className="w-8 h-8 text-[#ffcd05] mx-auto mb-2" />
|
||||
<p className="text-white font-extrabold text-2xl md:text-3xl">{stat.value}</p>
|
||||
<p className="text-white/70 text-sm">{stat.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 10M Trees Mission */}
|
||||
<section className="bg-white py-16 md:py-24">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<TreePine className="w-12 h-12 text-[#006838] mx-auto mb-4" />
|
||||
<span className="text-[#006838] font-semibold text-xs uppercase tracking-[0.12em] mb-2 block">
|
||||
The 10 Million Trees Mission
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-[#006838]">
|
||||
Run for Kenya. Plant a Future.
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="prose prose-lg max-w-none text-[#6b7280]">
|
||||
<p className="text-lg leading-relaxed mb-6">
|
||||
The Lukenya University Marathon was founded with a simple but ambitious goal: to become Kenya's first and only marathon with a verified environmental mission.
|
||||
</p>
|
||||
<p className="text-lg leading-relaxed mb-6">
|
||||
Every entry fee directly funds tree planting in Kenya's ASAL (Arid and Semi-Arid Land) regions — the dryland ecosystems that cover over 80% of our country and are most threatened by climate change. We don't just promise impact; we verify it. Each tree is GPS-tracked, monitored for survival, and reported in our annual impact report.
|
||||
</p>
|
||||
<p className="text-lg leading-relaxed">
|
||||
Our target: 10 million trees. Every entry brings Kenya one step closer to a greener future.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ASAL Information */}
|
||||
<section className="bg-[#F5F7FC] py-16 md:py-24">
|
||||
<div className="max-w-5xl mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<span className="text-[#006838] font-semibold text-xs uppercase tracking-[0.12em] mb-2 block">
|
||||
Understanding the Impact
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-[#006838]">
|
||||
Why ASAL Regions?
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{asalInfo.map((item, index) => (
|
||||
<div key={index} className="bg-white rounded-xl p-6 shadow-[0_2px_16px_rgba(0,0,0,0.08)]">
|
||||
<item.icon className="w-10 h-10 text-[#006838] mb-4" />
|
||||
<h3 className="font-bold text-[#111518] mb-2">{item.title}</h3>
|
||||
<p className="text-[#6b7280] text-sm leading-relaxed">{item.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Timeline */}
|
||||
<section className="bg-white py-16 md:py-24">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<span className="text-[#006838] font-semibold text-xs uppercase tracking-[0.12em] mb-2 block">
|
||||
Our Journey
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-[#006838]">
|
||||
Growing Every Year
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{timeline.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex gap-6"
|
||||
>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-12 h-12 bg-[#006838] rounded-full flex items-center justify-center text-white font-bold text-sm">
|
||||
{item.year}
|
||||
</div>
|
||||
{index < timeline.length - 1 && (
|
||||
<div className="w-0.5 flex-1 bg-[#e5e7eb] mt-2" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 pb-8">
|
||||
<h3 className="font-bold text-[#111518] text-lg mb-2">{item.title}</h3>
|
||||
<p className="text-[#6b7280]">{item.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* About Lukenya University */}
|
||||
<section className="bg-[#006838] py-16 md:py-24">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<GraduationCap className="w-12 h-12 text-[#ffcd05] mx-auto mb-4" />
|
||||
<span className="text-[#ffcd05] font-semibold text-xs uppercase tracking-[0.12em] mb-2 block">
|
||||
Our Home
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-white">
|
||||
Lukenya University
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/10 backdrop-blur rounded-2xl p-8 md:p-12">
|
||||
<p className="text-white/90 text-lg leading-relaxed mb-6">
|
||||
Lukenya University, located in Machakos County, Kenya, serves as the proud host of the Lukenya University Marathon. The university's commitment to environmental sustainability and community development aligns perfectly with the marathon's mission.
|
||||
</p>
|
||||
<p className="text-white/90 text-lg leading-relaxed mb-6">
|
||||
The scenic campus and surrounding terrain provide a challenging yet beautiful course that showcases the natural beauty of Machakos County — while reminding every runner why protecting Kenya's landscapes matters.
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-6">
|
||||
<div className="flex items-center gap-2 text-white/80">
|
||||
<MapPin className="w-5 h-5 text-[#ffcd05]" />
|
||||
<span>Machakos County, Kenya</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-white/80">
|
||||
<Heart className="w-5 h-5 text-[#ffcd05]" />
|
||||
<span>University-Verified Programme</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Values Section */}
|
||||
<section className="bg-[#F5F7FC] py-16 md:py-24">
|
||||
<div className="max-w-5xl mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<span className="text-[#006838] font-semibold text-xs uppercase tracking-[0.12em] mb-2 block">
|
||||
What We Stand For
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-[#006838]">
|
||||
Our Values
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div className="bg-white rounded-xl p-6 text-center shadow-[0_2px_16px_rgba(0,0,0,0.08)]">
|
||||
<div className="w-12 h-12 bg-[#006838]/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<TreePine className="w-6 h-6 text-[#006838]" />
|
||||
</div>
|
||||
<h3 className="font-bold text-[#111518] mb-2">Environmental Impact</h3>
|
||||
<p className="text-[#6b7280] text-sm">Every race entry contributes to verified reforestation.</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-6 text-center shadow-[0_2px_16px_rgba(0,0,0,0.08)]">
|
||||
<div className="w-12 h-12 bg-[#006838]/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Users className="w-6 h-6 text-[#006838]" />
|
||||
</div>
|
||||
<h3 className="font-bold text-[#111518] mb-2">Inclusivity</h3>
|
||||
<p className="text-[#6b7280] text-sm">7 categories welcoming runners of all abilities.</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-6 text-center shadow-[0_2px_16px_rgba(0,0,0,0.08)]">
|
||||
<div className="w-12 h-12 bg-[#006838]/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Target className="w-6 h-6 text-[#006838]" />
|
||||
</div>
|
||||
<h3 className="font-bold text-[#111518] mb-2">Transparency</h3>
|
||||
<p className="text-[#6b7280] text-sm">GPS-tracked trees and published impact reports.</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-6 text-center shadow-[0_2px_16px_rgba(0,0,0,0.08)]">
|
||||
<div className="w-12 h-12 bg-[#006838]/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Heart className="w-6 h-6 text-[#006838]" />
|
||||
</div>
|
||||
<h3 className="font-bold text-[#111518] mb-2">Community</h3>
|
||||
<p className="text-[#6b7280] text-sm">Building a movement of runners who care.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="bg-[#006838] py-16">
|
||||
<div className="max-w-3xl mx-auto px-4 text-center">
|
||||
<h2 className="text-3xl md:text-4xl font-extrabold text-white mb-4">
|
||||
Join the Mission
|
||||
</h2>
|
||||
<p className="text-white/80 text-lg mb-8">
|
||||
Run with purpose. Plant a future. Be part of Kenya's environmental marathon movement.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row justify-center gap-4">
|
||||
<Link
|
||||
href="/registration"
|
||||
className="inline-block bg-[#ffcd05] text-[#111518] font-bold text-sm px-10 py-4 rounded-lg hover:bg-[#c4930f] transition-colors duration-200"
|
||||
>
|
||||
REGISTER NOW
|
||||
</Link>
|
||||
<Link
|
||||
href="/sponsors"
|
||||
className="inline-block border border-white text-white font-semibold text-sm px-10 py-4 rounded-lg hover:bg-white/10 transition-colors duration-200"
|
||||
>
|
||||
BECOME A PARTNER
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { METHODS, frappeCall } from '@/lib/lukenya-api'
|
||||
|
||||
// Current published legal documents (terms, indemnity, privacy, optional consents).
|
||||
export async function GET() {
|
||||
try {
|
||||
const docs = await frappeCall<unknown[]>(METHODS.legalDocuments, { revalidate: 60 })
|
||||
return NextResponse.json(docs || [], { headers: { 'Cache-Control': 'public, max-age=60' } })
|
||||
} catch {
|
||||
return NextResponse.json([], { status: 503 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { FrappeError, METHODS, frappeCall } from '@/lib/lukenya-api'
|
||||
|
||||
// POST { token } → the runner's entry. POST { email } → email a fresh My Race link.
|
||||
// Tokens travel in the request body, not the URL, so they don't end up in access logs.
|
||||
export async function POST(req: Request) {
|
||||
const body = await req.json().catch(() => ({}))
|
||||
try {
|
||||
if (typeof body.token === 'string' && body.token) {
|
||||
const entry = await frappeCall<Record<string, unknown>>(METHODS.myRace, { args: { token: body.token }, post: true })
|
||||
return NextResponse.json(entry, { headers: { 'Cache-Control': 'no-store' } })
|
||||
}
|
||||
if (typeof body.email === 'string' && body.email.includes('@')) {
|
||||
const result = await frappeCall<{ message: string }>(METHODS.myRaceLink, {
|
||||
args: { email: body.email.trim().toLowerCase() },
|
||||
post: true,
|
||||
})
|
||||
return NextResponse.json(result)
|
||||
}
|
||||
return NextResponse.json({ error: 'Enter the email you registered with.' }, { status: 400 })
|
||||
} catch (err) {
|
||||
const status = err instanceof FrappeError && err.status < 500 ? err.status : 502
|
||||
return NextResponse.json({ error: (err as Error).message }, { status })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { FrappeError, METHODS, frappeCall, registrationEnabled } from '@/lib/lukenya-api'
|
||||
|
||||
// Asks the backend to open a Paystack checkout for a saved entry and returns its link.
|
||||
export async function POST(req: Request) {
|
||||
if (!registrationEnabled()) {
|
||||
return NextResponse.json({ error: 'Online payment is not open yet.' }, { status: 403 })
|
||||
}
|
||||
const body = await req.json().catch(() => ({}))
|
||||
const registration = typeof body.registration === 'string' ? body.registration : ''
|
||||
if (!registration) return NextResponse.json({ error: 'No entry to pay for.' }, { status: 400 })
|
||||
|
||||
try {
|
||||
const result = await frappeCall<{ authorization_url: string; reference: string }>(METHODS.startPayment, {
|
||||
args: {
|
||||
registration,
|
||||
payment_token: typeof body.payment_token === 'string' ? body.payment_token : undefined,
|
||||
channel: body.channel === 'card' ? 'card' : 'mpesa',
|
||||
},
|
||||
post: true,
|
||||
})
|
||||
if (!result?.authorization_url) throw new Error('The payment page could not be opened.')
|
||||
return NextResponse.json(result)
|
||||
} catch (err) {
|
||||
const status = err instanceof FrappeError && err.status < 500 ? 400 : 502
|
||||
return NextResponse.json({ error: (err as Error).message }, { status })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { FrappeError, METHODS, frappeCall } from '@/lib/lukenya-api'
|
||||
|
||||
// Polled by /payment/return. The backend checks with Paystack itself before answering,
|
||||
// so the browser being sent back here never confirms a payment on its own.
|
||||
export async function GET(req: Request) {
|
||||
const reference = new URL(req.url).searchParams.get('reference') || ''
|
||||
if (!/^[A-Za-z0-9._-]{4,100}$/.test(reference)) {
|
||||
return NextResponse.json({ error: 'Missing or invalid payment reference.' }, { status: 400 })
|
||||
}
|
||||
try {
|
||||
const result = await frappeCall<Record<string, unknown>>(METHODS.paymentStatus, { args: { reference } })
|
||||
return NextResponse.json(result, { headers: { 'Cache-Control': 'no-store' } })
|
||||
} catch (err) {
|
||||
const status = (err as FrappeError).status
|
||||
if (status === 404) return NextResponse.json({ error: 'We could not find this payment.' }, { status: 404 })
|
||||
// The server re-checks with Paystack in the background; keep the runner on the waiting screen.
|
||||
return NextResponse.json({ status: 'Pending' }, { headers: { 'Cache-Control': 'no-store' } })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { FrappeError, METHODS, frappeCall, registrationEnabled } from '@/lib/lukenya-api'
|
||||
|
||||
// The browser posts the form here; this route forwards it to Frappe. The backend
|
||||
// re-validates everything and decides the price, so nothing here is trusted for money.
|
||||
|
||||
const ALLOWED = [
|
||||
'event_edition', 'race_category', 'residency', 'first_name', 'last_name', 'id_number',
|
||||
'date_of_birth', 'gender', 'nationality', 'email', 'phone', 'whatsapp', 'shirt_size',
|
||||
'emergency_contact_name', 'emergency_contact_phone', 'guardian_name', 'guardian_phone',
|
||||
'indemnity_accepted', 'communication_consent', 'preferred_payment', 'emergency_contact_relationship',
|
||||
'support_request', 'consents',
|
||||
] as const
|
||||
|
||||
const REQUIRED = [
|
||||
'race_category', 'first_name', 'last_name', 'email', 'phone',
|
||||
'emergency_contact_name', 'emergency_contact_phone',
|
||||
] as const
|
||||
|
||||
export async function POST(req: Request) {
|
||||
if (!registrationEnabled()) {
|
||||
return NextResponse.json({ error: 'Online registration is not open yet.' }, { status: 403 })
|
||||
}
|
||||
|
||||
let body: Record<string, unknown>
|
||||
try {
|
||||
body = await req.json()
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'The form data could not be read.' }, { status: 400 })
|
||||
}
|
||||
|
||||
const payload: Record<string, unknown> = {}
|
||||
for (const key of ALLOWED) {
|
||||
const value = body[key]
|
||||
if (value === undefined || value === null) continue
|
||||
payload[key] = typeof value === 'string' ? value.trim().slice(0, key === 'support_request' ? 1000 : 500) : value
|
||||
}
|
||||
payload.event_edition = payload.event_edition || process.env.FRAPPE_EDITION
|
||||
|
||||
const missing = REQUIRED.filter((k) => !payload[k])
|
||||
if (!payload.event_edition) missing.unshift('race edition' as never)
|
||||
if (payload.indemnity_accepted !== true) missing.push('indemnity acceptance' as never)
|
||||
if (missing.length) {
|
||||
return NextResponse.json({ error: `Please complete: ${missing.join(', ').replace(/_/g, ' ')}.` }, { status: 400 })
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await frappeCall<{
|
||||
registration: string
|
||||
amount_due: number
|
||||
currency: string
|
||||
payment_token?: string
|
||||
}>(METHODS.createRegistration, { args: { payload }, post: true })
|
||||
return NextResponse.json({
|
||||
registration: result.registration,
|
||||
amount_due: result.amount_due,
|
||||
currency: result.currency,
|
||||
payment_token: result.payment_token,
|
||||
})
|
||||
} catch (err) {
|
||||
const status = err instanceof FrappeError && err.status < 500 ? 400 : 502
|
||||
return NextResponse.json({ error: (err as Error).message }, { status })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
// Called by the Frappe backend after race data changes, so the site updates at once
|
||||
// instead of within the 60-second refresh window.
|
||||
export async function POST(req: Request) {
|
||||
const secret = process.env.REVALIDATE_SECRET
|
||||
if (!secret || req.headers.get('x-revalidate-secret') !== secret) {
|
||||
return NextResponse.json({ error: 'Not allowed.' }, { status: 401 })
|
||||
}
|
||||
revalidatePath('/', 'layout')
|
||||
return NextResponse.json({ revalidated: true, at: new Date().toISOString() })
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
/* Lukenya University Marathon Design System - V3 */
|
||||
--green-dark: #006838;
|
||||
--green-mid: #3BB34A;
|
||||
--yellow: #FFF100;
|
||||
--gold: #F0C000;
|
||||
--dark: #111518;
|
||||
--gray-light: #F9FAFB;
|
||||
--white: #FFFFFF;
|
||||
--text-muted: #687279;
|
||||
--btn-text: #714508;
|
||||
|
||||
/* Semantic tokens */
|
||||
--background: var(--white);
|
||||
--foreground: var(--dark);
|
||||
--card: var(--white);
|
||||
--card-foreground: var(--dark);
|
||||
--popover: var(--white);
|
||||
--popover-foreground: var(--dark);
|
||||
--primary: var(--green-dark);
|
||||
--primary-foreground: var(--white);
|
||||
--secondary: var(--gray-light);
|
||||
--secondary-foreground: var(--green-dark);
|
||||
--muted: var(--gray-light);
|
||||
--muted-foreground: var(--text-muted);
|
||||
--accent: var(--yellow);
|
||||
--accent-foreground: var(--btn-text);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: var(--white);
|
||||
--border: #e5e7eb;
|
||||
--input: #e5e7eb;
|
||||
--ring: var(--green-dark);
|
||||
--radius: 0.375rem;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-sans: 'Quicksand', sans-serif;
|
||||
--font-display: 'Antonio', sans-serif;
|
||||
--font-heading: 'Raleway', sans-serif;
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--radius-sm: calc(var(--radius) - 2px);
|
||||
--radius-md: var(--radius);
|
||||
--radius-lg: calc(var(--radius) + 2px);
|
||||
--radius-xl: calc(var(--radius) + 6px);
|
||||
|
||||
/* Custom colors for direct use */
|
||||
--color-green-dark: var(--green-dark);
|
||||
--color-green-mid: var(--green-mid);
|
||||
--color-yellow: var(--yellow);
|
||||
--color-gold: var(--gold);
|
||||
--color-dark: var(--dark);
|
||||
--color-gray-light: var(--gray-light);
|
||||
--color-text-muted: var(--text-muted);
|
||||
--color-btn-text: var(--btn-text);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
/* Registration Status Badge Pulse Animation */
|
||||
@keyframes pulse-dot {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.animate-pulse-dot {
|
||||
animation: pulse-dot 2s infinite;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { Metadata, Viewport } from 'next'
|
||||
import { Antonio, Raleway, Quicksand } from 'next/font/google'
|
||||
import { Analytics } from '@vercel/analytics/next'
|
||||
import './globals.css'
|
||||
import { EventProvider } from '@/components/event-provider'
|
||||
import { getPublicEvent, registrationEnabled } from '@/lib/lukenya-api'
|
||||
|
||||
// Race data comes from the backend; refresh it at most once a minute.
|
||||
export const revalidate = 60
|
||||
|
||||
const antonio = Antonio({
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '700'],
|
||||
variable: '--font-antonio',
|
||||
display: 'swap',
|
||||
})
|
||||
|
||||
const raleway = Raleway({
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '600', '700'],
|
||||
variable: '--font-raleway',
|
||||
display: 'swap',
|
||||
})
|
||||
|
||||
const quicksand = Quicksand({
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '500', '600'],
|
||||
variable: '--font-quicksand',
|
||||
display: 'swap',
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '10 Million Trees Marathon | Lukenya University',
|
||||
description: 'Africa\'s only university-backed marathon with a verified reforestation mission. Running, cycling and virtual races, competitive prize money and certified timing.',
|
||||
keywords: ['marathon', 'Kenya', 'Lukenya University', 'environmental', 'running', 'ASAL', 'reforestation', '10 million trees', 'Machakos'],
|
||||
openGraph: {
|
||||
title: '10 Million Trees Marathon | Lukenya University',
|
||||
description: 'Run for Kenya. Plant a Future. Africa\'s only university-backed environmental marathon.',
|
||||
type: 'website',
|
||||
},
|
||||
}
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: '#006838',
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
}
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
const event = await getPublicEvent()
|
||||
return (
|
||||
<html lang="en" className="bg-background">
|
||||
<body className={`${antonio.variable} ${raleway.variable} ${quicksand.variable} font-sans antialiased`}>
|
||||
<EventProvider event={event} registrationEnabled={registrationEnabled()}>
|
||||
{children}
|
||||
</EventProvider>
|
||||
{process.env.NODE_ENV === 'production' && <Analytics />}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Navigation } from '@/components/navigation'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { RichText } from '@/components/rich-text'
|
||||
import { METHODS, frappeCall } from '@/lib/lukenya-api'
|
||||
import { formatEventDate } from '@/lib/event-types'
|
||||
|
||||
export const revalidate = 60
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Terms and Privacy | 10 Million Trees Marathon',
|
||||
description: 'Participation terms, indemnity, privacy notice and consent documents for the Lukenya University Marathon.',
|
||||
}
|
||||
|
||||
interface LegalDoc {
|
||||
name: string
|
||||
document_type: string
|
||||
slug: string
|
||||
version: string
|
||||
title: string
|
||||
summary: string | null
|
||||
body: string
|
||||
effective_from: string | null
|
||||
required: boolean
|
||||
applies_to: string
|
||||
}
|
||||
|
||||
async function documents(): Promise<LegalDoc[]> {
|
||||
try {
|
||||
return (await frappeCall<LegalDoc[]>(METHODS.legalDocuments, { revalidate: 60 })) || []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export default async function LegalPage() {
|
||||
const docs = await documents()
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
<Navigation />
|
||||
<section className="bg-[#006838] pt-32 pb-14 px-4">
|
||||
<div className="max-w-3xl mx-auto text-center">
|
||||
<h1 className="text-3xl md:text-4xl font-extrabold text-white mb-3">Terms, Privacy and Consent</h1>
|
||||
<p className="text-white/80">The documents every runner agrees to when they enter. Each shows its version and when it took effect.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section className="py-12 px-4">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
{docs.length === 0 ? (
|
||||
<p className="text-[#6b7280]">The documents couldn't be loaded just now. Please try again shortly.</p>
|
||||
) : (
|
||||
<>
|
||||
<nav aria-label="Documents" className="mb-10">
|
||||
<ul className="flex flex-wrap gap-2">
|
||||
{docs.map((d) => (
|
||||
<li key={d.slug}>
|
||||
<a href={`#${d.slug}`} className="inline-block rounded-full border border-[#006838] px-4 py-1.5 text-sm font-semibold text-[#006838] hover:bg-[#006838] hover:text-white">
|
||||
{d.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
{docs.map((d) => (
|
||||
<article key={d.slug} id={d.slug} className="scroll-mt-28 border-t border-[#e5e7eb] py-10">
|
||||
<h2 className="text-2xl font-bold text-[#111518]">{d.title}</h2>
|
||||
<p className="mt-1 mb-5 text-sm text-[#6b7280]">
|
||||
Version {d.version}
|
||||
{d.effective_from ? ` · effective ${formatEventDate(d.effective_from.slice(0, 10))}` : ''}
|
||||
{d.required ? '' : ' · optional'}
|
||||
{d.applies_to === 'Under 18 only' ? ' · for runners under 18' : ''}
|
||||
</p>
|
||||
<RichText html={d.body} className="prose-legal space-y-4 text-[15px] leading-relaxed text-[#374151] [&_a]:text-[#006838] [&_a]:underline [&_ul]:list-disc [&_ul]:pl-6 [&_ol]:list-decimal [&_ol]:pl-6" />
|
||||
</article>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
'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<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
|
||||
}
|
||||
|
||||
export function MyRaceView() {
|
||||
const [token, setToken] = useState<string | null>(null)
|
||||
const [entry, setEntry] = useState<Entry | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [email, setEmail] = useState('')
|
||||
const [sent, setSent] = useState<string | null>(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<Entry>('/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 (
|
||||
<div className={`${card} text-center`}>
|
||||
<Loader2 className="w-10 h-10 text-[#006838] mx-auto animate-spin" />
|
||||
<p className="font-sans text-[#687279] mt-4">Loading your entry…</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!entry) {
|
||||
return (
|
||||
<div className={card}>
|
||||
<h1 className="font-heading font-bold text-[#111518] text-[26px] mb-2">My Race</h1>
|
||||
<p className="font-sans text-[#687279] text-[15px] mb-6">
|
||||
Enter the email you registered with and we'll send you a private link to your entry.
|
||||
</p>
|
||||
{error && <p role="alert" className="mb-4 font-sans text-[13px] text-[#c0392b]">{error}</p>}
|
||||
{sent ? (
|
||||
<p className="font-sans text-[#006838] text-[15px]">{sent}</p>
|
||||
) : (
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<label htmlFor="my-race-email" className="sr-only">Email</label>
|
||||
<input
|
||||
id="my-race-email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<button
|
||||
onClick={requestLink}
|
||||
disabled={!email.includes('@')}
|
||||
className="h-12 px-6 rounded-lg bg-[#006838] text-white font-heading font-bold text-sm disabled:bg-[#E5E7EB] disabled:text-[#687279]"
|
||||
>
|
||||
Email my link
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={card}>
|
||||
<p className="font-sans text-[#687279] text-[14px]">{entry.edition.edition_name}</p>
|
||||
<h1 className="font-heading font-bold text-[#111518] text-[26px] mb-1">
|
||||
{entry.runner.first_name ? `${entry.runner.first_name}, ` : ''}
|
||||
{confirmed ? "you're in." : 'your entry is not confirmed yet.'}
|
||||
</h1>
|
||||
<p className="font-sans text-[15px] mb-6 text-[#687279]">
|
||||
{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}.`}
|
||||
</p>
|
||||
|
||||
{entry.next_action && (
|
||||
<p className="mb-6 rounded-lg bg-[#F2F8F4] border border-[#3BB34A]/30 px-4 py-3 font-sans text-[14px] text-[#111518]">
|
||||
<span className="font-heading font-bold text-[#006838]">Next: </span>{entry.next_action}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{entry.can_pay && (
|
||||
<button
|
||||
onClick={pay}
|
||||
disabled={paying}
|
||||
className="w-full h-12 mb-6 rounded-lg bg-[#FFF100] text-[#714508] font-heading font-bold text-sm uppercase tracking-wide hover:bg-[#3BB34A] hover:text-white inline-flex items-center justify-center gap-2"
|
||||
>
|
||||
{paying && <Loader2 className="w-4 h-4 animate-spin" />}
|
||||
{paying ? 'Opening payment…' : `Pay ${formatMoney(balance, entry.currency)}`}
|
||||
</button>
|
||||
)}
|
||||
{error && <p role="alert" className="mb-4 font-sans text-[13px] text-[#c0392b]">{error}</p>}
|
||||
|
||||
<dl className="divide-y divide-[#E5E7EB] border-y border-[#E5E7EB]">
|
||||
{rows.map(([label, value]) => (
|
||||
<div key={label} className="flex justify-between gap-4 py-3">
|
||||
<dt className="font-heading font-semibold text-[13px] text-[#687279]">{label}</dt>
|
||||
<dd className="font-sans text-[14px] text-[#111518] text-right">{value || '—'}</dd>
|
||||
</div>
|
||||
))}
|
||||
{entry.result && (
|
||||
<div className="flex justify-between gap-4 py-3">
|
||||
<dt className="font-heading font-semibold text-[13px] text-[#687279]">Result</dt>
|
||||
<dd className="font-sans text-[14px] text-[#111518] text-right">
|
||||
{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}` : ''}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
|
||||
{entry.payments.length > 0 && (
|
||||
<div className="mt-6">
|
||||
<h2 className="font-heading font-bold text-[#111518] text-[15px] mb-2">Payments</h2>
|
||||
<ul className="space-y-1">
|
||||
{entry.payments.map((p) => (
|
||||
<li key={p.reference || p.date} className="flex justify-between font-sans text-[13px] text-[#687279]">
|
||||
<span>{formatEventDate(p.date)} · {p.method || 'Online'}</span>
|
||||
<span>{formatMoney(p.amount, entry.currency)} · {p.status}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="mt-8 font-sans text-[12px] text-[#687279]">
|
||||
This page is private to you. Don't share the link from your email.{' '}
|
||||
<Link href="/race-info" className="text-[#006838] underline">Race information</Link>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Navigation } from '@/components/navigation'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { MyRaceView } from './my-race-view'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'My Race | 10 Million Trees Marathon',
|
||||
robots: { index: false },
|
||||
referrer: 'no-referrer',
|
||||
}
|
||||
|
||||
export default function MyRacePage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F9FAFB]">
|
||||
<Navigation />
|
||||
<section className="bg-[#006838] pt-32 pb-24 px-4">
|
||||
<MyRaceView />
|
||||
</section>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+632
@@ -0,0 +1,632 @@
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import {
|
||||
ShieldCheck,
|
||||
Trophy,
|
||||
TreePine,
|
||||
Users,
|
||||
MapPin,
|
||||
Globe,
|
||||
Bike,
|
||||
ArrowRight
|
||||
} from 'lucide-react'
|
||||
import { Navigation } from '@/components/navigation'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { HeroCarousel } from '@/components/hero-carousel'
|
||||
import { getPublicEvent, getPublicPrizes } from '@/lib/lukenya-api'
|
||||
import {
|
||||
countWord,
|
||||
displayPrice,
|
||||
editionLabel,
|
||||
findPrize,
|
||||
formatClock,
|
||||
formatEventDate,
|
||||
formatMoney,
|
||||
registrationIsOpen,
|
||||
registrationStatusText,
|
||||
venueLine,
|
||||
} from '@/lib/event-types'
|
||||
|
||||
export const revalidate = 60
|
||||
|
||||
const galleryImages = [
|
||||
'https://marathon.lukenyauniversity.ac.ke/wp-content/uploads/2026/04/Artboard-1-copy-4-100_result.jpg',
|
||||
'https://marathon.lukenyauniversity.ac.ke/wp-content/uploads/2026/04/Artboard-1-copy-5-100_result.jpg',
|
||||
'https://marathon.lukenyauniversity.ac.ke/wp-content/uploads/2026/04/Artboard-1-copy-6-100_result.jpg',
|
||||
'https://marathon.lukenyauniversity.ac.ke/wp-content/uploads/2026/04/Artboard-1-copy-7-100_result.jpg',
|
||||
'https://marathon.lukenyauniversity.ac.ke/wp-content/uploads/2026/04/Artboard-1-copy-8-100_result.jpg',
|
||||
]
|
||||
|
||||
const sponsors = [
|
||||
{ name: 'Betika', url: 'https://marathon.lukenyauniversity.ac.ke/wp-content/uploads/2025/04/Betika.jpg' },
|
||||
{ name: 'Greenlife', url: 'https://marathon.lukenyauniversity.ac.ke/wp-content/uploads/2025/04/Greenlife.jpg' },
|
||||
{ name: 'Family Bank', url: 'https://marathon.lukenyauniversity.ac.ke/wp-content/uploads/2025/04/Family-bank.jpg' },
|
||||
{ name: 'Lukenya Schools', url: 'https://marathon.lukenyauniversity.ac.ke/wp-content/uploads/2025/04/Lukenya-schools-1.jpg' },
|
||||
]
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
quote: "The Lukenya Hills will humble you. The finish line will change you. I have run 11 marathons. This one meant something different.",
|
||||
name: "David M.",
|
||||
category: "42KM Full Marathon",
|
||||
edition: "4th Edition"
|
||||
},
|
||||
{
|
||||
quote: "I came for the race. I stayed because of the trees. Knowing my entry funded real reforestation made this the most purposeful race I have ever entered.",
|
||||
name: "Amina K.",
|
||||
category: "21KM Half Marathon",
|
||||
edition: "3rd Edition"
|
||||
}
|
||||
]
|
||||
|
||||
export default async function HomePage() {
|
||||
const [event, prizes] = await Promise.all([getPublicEvent(), getPublicPrizes()])
|
||||
const edition = event.edition
|
||||
const categories = event.categories
|
||||
const isOpen = registrationIsOpen(event)
|
||||
const label = editionLabel(edition)
|
||||
const date = formatEventDate(edition?.event_date)
|
||||
const venue = venueLine(edition)
|
||||
const featured = categories.find((c) => c.is_featured) || categories[0]
|
||||
const featuredStart = featured ? formatClock(featured.start_time) : null
|
||||
|
||||
const heroStats = [
|
||||
{ value: '300+', label: 'Runners' },
|
||||
{ value: label || 'Next', label: isOpen ? 'Edition: entries open' : 'Edition' },
|
||||
{ value: categories.length ? String(categories.length) : '–', label: 'Categories' },
|
||||
{ value: '10M', label: 'Trees Mission' },
|
||||
]
|
||||
|
||||
const trustSignals = [
|
||||
{ icon: ShieldCheck, label: 'Sporthive Certified Timing' },
|
||||
{ icon: Trophy, label: 'Competitive Prize Money' },
|
||||
{ icon: TreePine, label: '10M Trees Mission' },
|
||||
{ icon: Users, label: '300+ Annual Finishers' },
|
||||
{ icon: MapPin, label: venue || 'Venue to be announced' },
|
||||
{ icon: Globe, label: 'Open to International Runners' },
|
||||
]
|
||||
|
||||
const featuredPrizeCategory = featured?.prize_eligible ? featured.name : undefined
|
||||
const prizeCards = [
|
||||
{ icon: Trophy, title: `Men, ${featured?.distance_label || 'Marathon'}, 1st place`, amount: findPrize(prizes, 'Men', 1, featuredPrizeCategory) },
|
||||
{ icon: Trophy, title: `Women, ${featured?.distance_label || 'Marathon'}, 1st place`, amount: findPrize(prizes, 'Women', 1, featuredPrizeCategory) },
|
||||
{ icon: Users, title: 'Top corporate relay team', amount: findPrize(prizes, 'Corporate Relay', 1) },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
<Navigation />
|
||||
|
||||
{/* SECTION 1 — HERO (FIX 1: Left-aligned layout) */}
|
||||
<section className="relative min-h-screen flex flex-col">
|
||||
<HeroCarousel />
|
||||
|
||||
{/* Hero Content - LEFT ALIGNED */}
|
||||
<div className="relative z-10 flex-1 flex flex-col justify-center px-6 md:px-12 pt-20">
|
||||
<div className="max-w-[1200px] mx-auto w-full">
|
||||
<div className="max-w-[600px]">
|
||||
{/* Eyebrow Badge */}
|
||||
<span className="inline-block bg-[#3BB34A] text-white font-[family-name:var(--font-raleway)] font-semibold text-[11px] uppercase tracking-[0.06em] px-4 py-2 rounded-full mb-6">
|
||||
{['Kenya', edition?.county, label].filter(Boolean).join(' · ')}
|
||||
</span>
|
||||
|
||||
{/* H1 */}
|
||||
<h1 className="font-[family-name:var(--font-antonio)] font-bold text-white text-[40px] md:text-[56px] lg:text-[88px] uppercase leading-none tracking-[-0.01em] mb-6">
|
||||
Run, Ride<br />& Restore.
|
||||
</h1>
|
||||
|
||||
{/* Subheading */}
|
||||
<p className="font-[family-name:var(--font-quicksand)] font-medium text-white/85 text-[17px] md:text-[20px] leading-relaxed mb-10">
|
||||
Africa's only university-backed marathon with a verified reforestation mission.<br />
|
||||
{[categories.length ? `${categories.length} categories` : null, venue, date].filter(Boolean).join(' · ')}
|
||||
</p>
|
||||
|
||||
{/* CTA Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-6">
|
||||
<Link
|
||||
href="/registration"
|
||||
className="h-[52px] px-7 bg-[#FFF100] text-[#714508] font-[family-name:var(--font-raleway)] font-bold text-[14px] uppercase tracking-[0.08em] rounded-md hover:bg-[#3BB34A] hover:text-white transition-colors duration-200 flex items-center justify-center"
|
||||
>
|
||||
Register Now
|
||||
</Link>
|
||||
<Link
|
||||
href="/race-info"
|
||||
className="h-[52px] px-7 border border-white text-white font-[family-name:var(--font-raleway)] font-bold text-[14px] uppercase tracking-[0.08em] rounded-md hover:bg-white hover:text-[#111518] transition-colors duration-200 flex items-center justify-center"
|
||||
>
|
||||
Race Information
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* FIX 2: Registration Status Badge (replaces countdown) */}
|
||||
<div
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full border border-[#3BB34A]"
|
||||
style={{ backgroundColor: 'rgba(59,179,74,0.20)' }}
|
||||
>
|
||||
<span className="w-2 h-2 rounded-full bg-[#3BB34A] animate-pulse-dot" />
|
||||
<span className="font-[family-name:var(--font-raleway)] font-bold text-[13px] text-[#3BB34A] uppercase">
|
||||
{registrationStatusText(event)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stat Strip (FIX 3: Edition contradiction fixed) */}
|
||||
<div className="relative z-10 w-full bg-black/45 backdrop-blur-[4px]">
|
||||
<div className="max-w-7xl mx-auto px-6 py-7">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{heroStats.map((stat, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`text-center py-2 ${
|
||||
index < heroStats.length - 1 ? 'md:border-r md:border-white/15' : ''
|
||||
}`}
|
||||
>
|
||||
<p className="font-[family-name:var(--font-antonio)] font-bold text-[#F0C000] text-[22px]">
|
||||
{stat.value}
|
||||
</p>
|
||||
<p className="font-[family-name:var(--font-quicksand)] font-medium text-white/60 text-[12px]">
|
||||
{stat.label}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* SECTION 2 — TRUST BAR (FIX 6: Icons restored, location canonicalized) */}
|
||||
<section className="bg-[#006838] py-9">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-6">
|
||||
{trustSignals.map((signal, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex flex-col items-center text-center py-2 ${
|
||||
index < trustSignals.length - 1 ? 'lg:border-r lg:border-white/15' : ''
|
||||
}`}
|
||||
>
|
||||
<signal.icon className="w-[22px] h-[22px] text-[#FFF100] mb-2" strokeWidth={2} />
|
||||
<span className="font-[family-name:var(--font-raleway)] font-semibold text-white text-[13px]">
|
||||
{signal.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FIX 8: NEW RACE DATE ANCHOR SECTION */}
|
||||
<section className="bg-[#111518] py-14">
|
||||
<div className="max-w-5xl mx-auto px-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-0">
|
||||
{/* Column 1 — WHEN */}
|
||||
<div className="text-center md:text-left md:border-r md:border-white/10 md:pr-8">
|
||||
<span className="font-[family-name:var(--font-raleway)] font-semibold text-[11px] text-[#3BB34A] uppercase tracking-[0.14em]">
|
||||
Race Day
|
||||
</span>
|
||||
<div className="w-8 h-[2px] bg-[#F0C000] mt-2 mb-3 mx-auto md:mx-0" />
|
||||
<p className="font-[family-name:var(--font-antonio)] font-bold text-white text-[32px] md:text-[40px] leading-tight">
|
||||
{date ? date.toUpperCase() : 'DATE TO BE ANNOUNCED'}
|
||||
</p>
|
||||
<p className="font-[family-name:var(--font-quicksand)] font-medium text-[#687279] text-[14px] mt-2">
|
||||
{label ? `${label} · ` : ''}Annual event
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Column 2 — WHERE */}
|
||||
<div className="text-center md:text-left md:border-r md:border-white/10 md:px-8">
|
||||
<span className="font-[family-name:var(--font-raleway)] font-semibold text-[11px] text-[#3BB34A] uppercase tracking-[0.14em]">
|
||||
Location
|
||||
</span>
|
||||
<div className="w-8 h-[2px] bg-[#F0C000] mt-2 mb-3 mx-auto md:mx-0" />
|
||||
<p className="font-[family-name:var(--font-antonio)] font-bold text-white text-[32px] md:text-[40px] leading-tight">
|
||||
{(edition?.venue || 'To be announced').toUpperCase()}
|
||||
</p>
|
||||
<p className="font-[family-name:var(--font-quicksand)] font-medium text-[#687279] text-[14px] mt-2">
|
||||
{[edition?.county ? `${edition.county}, Kenya` : 'Kenya', featured && featuredStart ? `${featured.distance_label} start: ${featuredStart}` : null].filter(Boolean).join(' · ')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Column 3 — START TIMES (from the race categories) */}
|
||||
<div className="text-center md:text-left md:pl-8">
|
||||
<span className="font-[family-name:var(--font-raleway)] font-semibold text-[11px] text-[#3BB34A] uppercase tracking-[0.14em]">
|
||||
Start Times
|
||||
</span>
|
||||
<div className="w-8 h-[2px] bg-[#F0C000] mt-2 mb-3 mx-auto md:mx-0" />
|
||||
<p className="font-[family-name:var(--font-antonio)] font-bold text-white text-[32px] md:text-[40px] leading-tight">
|
||||
{featuredStart || 'TBA'}
|
||||
</p>
|
||||
<p className="font-[family-name:var(--font-quicksand)] font-medium text-[#687279] text-[14px] mt-2">
|
||||
{categories.filter((c) => c.start_time).slice(0, 4).map((c) => `${c.distance_label} ${formatClock(c.start_time)}`).join(' · ') || 'Start times are published with the race categories.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* SECTION 3 — ENVIRONMENTAL MISSION (FIX 7: Clarified 10,000 vs 10M) */}
|
||||
<section className="bg-white py-24 md:py-32">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[55%_45%] gap-16 lg:gap-20 items-center">
|
||||
{/* Left Column */}
|
||||
<div>
|
||||
{/* Eyebrow */}
|
||||
<span className="font-[family-name:var(--font-raleway)] font-semibold text-[#006838] text-[11px] uppercase tracking-[0.16em]">
|
||||
Our Mission
|
||||
</span>
|
||||
{/* Gold Rule */}
|
||||
<div className="w-10 h-[3px] bg-[#F0C000] mt-3 mb-4" />
|
||||
|
||||
{/* H2 */}
|
||||
<h2 className="font-[family-name:var(--font-raleway)] font-bold text-[#006838] text-[34px] md:text-[48px] leading-[1.15] mb-4">
|
||||
More Than a Finish Line.
|
||||
</h2>
|
||||
|
||||
{/* Body (FIX 7: Updated copy) */}
|
||||
<p className="font-[family-name:var(--font-quicksand)] font-medium text-[#111518] text-[17px] leading-[1.75] mb-8">
|
||||
The 10 Million Trees Marathon exists because of a promise made to Kenya's dryland ecosystems. Every runner, every kilometre, every edition is a contribution to one of East Africa's most ambitious conservation programmes. By 2027, the programme will have planted 10 million trees across the ASAL region — a goal now in its 5th year of active delivery.
|
||||
</p>
|
||||
|
||||
{/* Stat Pills */}
|
||||
<div className="flex flex-wrap gap-3 mb-8">
|
||||
<div className="bg-[#006838] text-white rounded-lg px-6 py-4 flex items-center gap-3">
|
||||
<span className="font-[family-name:var(--font-raleway)] font-bold text-[24px]">10M</span>
|
||||
<span className="font-[family-name:var(--font-quicksand)] font-medium text-[12px] text-white/80">Trees by 2027</span>
|
||||
</div>
|
||||
<div className="bg-[#006838] text-white rounded-lg px-6 py-4 flex items-center gap-3">
|
||||
<span className="font-[family-name:var(--font-raleway)] font-bold text-[24px]">4</span>
|
||||
<span className="font-[family-name:var(--font-quicksand)] font-medium text-[12px] text-white/80">Editions Completed</span>
|
||||
</div>
|
||||
<div className="bg-[#006838] text-white rounded-lg px-6 py-4 flex items-center gap-3">
|
||||
<span className="font-[family-name:var(--font-raleway)] font-bold text-[24px]">ASAL</span>
|
||||
<span className="font-[family-name:var(--font-quicksand)] font-medium text-[12px] text-white/80">Region Impact</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<Link
|
||||
href="/about"
|
||||
className="inline-flex items-center gap-2 h-[52px] px-7 border-2 border-[#006838] text-[#006838] font-[family-name:var(--font-raleway)] font-bold text-[14px] uppercase tracking-[0.08em] rounded-md hover:bg-[#006838] hover:text-white transition-colors duration-200"
|
||||
>
|
||||
Our Conservation Story <ArrowRight size={16} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Right Column (FIX 7: Updated labels) */}
|
||||
<div className="text-center">
|
||||
<p className="font-[family-name:var(--font-antonio)] font-bold text-[#F0C000] text-[80px] md:text-[96px] leading-none">
|
||||
10,000+
|
||||
</p>
|
||||
<p className="font-[family-name:var(--font-raleway)] font-semibold text-[#006838] text-[16px] mt-2 mb-2">
|
||||
Trees planted and GPS-tagged to date
|
||||
</p>
|
||||
<p className="font-[family-name:var(--font-quicksand)] font-medium text-[#687279] text-[13px] mb-4">
|
||||
Goal: 10 Million by 2027 · 4 editions completed · On track
|
||||
</p>
|
||||
<p className="font-[family-name:var(--font-quicksand)] font-medium text-[#687279] text-[14px] leading-[1.65] max-w-[280px] mx-auto mb-4">
|
||||
GPS-tagged planting zones in Lukenya Conservation Area. Documented, verified, reportable.
|
||||
</p>
|
||||
<span className="inline-block bg-[#3BB34A] text-white font-[family-name:var(--font-raleway)] font-bold text-[11px] uppercase px-4 py-2 rounded-full">
|
||||
Verified Programme
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* SECTION 4 — RACE CATEGORIES (FIX 4: Removed TBD pricing, added fallback) */}
|
||||
<section className="bg-[#F9FAFB] py-24 md:py-32">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
{/* Header */}
|
||||
<div className="mb-12">
|
||||
<span className="font-[family-name:var(--font-raleway)] font-semibold text-[#006838] text-[11px] uppercase tracking-[0.16em]">
|
||||
Race Categories
|
||||
</span>
|
||||
<div className="w-10 h-[3px] bg-[#F0C000] mt-3 mb-4" />
|
||||
<h2 className="font-[family-name:var(--font-raleway)] font-bold text-[#111518] text-[36px] md:text-[44px] leading-tight mb-3">
|
||||
Find Your Distance.
|
||||
</h2>
|
||||
<p className="font-[family-name:var(--font-quicksand)] font-medium text-[#687279] text-[17px]">
|
||||
{categories.length ? `${countWord(categories.length)} ${categories.length === 1 ? 'category' : 'categories'}. ` : ''}Every level of runner and rider. One extraordinary finish line.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Category Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5 mb-10">
|
||||
{categories.length === 0 && (
|
||||
<p className="md:col-span-2 lg:col-span-4 font-[family-name:var(--font-quicksand)] font-medium text-[#687279] text-[15px]">
|
||||
Race categories for the next edition will be published here soon.
|
||||
</p>
|
||||
)}
|
||||
{categories.map((category) => {
|
||||
const price = formatMoney(displayPrice(category, 'resident'), event.currency)
|
||||
const isCycling = category.discipline === 'Cycling'
|
||||
return (
|
||||
<div
|
||||
key={category.name}
|
||||
className={`relative bg-white rounded-xl shadow-[0_2px_20px_rgba(0,0,0,0.07)] p-7 flex flex-col ${
|
||||
category.is_featured ? 'border-l-4 border-l-[#006838]' : ''
|
||||
}`}
|
||||
>
|
||||
{/* Prize Badge */}
|
||||
{category.prize_eligible && (
|
||||
<span className="absolute top-4 right-4 bg-[#F0C000] text-[#714508] font-[family-name:var(--font-raleway)] font-bold text-[10px] uppercase px-2 py-1 rounded">
|
||||
Prize Money
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Cycling Icon */}
|
||||
{isCycling && !category.prize_eligible && (
|
||||
<Bike className="absolute top-4 right-4 w-5 h-5 text-[#3BB34A]" />
|
||||
)}
|
||||
|
||||
{/* Distance */}
|
||||
<p className="font-[family-name:var(--font-antonio)] font-bold text-[#006838] text-[40px] leading-none">
|
||||
{category.distance_label}
|
||||
</p>
|
||||
|
||||
{/* Name */}
|
||||
<h3 className="font-[family-name:var(--font-raleway)] font-bold text-[#111518] text-[16px] mt-2">
|
||||
{category.category_name}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
<p className="font-[family-name:var(--font-quicksand)] font-medium text-[#687279] text-[13px] leading-relaxed mt-2 mb-3 flex-1">
|
||||
{category.short_description || category.eligibility || category.inclusions.join(' · ')}
|
||||
</p>
|
||||
|
||||
{/* Featured race: inline stat */}
|
||||
{category.is_featured && category.prize_eligible && (
|
||||
<p className="font-[family-name:var(--font-quicksand)] font-medium text-[#687279] text-[12px] mb-3">
|
||||
🏆 Prize money available · Certified course · Chip timing
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Price */}
|
||||
<div className="mb-3">
|
||||
{category.sold_out ? (
|
||||
<span className="inline-block bg-[#E5E7EB] text-[#111518] font-[family-name:var(--font-raleway)] font-bold text-[12px] uppercase px-2.5 py-1 rounded">
|
||||
Sold out
|
||||
</span>
|
||||
) : price ? (
|
||||
<>
|
||||
<p className="font-[family-name:var(--font-antonio)] font-bold text-[#111518] text-[22px] leading-none">{price}</p>
|
||||
<p className="font-[family-name:var(--font-quicksand)] font-medium text-[#687279] text-[12px] mt-1">
|
||||
{category.prices.early_bird_active ? 'Early-bird rate for Kenyan residents' : 'Kenyan resident rate'}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<span className="inline-block bg-[#FFF100] text-[#714508] font-[family-name:var(--font-raleway)] font-bold text-[12px] uppercase px-2.5 py-1 rounded">
|
||||
Pricing announced soon
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<Link
|
||||
href={isOpen && !category.sold_out ? `/registration?race=${encodeURIComponent(category.name)}#registration-form` : '/registration#waitlist'}
|
||||
className="font-[family-name:var(--font-raleway)] font-semibold text-[#006838] text-[13px] hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
{isOpen && !category.sold_out ? 'Enter this race' : 'Join waitlist'} <ArrowRight size={14} />
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* CTAs */}
|
||||
<div className="flex flex-col sm:flex-row justify-center gap-4">
|
||||
<Link
|
||||
href="/race-info"
|
||||
className="h-[52px] px-7 border-2 border-[#006838] text-[#006838] font-[family-name:var(--font-raleway)] font-bold text-[14px] uppercase tracking-[0.08em] rounded-md hover:bg-[#006838] hover:text-white transition-colors duration-200 flex items-center justify-center"
|
||||
>
|
||||
View All Race Info
|
||||
</Link>
|
||||
<Link
|
||||
href="/registration"
|
||||
className="h-[52px] px-7 bg-[#FFF100] text-[#714508] font-[family-name:var(--font-raleway)] font-bold text-[14px] uppercase tracking-[0.08em] rounded-md hover:bg-[#3BB34A] hover:text-white transition-colors duration-200 flex items-center justify-center"
|
||||
>
|
||||
Register Now
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* SECTION 5 — PRIZE MONEY TEASER (FIX 5: Replaced TBD with competitive framing) */}
|
||||
<section className="bg-[#006838] py-24 md:py-32">
|
||||
<div className="max-w-5xl mx-auto px-6">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-12">
|
||||
<span className="font-[family-name:var(--font-raleway)] font-semibold text-[#FFF100] text-[11px] uppercase tracking-[0.16em]">
|
||||
Prize Money
|
||||
</span>
|
||||
<div className="w-10 h-[3px] bg-[#FFF100] mt-3 mb-4 mx-auto" />
|
||||
<h2 className="font-[family-name:var(--font-raleway)] font-bold text-white text-[36px] md:text-[44px] leading-tight">
|
||||
Compete for Real Rewards.
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Prize Cards: amounts come from the edition's prize rules */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
{prizeCards.map((card) => (
|
||||
<div key={card.title} className="bg-white rounded-xl p-8 shadow-[0_2px_20px_rgba(0,0,0,0.07)] text-center">
|
||||
<card.icon className="w-7 h-7 text-[#F0C000] mx-auto mb-3" />
|
||||
<p className="font-[family-name:var(--font-raleway)] font-semibold text-[#687279] text-[13px] mb-2">
|
||||
{card.title}
|
||||
</p>
|
||||
<p className="font-[family-name:var(--font-antonio)] font-bold text-[#F0C000] text-[28px]">
|
||||
{formatMoney(card.amount, prizes?.currency || event.currency) || 'PRIZE MONEY AVAILABLE'}
|
||||
</p>
|
||||
{!card.amount && (
|
||||
<p className="font-[family-name:var(--font-quicksand)] font-medium text-[#687279] text-[13px] mt-2">
|
||||
Amounts published before entries open
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Prize structure note (FIX 5) */}
|
||||
<p className="text-center font-[family-name:var(--font-quicksand)] font-medium text-white/65 text-[14px] mb-10">
|
||||
Prize structure includes Men, Women, Wheelchair, and Corporate Relay categories. Full breakdown published on the Prize Money page.
|
||||
</p>
|
||||
|
||||
{/* CTA */}
|
||||
<div className="text-center">
|
||||
<Link
|
||||
href="/prize-money"
|
||||
className="inline-flex items-center gap-2 h-[52px] px-7 border border-white text-white font-[family-name:var(--font-raleway)] font-bold text-[14px] uppercase tracking-[0.08em] rounded-md hover:bg-white hover:text-[#111518] transition-colors duration-200"
|
||||
>
|
||||
View Full Prize Structure <ArrowRight size={16} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* SECTION 6 — PAST EDITIONS / SOCIAL PROOF (FIX 3: Updated headline) */}
|
||||
<section className="bg-white py-24 md:py-32">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
{/* Header */}
|
||||
<div className="mb-10">
|
||||
<span className="font-[family-name:var(--font-raleway)] font-semibold text-[#006838] text-[11px] uppercase tracking-[0.16em]">
|
||||
Past Editions
|
||||
</span>
|
||||
<div className="w-10 h-[3px] bg-[#F0C000] mt-3 mb-4" />
|
||||
<h2 className="font-[family-name:var(--font-raleway)] font-bold text-[#111518] text-[36px] md:text-[44px] leading-tight">
|
||||
300+ Runners. Across 4 Editions. One Growing Legacy.
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Photo Strip */}
|
||||
<div className="flex gap-4 overflow-x-auto pb-4 -mx-6 px-6 snap-x snap-mandatory scroll-smooth">
|
||||
{galleryImages.map((src, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex-shrink-0 w-[340px] h-[220px] rounded-[10px] overflow-hidden snap-start relative group"
|
||||
>
|
||||
<Image
|
||||
src={src}
|
||||
alt={`Marathon gallery image ${index + 1}`}
|
||||
fill
|
||||
className="object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-[#006838]/0 group-hover:bg-[#006838]/25 transition-colors duration-300" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Testimonials (ADDITIONAL FIX 2) */}
|
||||
<div className="flex flex-col md:flex-row justify-center gap-6 mt-12 mb-12">
|
||||
{testimonials.map((testimonial, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white rounded-xl shadow-[0_2px_20px_rgba(0,0,0,0.07)] p-6 max-w-[380px]"
|
||||
>
|
||||
<span className="font-[family-name:var(--font-antonio)] font-bold text-[#F0C000] text-[48px] leading-[0.8] block mb-2">
|
||||
“
|
||||
</span>
|
||||
<p className="font-[family-name:var(--font-quicksand)] font-medium text-[#111518] text-[15px] leading-[1.65] italic mb-3">
|
||||
{testimonial.quote}
|
||||
</p>
|
||||
<p className="font-[family-name:var(--font-raleway)] font-bold text-[#006838] text-[13px]">
|
||||
— {testimonial.name}
|
||||
</p>
|
||||
<p className="font-[family-name:var(--font-quicksand)] font-medium text-[#687279] text-[12px]">
|
||||
{testimonial.category} · {testimonial.edition}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Stat */}
|
||||
<div className="text-center mb-10">
|
||||
<p className="font-[family-name:var(--font-antonio)] font-bold text-[#006838] text-[72px] md:text-[88px] leading-none">
|
||||
300+
|
||||
</p>
|
||||
<p className="font-[family-name:var(--font-raleway)] font-semibold text-[#687279] text-[18px] mt-2">
|
||||
runners have crossed this finish line
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<div className="text-center">
|
||||
<Link
|
||||
href="/results"
|
||||
className="inline-flex items-center gap-2 h-[52px] px-7 border-2 border-[#006838] text-[#006838] font-[family-name:var(--font-raleway)] font-bold text-[14px] uppercase tracking-[0.08em] rounded-md hover:bg-[#006838] hover:text-white transition-colors duration-200"
|
||||
>
|
||||
View Results Archive
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* SECTION 7 — SPONSORS */}
|
||||
<section className="bg-[#F9FAFB] py-24 md:py-32">
|
||||
<div className="max-w-5xl mx-auto px-6 text-center">
|
||||
{/* Header */}
|
||||
<span className="font-[family-name:var(--font-raleway)] font-semibold text-[#006838] text-[11px] uppercase tracking-[0.16em]">
|
||||
Our Sponsors
|
||||
</span>
|
||||
<div className="w-10 h-[3px] bg-[#F0C000] mt-3 mb-4 mx-auto" />
|
||||
<h2 className="font-[family-name:var(--font-raleway)] font-bold text-[#111518] text-[32px] md:text-[40px] leading-tight mb-10">
|
||||
Brands That Run With Purpose.
|
||||
</h2>
|
||||
|
||||
{/* Sponsor Logos */}
|
||||
<div className="flex flex-wrap justify-center gap-10 mb-8">
|
||||
{sponsors.map((sponsor, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="relative w-auto h-16 grayscale hover:grayscale-0 transition-all duration-300"
|
||||
>
|
||||
<Image
|
||||
src={sponsor.url}
|
||||
alt={sponsor.name}
|
||||
width={120}
|
||||
height={64}
|
||||
className="h-16 w-auto object-contain"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="font-[family-name:var(--font-quicksand)] font-medium text-[#687279] text-[14px] mb-10">
|
||||
Including our Environmental Partner tier — the only ESG-verified sponsorship in Kenyan running.
|
||||
</p>
|
||||
|
||||
{/* CTA */}
|
||||
<Link
|
||||
href="/sponsors"
|
||||
className="inline-flex items-center gap-2 h-[52px] px-7 bg-[#FFF100] text-[#714508] font-[family-name:var(--font-raleway)] font-bold text-[14px] uppercase tracking-[0.08em] rounded-md hover:bg-[#3BB34A] hover:text-white transition-colors duration-200"
|
||||
>
|
||||
Become a Partner <ArrowRight size={16} />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* SECTION 8 — REGISTRATION CTA BAND */}
|
||||
<section className="bg-[#006838] py-24">
|
||||
<div className="max-w-4xl mx-auto px-6 text-center">
|
||||
<h2 className="font-[family-name:var(--font-antonio)] font-bold text-[#FFF100] text-[40px] md:text-[64px] uppercase leading-none mb-4">
|
||||
{edition?.trees_commitment_per_entry ? 'Your Entry Plants a Tree.' : 'Run With Purpose.'}
|
||||
</h2>
|
||||
<p className="font-[family-name:var(--font-quicksand)] font-medium text-white/80 text-[19px] mb-8">
|
||||
{isOpen ? `Register for the ${label || 'next edition'}. Run for Kenya. Entries open now.` : `Entries for the ${label || 'next edition'} open soon. Join the waitlist to hear first.`}
|
||||
</p>
|
||||
<Link
|
||||
href="/registration"
|
||||
className="inline-flex items-center justify-center h-14 px-10 bg-[#FFF100] text-[#714508] font-[family-name:var(--font-raleway)] font-bold text-[15px] uppercase tracking-[0.08em] rounded-md hover:bg-[#3BB34A] hover:text-white transition-colors duration-200"
|
||||
>
|
||||
Register Now
|
||||
</Link>
|
||||
<p className="font-[family-name:var(--font-quicksand)] font-medium text-white/50 text-[13px] mt-4">
|
||||
{featured?.prices.early_bird_active ? 'Early-bird pricing available. Secure your place before rates increase.' : 'Pay by M-PESA or card.'}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Navigation } from '@/components/navigation'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { PaymentStatus } from './payment-status'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Payment | 10 Million Trees Marathon',
|
||||
robots: { index: false },
|
||||
}
|
||||
|
||||
export default function PaymentReturnPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F9FAFB]">
|
||||
<Navigation />
|
||||
<section className="bg-[#006838] pt-32 pb-24 px-4">
|
||||
<PaymentStatus />
|
||||
</section>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { CheckCircle2, Loader2, XCircle, Clock } from 'lucide-react'
|
||||
import { formatMoney } from '@/lib/event-types'
|
||||
|
||||
interface StatusResponse {
|
||||
status?: string
|
||||
registration?: string
|
||||
registration_status?: string
|
||||
amount?: number
|
||||
currency?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
type View = 'checking' | 'paid' | 'failed' | 'waiting' | 'error'
|
||||
|
||||
const POLL_MS = 3000
|
||||
const MAX_POLLS = 20 // about a minute; M-PESA confirmations usually land well inside this
|
||||
|
||||
export function PaymentStatus() {
|
||||
const [view, setView] = useState<View>('checking')
|
||||
const [data, setData] = useState<StatusResponse>({})
|
||||
const [reference, setReference] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
// Paystack sends the runner back with ?reference=... (and ?trxref=... as a copy)
|
||||
const ref = params.get('reference') || params.get('trxref')
|
||||
setReference(ref)
|
||||
if (!ref) {
|
||||
setView('error')
|
||||
setData({ error: 'This page needs a payment reference. Open it from the payment page, or check your email.' })
|
||||
return
|
||||
}
|
||||
|
||||
let polls = 0
|
||||
let timer: ReturnType<typeof setTimeout>
|
||||
let stopped = false
|
||||
|
||||
const check = async () => {
|
||||
polls += 1
|
||||
try {
|
||||
const res = await fetch(`/api/payment/status?reference=${encodeURIComponent(ref)}`, { cache: 'no-store' })
|
||||
const body = (await res.json()) as StatusResponse
|
||||
if (stopped) return
|
||||
if (!res.ok) throw new Error(body.error || 'Could not check the payment.')
|
||||
setData(body)
|
||||
const status = (body.status || '').toLowerCase()
|
||||
if (status === 'paid' || status === 'reconciled') return setView('paid')
|
||||
if (status === 'failed' || status === 'abandoned') return setView('failed')
|
||||
if (polls >= MAX_POLLS) return setView('waiting')
|
||||
setView('checking')
|
||||
} catch (err) {
|
||||
if (stopped) return
|
||||
if (polls >= MAX_POLLS) {
|
||||
setData({ error: (err as Error).message })
|
||||
return setView('error')
|
||||
}
|
||||
}
|
||||
timer = setTimeout(check, POLL_MS)
|
||||
}
|
||||
check()
|
||||
return () => {
|
||||
stopped = true
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const amount = formatMoney(data.amount ?? null, data.currency || 'KES')
|
||||
|
||||
return (
|
||||
<div className="max-w-[520px] mx-auto bg-white rounded-xl shadow-[0_2px_16px_rgba(0,0,0,0.08)] p-8 md:p-12 text-center" aria-live="polite">
|
||||
{view === 'checking' && (
|
||||
<>
|
||||
<Loader2 className="w-12 h-12 text-[#006838] mx-auto mb-4 animate-spin" />
|
||||
<h1 className="font-heading font-bold text-[#111518] text-[26px] mb-3">Confirming your payment</h1>
|
||||
<p className="font-sans text-[#687279] text-[15px]">
|
||||
We're checking with the payment provider. If you paid by M-PESA, this can take up to a minute. Keep this page open.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{view === 'paid' && (
|
||||
<>
|
||||
<CheckCircle2 className="w-14 h-14 text-[#3BB34A] mx-auto mb-4" />
|
||||
<h1 className="font-heading font-bold text-[#006838] text-[28px] mb-3">You're in.</h1>
|
||||
<p className="font-sans text-[#111518] text-[15px] mb-2">
|
||||
Payment received{amount ? ` (${amount})` : ''}. Your entry{data.registration ? ` ${data.registration}` : ''} is confirmed.
|
||||
</p>
|
||||
<p className="font-sans text-[#687279] text-[14px] mb-8">
|
||||
A confirmation email is on its way with your private My Race link, where you can see your bib and race pack status.
|
||||
</p>
|
||||
<Link href="/race-info" className="inline-flex items-center justify-center h-12 px-6 bg-[#006838] text-white font-heading font-bold text-sm rounded-lg hover:bg-[#005030] transition-colors">
|
||||
Read the race information
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
{view === 'failed' && (
|
||||
<>
|
||||
<XCircle className="w-14 h-14 text-[#c0392b] mx-auto mb-4" />
|
||||
<h1 className="font-heading font-bold text-[#111518] text-[26px] mb-3">Payment not completed</h1>
|
||||
<p className="font-sans text-[#687279] text-[15px] mb-8">
|
||||
No money was taken for this attempt. Your details are saved{data.registration ? ` under entry ${data.registration}` : ''}, so you can try again without filling the form.
|
||||
</p>
|
||||
<Link href="/registration#registration-form" className="inline-flex items-center justify-center h-12 px-6 bg-[#FFF100] text-[#714508] font-heading font-bold text-sm rounded-lg hover:bg-[#3BB34A] hover:text-white transition-colors">
|
||||
Try again
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
{view === 'waiting' && (
|
||||
<>
|
||||
<Clock className="w-14 h-14 text-[#F0C000] mx-auto mb-4" />
|
||||
<h1 className="font-heading font-bold text-[#111518] text-[26px] mb-3">Still waiting for confirmation</h1>
|
||||
<p className="font-sans text-[#687279] text-[15px] mb-8">
|
||||
The payment provider hasn't confirmed your payment yet. If money left your account, don't pay again: your entry is confirmed automatically when the confirmation arrives, and we'll email you.
|
||||
</p>
|
||||
<button onClick={() => window.location.reload()} className="inline-flex items-center justify-center h-12 px-6 border-2 border-[#006838] text-[#006838] font-heading font-bold text-sm rounded-lg hover:bg-[#006838] hover:text-white transition-colors">
|
||||
Check again
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{view === 'error' && (
|
||||
<>
|
||||
<XCircle className="w-14 h-14 text-[#687279] mx-auto mb-4" />
|
||||
<h1 className="font-heading font-bold text-[#111518] text-[26px] mb-3">We couldn't check this payment</h1>
|
||||
<p className="font-sans text-[#687279] text-[15px] mb-2">{data.error}</p>
|
||||
{reference && <p className="font-sans text-[#687279] text-[13px] mb-6">Payment reference: {reference}</p>}
|
||||
<a href="mailto:marathon@lukenyauniversity.ac.ke" className="font-heading font-semibold text-[#006838] text-sm underline">
|
||||
Email us with your payment reference
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
import Link from 'next/link'
|
||||
import { Trophy, ShieldCheck, Accessibility, Users, Award } from 'lucide-react'
|
||||
import { Navigation } from '@/components/navigation'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { getPublicEvent, getPublicPrizes } from '@/lib/lukenya-api'
|
||||
import { findPrize, formatMoney, ordinal, type PrizeCategory, type PrizeRow } from '@/lib/event-types'
|
||||
|
||||
export const revalidate = 60
|
||||
|
||||
type PrizeLine = { place: string; amount: string; highlight?: boolean }
|
||||
|
||||
const PLACE = (n: number) => ordinal(n) || String(n)
|
||||
|
||||
// Men and women side by side, one row per finishing position.
|
||||
function rowsFor(category: PrizeCategory, currency: string): { men: PrizeLine[]; women: PrizeLine[] } {
|
||||
const men = category.divisions.find((d) => d.division === 'Men')?.places || []
|
||||
const women = category.divisions.find((d) => d.division === 'Women')?.places || []
|
||||
const positions = Array.from(new Set([...men, ...women].map((p) => p.position))).sort((x, y) => x - y)
|
||||
const fmt = (rows: PrizeRow[], pos: number) => {
|
||||
const row = rows.find((r) => r.position === pos)
|
||||
return row ? (formatMoney(row.amount, currency)?.replace(`${currency} `, '') || '—') : '—'
|
||||
}
|
||||
return {
|
||||
men: positions.map((pos) => ({ place: PLACE(pos), amount: fmt(men, pos), highlight: pos === 1 })),
|
||||
women: positions.map((pos) => ({ place: PLACE(pos), amount: fmt(women, pos), highlight: pos === 1 })),
|
||||
}
|
||||
}
|
||||
|
||||
const SPECIAL = [
|
||||
{ division: 'Wheelchair', icon: Accessibility, title: 'Wheelchair Category', description: 'Top finishers in the wheelchair division' },
|
||||
{ division: 'Corporate Relay', icon: Users, title: 'Corporate Relay', description: 'Fastest corporate relay teams' },
|
||||
{ division: 'Age Group', icon: Award, title: 'Age Category Awards', description: 'Top finishers in each age group' },
|
||||
]
|
||||
|
||||
function PrizeTable({
|
||||
title,
|
||||
men,
|
||||
women
|
||||
}: {
|
||||
title: string
|
||||
men: { place: string; amount: string; highlight?: boolean }[]
|
||||
women: { place: string; amount: string; highlight?: boolean }[]
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-[0_2px_16px_rgba(0,0,0,0.08)] overflow-hidden">
|
||||
<div className="bg-[#ffcd05]/10 border-b border-[#ffcd05]/20 px-6 py-4">
|
||||
<h3 className="text-xl font-bold text-[#006838]">{title}</h3>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-[#e5e7eb]">
|
||||
<th className="text-left py-3 font-semibold text-[#111518]">Place</th>
|
||||
<th className="text-left py-3 font-semibold text-[#111518]">Men</th>
|
||||
<th className="text-left py-3 font-semibold text-[#111518]">Women</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{men.map((prize, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className={`border-b border-[#e5e7eb] ${
|
||||
prize.highlight ? 'bg-[#ffcd05]/5' : ''
|
||||
}`}
|
||||
>
|
||||
<td className={`py-3 font-semibold ${prize.highlight ? 'text-[#ffcd05]' : 'text-[#111518]'}`}>
|
||||
{prize.place}
|
||||
</td>
|
||||
<td className={`py-3 font-extrabold text-lg ${
|
||||
prize.highlight ? 'text-[#ffcd05]' : index < 3 ? 'text-[#3bb34a]' : 'text-[#111518]'
|
||||
}`}>
|
||||
{prize.amount}
|
||||
</td>
|
||||
<td className={`py-3 font-extrabold text-lg ${
|
||||
women[index]?.highlight ? 'text-[#ffcd05]' : index < 3 ? 'text-[#3bb34a]' : 'text-[#111518]'
|
||||
}`}>
|
||||
{women[index]?.amount}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CompactPrizeTable({
|
||||
title,
|
||||
men,
|
||||
women
|
||||
}: {
|
||||
title: string
|
||||
men: { place: string; amount: string }[]
|
||||
women: { place: string; amount: string }[]
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-[0_2px_16px_rgba(0,0,0,0.08)] overflow-hidden">
|
||||
<div className="bg-[#F5F7FC] border-b border-[#e5e7eb] px-6 py-3">
|
||||
<h3 className="text-lg font-bold text-[#006838]">{title}</h3>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-[#e5e7eb]">
|
||||
<th className="text-left py-2 font-semibold text-[#111518]">Place</th>
|
||||
<th className="text-left py-2 font-semibold text-[#111518]">Men</th>
|
||||
<th className="text-left py-2 font-semibold text-[#111518]">Women</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{men.map((prize, index) => (
|
||||
<tr key={index} className="border-b border-[#e5e7eb] last:border-0">
|
||||
<td className="py-2 font-semibold text-[#111518]">{prize.place}</td>
|
||||
<td className="py-2 font-bold text-[#3bb34a]">{prize.amount}</td>
|
||||
<td className="py-2 font-bold text-[#3bb34a]">{women[index]?.amount}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default async function PrizeMoneyPage() {
|
||||
const [event, prizes] = await Promise.all([getPublicEvent(), getPublicPrizes()])
|
||||
const currency = prizes?.currency || event.currency
|
||||
// Races with Men/Women prize rules, in the order the backend sends them (featured first).
|
||||
const raceTables = (prizes?.categories || []).filter((c) =>
|
||||
c.divisions.some((d) => d.division === 'Men' || d.division === 'Women'),
|
||||
)
|
||||
const [mainRace, ...otherRaces] = raceTables
|
||||
const specials = SPECIAL.map((s) => ({ ...s, amount: findPrize(prizes, s.division, 1) }))
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Navigation />
|
||||
|
||||
{/* Page Hero */}
|
||||
<section className="bg-[#006838] pt-20">
|
||||
<div className="max-w-5xl mx-auto px-4 text-center py-16 md:py-20">
|
||||
<span className="text-[#ffcd05] font-semibold text-xs uppercase tracking-[0.12em] mb-4 block">
|
||||
Compete for Real Rewards
|
||||
</span>
|
||||
<h1 className="text-3xl md:text-4xl lg:text-5xl font-extrabold text-white mb-4">
|
||||
Prize Money
|
||||
</h1>
|
||||
<p className="text-white/80 text-lg max-w-2xl mx-auto">
|
||||
{raceTables.length ? `Structured prize money across ${raceTables.length} ${raceTables.length === 1 ? 'race' : 'races'}, paid on race day.` : 'Structured prize money, paid on race day.'}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Main race */}
|
||||
<section className="bg-white py-16 md:py-24">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<Trophy className="w-12 h-12 text-[#ffcd05] mx-auto mb-4" />
|
||||
<span className="text-[#006838] font-semibold text-xs uppercase tracking-[0.12em] mb-2 block">
|
||||
Top Prize Category
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-[#006838]">
|
||||
{mainRace ? `${mainRace.category_name}, ${mainRace.distance_label}` : 'Prize structure'}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{mainRace ? (
|
||||
<PrizeTable title={`${mainRace.category_name} prize structure (${currency})`} {...rowsFor(mainRace, currency)} />
|
||||
) : (
|
||||
<p className="text-center text-[#6b7280] text-lg">
|
||||
Prize amounts for each race are published here before entries open.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{otherRaces.length > 0 && (
|
||||
<section className="bg-[#F5F7FC] py-16 md:py-24">
|
||||
<div className="max-w-5xl mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-[#006838]">Other races</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{otherRaces.map((race) => (
|
||||
<CompactPrizeTable
|
||||
key={race.race_category}
|
||||
title={`${race.category_name} (${race.distance_label})`}
|
||||
{...rowsFor(race, currency)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Special Categories */}
|
||||
<section className="bg-[#006838] py-16 md:py-24">
|
||||
<div className="max-w-5xl mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<span className="text-[#ffcd05] font-semibold text-xs uppercase tracking-[0.12em] mb-2 block">
|
||||
Special Categories
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-white">
|
||||
Additional Prize Categories
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{specials.map((category, index) => (
|
||||
<div key={index} className="bg-white rounded-xl p-6 text-center">
|
||||
<category.icon className="w-10 h-10 text-[#006838] mx-auto mb-4" />
|
||||
<h3 className="font-bold text-[#111518] mb-2">{category.title}</h3>
|
||||
<p className="text-[#ffcd05] font-extrabold text-2xl mb-2">{formatMoney(category.amount, currency) || 'To be announced'}</p>
|
||||
<p className="text-[#6b7280] text-sm">{category.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Prize Payment Info */}
|
||||
<section className="bg-[#F5F7FC] py-12">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<div className="flex flex-col md:flex-row items-center justify-center gap-4 text-center md:text-left">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldCheck className="w-5 h-5 text-[#006838]" />
|
||||
<span className="text-[#111518] font-semibold text-sm">Sporthive Certified Results</span>
|
||||
</div>
|
||||
<span className="hidden md:block text-[#e5e7eb]">|</span>
|
||||
<p className="text-[#6b7280] text-sm">
|
||||
Prize money paid on race day. Winners must present valid ID. All prizes subject to race rules.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="bg-[#006838] py-16">
|
||||
<div className="max-w-3xl mx-auto px-4 text-center">
|
||||
<h2 className="text-3xl md:text-4xl font-extrabold text-white mb-4">
|
||||
Ready to Compete?
|
||||
</h2>
|
||||
<p className="text-white/80 text-lg mb-8">
|
||||
Register now and compete for prize money at Kenya's environmental marathon.
|
||||
</p>
|
||||
<Link
|
||||
href="/registration"
|
||||
className="inline-block bg-[#ffcd05] text-[#111518] font-bold text-sm px-10 py-4 rounded-lg hover:bg-[#c4930f] transition-colors duration-200"
|
||||
>
|
||||
REGISTER NOW
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
import Link from 'next/link'
|
||||
import { Clock, MapPin, Users, ChevronDown, Route, Droplets, Award, ShieldCheck } from 'lucide-react'
|
||||
import { Navigation } from '@/components/navigation'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { getPublicEvent } from '@/lib/lukenya-api'
|
||||
import {
|
||||
displayPrice,
|
||||
formatClock,
|
||||
formatCutoff,
|
||||
formatMoney,
|
||||
type PublicCategory,
|
||||
} from '@/lib/event-types'
|
||||
|
||||
export const revalidate = 60
|
||||
|
||||
function startTimesAnswer(categories: PublicCategory[]): string {
|
||||
const timed = categories.filter((c) => c.start_time)
|
||||
if (!timed.length) return 'Start times are published with the race categories closer to race day.'
|
||||
return timed.map((c) => `${c.category_name} (${c.distance_label}): ${formatClock(c.start_time)}`).join('. ') + '.'
|
||||
}
|
||||
|
||||
function ageAnswer(categories: PublicCategory[]): string {
|
||||
const parts = categories.map((c) => {
|
||||
if (c.min_age) return `${c.category_name}: ${c.min_age}+`
|
||||
if (c.eligibility) return `${c.category_name}: ${c.eligibility}`
|
||||
return null
|
||||
}).filter(Boolean)
|
||||
if (!parts.length) return 'Age limits are listed with each race category. Runners under 18 need a parent or guardian to consent.'
|
||||
return parts.join('. ') + '. Runners under 18 need a parent or guardian to consent.'
|
||||
}
|
||||
|
||||
const routeHighlights = [
|
||||
{ icon: Route, title: 'Course Type', description: 'Mixed terrain: tarmac and scenic trails' },
|
||||
{ icon: Droplets, title: 'Water Stations', description: 'Every 2.5km with hydration and first aid' },
|
||||
{ icon: Award, title: 'Timing', description: 'Sporthive certified chip timing' },
|
||||
]
|
||||
|
||||
const staticFaqs = [
|
||||
{
|
||||
question: 'Is parking available?',
|
||||
answer: 'Yes, free parking is available at the venue. Follow the signage to the designated parking areas. Arrive early for the best spots.'
|
||||
},
|
||||
{
|
||||
question: 'Where do I collect my race pack?',
|
||||
answer: 'Race packs can be collected at the Expo on the day before the race (10 AM - 6 PM) or on race morning from 4:30 AM at the registration tent.'
|
||||
},
|
||||
{
|
||||
question: 'Is bag drop available?',
|
||||
answer: 'Yes, secure bag drop is available near the start line. Please label your bag with your bib number. Bags can be collected after you finish.'
|
||||
},
|
||||
{
|
||||
question: 'What is included in my entry fee?',
|
||||
answer: 'Race bib with chip timing, finisher medal, event t-shirt, access to water stations and post-race refreshments. Inclusions for each race are listed on the registration page.'
|
||||
},
|
||||
{
|
||||
question: 'Can I transfer my entry to someone else?',
|
||||
answer: 'Entry transfers are allowed up to 7 days before the race for a small admin fee. Contact info@lukenyamarathon.co.ke to arrange a transfer.'
|
||||
},
|
||||
{
|
||||
question: 'What happens if it rains?',
|
||||
answer: 'The race proceeds rain or shine. Kenya\'s climate is generally favorable, but come prepared with appropriate gear.'
|
||||
},
|
||||
]
|
||||
|
||||
export default async function RaceInfoPage() {
|
||||
const event = await getPublicEvent()
|
||||
const categories = event.categories
|
||||
const edition = event.edition
|
||||
const featured = categories.find((c) => c.is_featured) || categories[0]
|
||||
const featuredStart = featured ? formatClock(featured.start_time) : null
|
||||
const routeItems = [
|
||||
{ icon: MapPin, title: 'Start/Finish', description: edition?.venue || 'Venue to be announced' },
|
||||
...routeHighlights,
|
||||
]
|
||||
const faqs = [
|
||||
{ question: 'What time does the race start?', answer: startTimesAnswer(categories) },
|
||||
staticFaqs[0],
|
||||
staticFaqs[1],
|
||||
staticFaqs[2],
|
||||
{ question: 'Are there age requirements?', answer: ageAnswer(categories) },
|
||||
...staticFaqs.slice(3),
|
||||
]
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Navigation />
|
||||
|
||||
{/* Page Hero */}
|
||||
<section className="bg-[#006838] pt-20">
|
||||
<div className="max-w-5xl mx-auto px-4 text-center py-16 md:py-20">
|
||||
<span className="text-[#006838] bg-white/10 backdrop-blur font-semibold text-xs uppercase tracking-[0.12em] px-4 py-2 rounded-full inline-block mb-4">
|
||||
Everything You Need to Know
|
||||
</span>
|
||||
<h1 className="text-3xl md:text-4xl lg:text-5xl font-extrabold text-white mb-4">
|
||||
Race Information
|
||||
</h1>
|
||||
<p className="text-white/80 text-lg max-w-2xl mx-auto">
|
||||
Categories, route details, timing, and everything you need for race day{edition?.venue ? ` at ${edition.venue}` : ''}.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Quick Info Bar */}
|
||||
<section className="bg-[#005030] py-6">
|
||||
<div className="max-w-5xl mx-auto px-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
<div className="text-center">
|
||||
<Clock className="w-6 h-6 text-[#ffcd05] mx-auto mb-2" />
|
||||
<p className="text-white font-bold">{featuredStart ? `${featuredStart} Start` : 'Start time TBA'}</p>
|
||||
<p className="text-white/60 text-sm">{featured?.category_name || 'First race'}</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<MapPin className="w-6 h-6 text-[#ffcd05] mx-auto mb-2" />
|
||||
<p className="text-white font-bold">{edition?.venue || 'Venue TBA'}</p>
|
||||
<p className="text-white/60 text-sm">{edition?.county || 'Kenya'}</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<Users className="w-6 h-6 text-[#ffcd05] mx-auto mb-2" />
|
||||
<p className="text-white font-bold">{categories.length || 'All'} Categories</p>
|
||||
<p className="text-white/60 text-sm">All abilities</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<ShieldCheck className="w-6 h-6 text-[#ffcd05] mx-auto mb-2" />
|
||||
<p className="text-white font-bold">Sporthive Timing</p>
|
||||
<p className="text-white/60 text-sm">Certified results</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Race Categories */}
|
||||
<section className="bg-white py-16 md:py-24">
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<span className="text-[#006838] font-semibold text-xs uppercase tracking-[0.12em] mb-2 block">
|
||||
Race Categories
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-[#006838]">
|
||||
Choose Your Challenge
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[600px]">
|
||||
<thead>
|
||||
<tr className="border-b-2 border-[#e5e7eb]">
|
||||
<th className="text-left py-4 px-4 font-semibold text-[#111518]">Category</th>
|
||||
<th className="text-left py-4 px-4 font-semibold text-[#111518]">Distance</th>
|
||||
<th className="text-left py-4 px-4 font-semibold text-[#111518]">Cut-off Time</th>
|
||||
<th className="text-left py-4 px-4 font-semibold text-[#111518]">Elevation</th>
|
||||
<th className="text-left py-4 px-4 font-semibold text-[#111518]">Entry Fee (Kenyan resident)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{categories.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={5} className="py-6 px-4 text-[#6b7280]">
|
||||
Race categories for the next edition will be published here soon.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{categories.map((category) => (
|
||||
<tr key={category.name} className="border-b border-[#e5e7eb] hover:bg-[#F5F7FC]/50">
|
||||
<td className="py-4 px-4 font-semibold text-[#111518]">
|
||||
{category.category_name}
|
||||
{category.sold_out && <span className="ml-2 text-xs font-bold text-[#c0392b]">Sold out</span>}
|
||||
</td>
|
||||
<td className="py-4 px-4 text-[#006838] font-bold">
|
||||
{category.distance_km ? `${category.distance_km}km` : category.distance_label}
|
||||
</td>
|
||||
<td className="py-4 px-4 text-[#6b7280]">{formatCutoff(category.cutoff_hours) || '—'}</td>
|
||||
<td className="py-4 px-4 text-[#6b7280]">{category.elevation_gain_m ? `+${category.elevation_gain_m}m` : '—'}</td>
|
||||
<td className="py-4 px-4 text-[#3bb34a] font-bold">
|
||||
{formatMoney(displayPrice(category, 'resident'), event.currency) || 'Announced soon'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="text-center mt-8">
|
||||
<Link
|
||||
href="/registration"
|
||||
className="inline-block bg-[#3bb34a] text-white font-semibold text-sm px-8 py-3 rounded-lg hover:bg-[#2a9a3a] transition-colors duration-200"
|
||||
>
|
||||
REGISTER NOW
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Route Information */}
|
||||
<section className="bg-[#F5F7FC] py-16 md:py-24">
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<span className="text-[#006838] font-semibold text-xs uppercase tracking-[0.12em] mb-2 block">
|
||||
The Course
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-[#006838]">
|
||||
Route Information
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Route Map Placeholder */}
|
||||
<div className="bg-white rounded-xl shadow-[0_2px_16px_rgba(0,0,0,0.08)] p-8 mb-12">
|
||||
<div className="aspect-video bg-[#e5e7eb] rounded-lg flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Route className="w-12 h-12 text-[#6b7280] mx-auto mb-2" />
|
||||
<p className="text-[#6b7280]">Course Map Coming Soon</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Route Highlights */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{routeItems.map((item, index) => (
|
||||
<div key={index} className="bg-white rounded-xl p-6 shadow-[0_2px_16px_rgba(0,0,0,0.08)]">
|
||||
<item.icon className="w-8 h-8 text-[#006838] mb-4" />
|
||||
<h3 className="font-bold text-[#111518] mb-2">{item.title}</h3>
|
||||
<p className="text-[#6b7280] text-sm">{item.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<section className="bg-white py-16 md:py-24">
|
||||
<div className="max-w-3xl mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<span className="text-[#006838] font-semibold text-xs uppercase tracking-[0.12em] mb-2 block">
|
||||
Common Questions
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-[#006838]">
|
||||
Frequently Asked Questions
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{faqs.map((faq, index) => (
|
||||
<details
|
||||
key={index}
|
||||
className="group bg-[#F5F7FC] rounded-xl overflow-hidden"
|
||||
>
|
||||
<summary className="flex items-center justify-between cursor-pointer p-6 font-semibold text-[#111518]">
|
||||
{faq.question}
|
||||
<ChevronDown className="w-5 h-5 text-[#6b7280] group-open:rotate-180 transition-transform" />
|
||||
</summary>
|
||||
<div className="px-6 pb-6 text-[#6b7280]">
|
||||
{faq.answer}
|
||||
</div>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="bg-[#006838] py-16">
|
||||
<div className="max-w-3xl mx-auto px-4 text-center">
|
||||
<h2 className="text-3xl md:text-4xl font-extrabold text-white mb-4">
|
||||
Ready to Run?
|
||||
</h2>
|
||||
<p className="text-white/80 text-lg mb-8">
|
||||
Secure your place in Kenya's only environmental marathon.
|
||||
</p>
|
||||
<Link
|
||||
href="/registration"
|
||||
className="inline-block bg-[#ffcd05] text-[#111518] font-bold text-sm px-10 py-4 rounded-lg hover:bg-[#c4930f] transition-colors duration-200"
|
||||
>
|
||||
REGISTER NOW
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,263 @@
|
||||
import Link from 'next/link'
|
||||
import { ExternalLink, Calendar, Users, Trophy, ShieldCheck } from 'lucide-react'
|
||||
import { Navigation } from '@/components/navigation'
|
||||
import { Footer } from '@/components/footer'
|
||||
|
||||
const editions = [
|
||||
{
|
||||
year: 2024,
|
||||
date: 'October 2024',
|
||||
participants: '350+',
|
||||
highlight: 'Record participation in wheelchair category',
|
||||
resultsUrl: '#',
|
||||
},
|
||||
{
|
||||
year: 2023,
|
||||
date: 'October 2023',
|
||||
participants: '280',
|
||||
highlight: 'First corporate relay category introduced',
|
||||
resultsUrl: '#',
|
||||
},
|
||||
{
|
||||
year: 2022,
|
||||
date: 'October 2022',
|
||||
participants: '150',
|
||||
highlight: 'Inaugural edition - 10M Trees mission launched',
|
||||
resultsUrl: '#',
|
||||
},
|
||||
]
|
||||
|
||||
const topFinishers = {
|
||||
fullMarathon: {
|
||||
men: [
|
||||
{ place: '1st', name: '[Winner TBD]', time: '[TIME]', year: 2024 },
|
||||
{ place: '2nd', name: '[Runner-up TBD]', time: '[TIME]', year: 2024 },
|
||||
{ place: '3rd', name: '[Third TBD]', time: '[TIME]', year: 2024 },
|
||||
],
|
||||
women: [
|
||||
{ place: '1st', name: '[Winner TBD]', time: '[TIME]', year: 2024 },
|
||||
{ place: '2nd', name: '[Runner-up TBD]', time: '[TIME]', year: 2024 },
|
||||
{ place: '3rd', name: '[Third TBD]', time: '[TIME]', year: 2024 },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default function ResultsPage() {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Navigation />
|
||||
|
||||
{/* Page Hero */}
|
||||
<section className="bg-[#006838] pt-20">
|
||||
<div className="max-w-5xl mx-auto px-4 text-center py-16 md:py-20">
|
||||
<span className="text-[#006838] bg-white/10 backdrop-blur font-semibold text-xs uppercase tracking-[0.12em] px-4 py-2 rounded-full inline-block mb-4">
|
||||
Race History
|
||||
</span>
|
||||
<h1 className="text-3xl md:text-4xl lg:text-5xl font-extrabold text-white mb-4">
|
||||
Results Archive
|
||||
</h1>
|
||||
<p className="text-white/80 text-lg max-w-2xl mx-auto">
|
||||
Complete results from all editions of the Lukenya University Marathon, certified by Sporthive.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Sporthive Integration Banner */}
|
||||
<section className="bg-[#005030] py-6">
|
||||
<div className="max-w-5xl mx-auto px-4">
|
||||
<div className="flex flex-col md:flex-row items-center justify-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldCheck className="w-6 h-6 text-[#ffcd05]" />
|
||||
<span className="text-white font-semibold">Powered by Sporthive</span>
|
||||
</div>
|
||||
<span className="hidden md:block text-white/30">|</span>
|
||||
<p className="text-white/70 text-sm text-center md:text-left">
|
||||
Certified chip timing and official race results
|
||||
</p>
|
||||
<a
|
||||
href="https://sporthive.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-[#ffcd05] hover:text-[#e5b02a] text-sm font-semibold transition-colors"
|
||||
>
|
||||
View on Sporthive
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Edition History */}
|
||||
<section className="bg-white py-16 md:py-24">
|
||||
<div className="max-w-5xl mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<span className="text-[#006838] font-semibold text-xs uppercase tracking-[0.12em] mb-2 block">
|
||||
All Editions
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-[#006838]">
|
||||
Race History
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{editions.map((edition) => (
|
||||
<div
|
||||
key={edition.year}
|
||||
className="bg-white rounded-xl shadow-[0_2px_16px_rgba(0,0,0,0.08)] overflow-hidden"
|
||||
>
|
||||
<div className="h-1 bg-[#006838]" />
|
||||
<div className="p-6 md:p-8">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-2xl font-extrabold text-[#006838]">{edition.year}</h3>
|
||||
<span className="text-[#6b7280] text-sm">{edition.date}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mb-3">
|
||||
<div className="flex items-center gap-1 text-[#111518]">
|
||||
<Users className="w-4 h-4 text-[#006838]" />
|
||||
<span className="text-sm font-medium">{edition.participants} Finishers</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[#6b7280] text-sm">{edition.highlight}</p>
|
||||
</div>
|
||||
<a
|
||||
href={edition.resultsUrl}
|
||||
className="inline-flex items-center gap-2 bg-[#3bb34a] text-white font-semibold text-sm px-6 py-3 rounded-lg hover:bg-[#2a9a3a] transition-colors duration-200"
|
||||
>
|
||||
View Full Results
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Top Finishers */}
|
||||
<section className="bg-[#F5F7FC] py-16 md:py-24">
|
||||
<div className="max-w-5xl mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<span className="text-[#006838] font-semibold text-xs uppercase tracking-[0.12em] mb-2 block">
|
||||
2024 Highlights
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-[#006838]">
|
||||
Top Finishers
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{/* Men's Full Marathon */}
|
||||
<div className="bg-white rounded-xl shadow-[0_2px_16px_rgba(0,0,0,0.08)] overflow-hidden">
|
||||
<div className="bg-[#006838] text-white px-6 py-4">
|
||||
<h3 className="font-bold">Full Marathon — Men</h3>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{topFinishers.fullMarathon.men.map((finisher, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex items-center justify-between py-3 ${
|
||||
index < topFinishers.fullMarathon.men.length - 1 ? 'border-b border-[#e5e7eb]' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`font-bold ${index === 0 ? 'text-[#ffcd05]' : 'text-[#111518]'}`}>
|
||||
{finisher.place}
|
||||
</span>
|
||||
<span className="text-[#111518]">{finisher.name}</span>
|
||||
</div>
|
||||
<span className="text-[#6b7280] font-medium">{finisher.time}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Women's Full Marathon */}
|
||||
<div className="bg-white rounded-xl shadow-[0_2px_16px_rgba(0,0,0,0.08)] overflow-hidden">
|
||||
<div className="bg-[#006838] text-white px-6 py-4">
|
||||
<h3 className="font-bold">Full Marathon — Women</h3>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{topFinishers.fullMarathon.women.map((finisher, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex items-center justify-between py-3 ${
|
||||
index < topFinishers.fullMarathon.women.length - 1 ? 'border-b border-[#e5e7eb]' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`font-bold ${index === 0 ? 'text-[#ffcd05]' : 'text-[#111518]'}`}>
|
||||
{finisher.place}
|
||||
</span>
|
||||
<span className="text-[#111518]">{finisher.name}</span>
|
||||
</div>
|
||||
<span className="text-[#6b7280] font-medium">{finisher.time}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Stats Section */}
|
||||
<section className="bg-white py-16 md:py-24">
|
||||
<div className="max-w-5xl mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<span className="text-[#006838] font-semibold text-xs uppercase tracking-[0.12em] mb-2 block">
|
||||
Growing Every Year
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-[#006838]">
|
||||
Race Statistics
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
<div className="bg-[#F5F7FC] rounded-xl p-6 text-center">
|
||||
<Calendar className="w-8 h-8 text-[#006838] mx-auto mb-3" />
|
||||
<p className="text-[#006838] font-extrabold text-3xl">3</p>
|
||||
<p className="text-[#6b7280] text-sm">Editions</p>
|
||||
</div>
|
||||
<div className="bg-[#F5F7FC] rounded-xl p-6 text-center">
|
||||
<Users className="w-8 h-8 text-[#006838] mx-auto mb-3" />
|
||||
<p className="text-[#006838] font-extrabold text-3xl">780+</p>
|
||||
<p className="text-[#6b7280] text-sm">Total Finishers</p>
|
||||
</div>
|
||||
<div className="bg-[#F5F7FC] rounded-xl p-6 text-center">
|
||||
<Trophy className="w-8 h-8 text-[#006838] mx-auto mb-3" />
|
||||
<p className="text-[#006838] font-extrabold text-3xl">7</p>
|
||||
<p className="text-[#6b7280] text-sm">Categories</p>
|
||||
</div>
|
||||
<div className="bg-[#F5F7FC] rounded-xl p-6 text-center">
|
||||
<ShieldCheck className="w-8 h-8 text-[#006838] mx-auto mb-3" />
|
||||
<p className="text-[#006838] font-extrabold text-3xl">100%</p>
|
||||
<p className="text-[#6b7280] text-sm">Certified Results</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="bg-[#006838] py-16">
|
||||
<div className="max-w-3xl mx-auto px-4 text-center">
|
||||
<h2 className="text-3xl md:text-4xl font-extrabold text-white mb-4">
|
||||
Ready to Add Your Name?
|
||||
</h2>
|
||||
<p className="text-white/80 text-lg mb-8">
|
||||
Join the growing list of finishers at Kenya's environmental marathon.
|
||||
</p>
|
||||
<Link
|
||||
href="/registration"
|
||||
className="inline-block bg-[#ffcd05] text-[#111518] font-bold text-sm px-10 py-4 rounded-lg hover:bg-[#c4930f] transition-colors duration-200"
|
||||
>
|
||||
REGISTER NOW
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
import Link from 'next/link'
|
||||
import { TreePine, BarChart3, Award, Building } from 'lucide-react'
|
||||
import { Navigation } from '@/components/navigation'
|
||||
import { Footer } from '@/components/footer'
|
||||
|
||||
const sponsorshipTiers = [
|
||||
{
|
||||
name: 'Title Sponsor',
|
||||
investment: '[INVESTMENT TBD]',
|
||||
color: '#ffcd05',
|
||||
benefits: [
|
||||
'Exclusive naming rights: "The [Your Brand] Lukenya University Marathon"',
|
||||
'Logo on all race materials, signage, and digital assets',
|
||||
'Prime branding at start/finish line',
|
||||
'VIP hospitality tent for 20 guests',
|
||||
'Speaking opportunity at awards ceremony',
|
||||
'First right of refusal for subsequent years',
|
||||
'Exclusive press release and media coverage',
|
||||
'Full access to participant database (opt-in)',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Gold Sponsor',
|
||||
investment: '[INVESTMENT TBD]',
|
||||
color: '#ffcd05',
|
||||
benefits: [
|
||||
'Logo on race bibs and finisher medals',
|
||||
'Branded water station on course',
|
||||
'VIP hospitality tent for 10 guests',
|
||||
'Logo on event website and social media',
|
||||
'Inclusion in press releases',
|
||||
'Banner placement at finish line',
|
||||
'Product sampling opportunity',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Silver Sponsor',
|
||||
investment: '[INVESTMENT TBD]',
|
||||
color: '#9ca3af',
|
||||
benefits: [
|
||||
'Logo on event t-shirts',
|
||||
'Banner placement on course',
|
||||
'VIP passes for 5 guests',
|
||||
'Logo on event website',
|
||||
'Social media mentions',
|
||||
'Expo booth space',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Community Sponsor',
|
||||
investment: '[INVESTMENT TBD]',
|
||||
color: '#006838',
|
||||
benefits: [
|
||||
'Logo on event website',
|
||||
'Social media recognition',
|
||||
'VIP passes for 2 guests',
|
||||
'Certificate of partnership',
|
||||
'Opportunity to provide goodie bag items',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const esgBenefits = [
|
||||
{ icon: TreePine, title: 'Verified Impact', description: 'Certified tree-planting offset with GPS-tracked verification in ASAL regions.' },
|
||||
{ icon: BarChart3, title: 'Brand Alignment', description: 'Associate your brand with Kenya\'s only marathon with a verified environmental mission.' },
|
||||
{ icon: Award, title: 'Annual Report', description: 'Comprehensive ESG impact report for your sustainability reporting needs.' },
|
||||
]
|
||||
|
||||
export default function SponsorsPage() {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Navigation />
|
||||
|
||||
{/* Page Hero */}
|
||||
<section className="bg-[#006838] pt-20">
|
||||
<div className="max-w-5xl mx-auto px-4 text-center py-16 md:py-24">
|
||||
<span className="text-[#006838] bg-white/10 backdrop-blur font-semibold text-xs uppercase tracking-[0.12em] px-4 py-2 rounded-full inline-block mb-4">
|
||||
Sponsorship Opportunities
|
||||
</span>
|
||||
<h1 className="text-3xl md:text-4xl lg:text-5xl font-extrabold text-white mb-4">
|
||||
Partner With Purpose
|
||||
</h1>
|
||||
<p className="text-white/80 text-lg max-w-2xl mx-auto">
|
||||
Associate your brand with Kenya's only marathon with a verified environmental mission.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Sponsorship Tiers */}
|
||||
<section className="bg-white py-16 md:py-24">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<span className="text-[#006838] font-semibold text-xs uppercase tracking-[0.12em] mb-2 block">
|
||||
Investment Levels
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-[#006838]">
|
||||
Sponsorship Tiers
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{sponsorshipTiers.map((tier, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white rounded-xl shadow-[0_2px_16px_rgba(0,0,0,0.08)] overflow-hidden"
|
||||
>
|
||||
<div
|
||||
className="h-1"
|
||||
style={{ backgroundColor: tier.color }}
|
||||
/>
|
||||
<div className="p-6 md:p-8">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h3 className="text-2xl font-extrabold text-[#111518]">{tier.name}</h3>
|
||||
</div>
|
||||
<p className="text-[#3bb34a] font-bold text-xl">KES {tier.investment}</p>
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{tier.benefits.map((benefit, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2">
|
||||
<span className="text-[#006838] mt-1">•</span>
|
||||
<span className="text-[#6b7280]">{benefit}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Environmental Partner Tier - Special Section */}
|
||||
<section className="bg-[#006838] py-16 md:py-24">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<div className="bg-white/10 rounded-2xl p-8 md:p-12">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<TreePine className="w-6 h-6 text-[#ffcd05]" />
|
||||
<span className="text-[#ffcd05] font-semibold text-xs uppercase tracking-[0.12em]">
|
||||
ESG Verified Partnership
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h2 className="text-3xl md:text-4xl font-extrabold text-white mb-4">
|
||||
Environmental Partner
|
||||
</h2>
|
||||
<p className="text-white/80 text-lg mb-6">
|
||||
The anchor tier of Lukenya University Marathon. Join as our Environmental Partner and align your brand with Kenya's only marathon committed to verified reforestation in ASAL regions.
|
||||
</p>
|
||||
|
||||
<div className="bg-white rounded-xl p-6 mb-8">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<span className="text-[#006838] font-semibold text-sm uppercase">Investment</span>
|
||||
<p className="text-[#3bb34a] font-extrabold text-2xl">KES [INVESTMENT TBD]</p>
|
||||
</div>
|
||||
<span className="inline-block bg-[#006838] text-white text-xs font-bold px-4 py-2 rounded-full uppercase tracking-wider">
|
||||
ESG Verified Partnership
|
||||
</span>
|
||||
</div>
|
||||
<ul className="space-y-2 text-[#6b7280]">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-[#006838] mt-1">•</span>
|
||||
Verified tree-planting offset in ASAL regions
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-[#006838] mt-1">•</span>
|
||||
ASAL region impact report with GPS verification
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-[#006838] mt-1">•</span>
|
||||
Environmental Partner branding on all race materials
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-[#006838] mt-1">•</span>
|
||||
Speaking slot on environmental mission at pre-race expo
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-[#006838] mt-1">•</span>
|
||||
Annual sustainability report co-branded with your organization
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
{esgBenefits.map((benefit, index) => (
|
||||
<div key={index} className="text-center">
|
||||
<benefit.icon className="w-10 h-10 text-[#ffcd05] mx-auto mb-3" />
|
||||
<h4 className="font-bold text-white mb-2">{benefit.title}</h4>
|
||||
<p className="text-white/70 text-sm">{benefit.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<Link
|
||||
href="#contact"
|
||||
className="inline-block bg-[#ffcd05] text-[#111518] font-bold text-sm px-8 py-4 rounded-lg hover:bg-[#c4930f] transition-colors duration-200"
|
||||
>
|
||||
REQUEST ESG PARTNERSHIP BRIEF
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Why Partner Section */}
|
||||
<section className="bg-[#F5F7FC] py-16 md:py-24">
|
||||
<div className="max-w-5xl mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<span className="text-[#006838] font-semibold text-xs uppercase tracking-[0.12em] mb-2 block">
|
||||
Why the Environmental Partner Tier?
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-[#006838]">
|
||||
Corporate ESG Value
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div className="bg-white rounded-xl p-6 shadow-[0_2px_16px_rgba(0,0,0,0.08)]">
|
||||
<TreePine className="w-10 h-10 text-[#006838] mb-4" />
|
||||
<h3 className="font-bold text-[#111518] mb-2">Verified Impact</h3>
|
||||
<p className="text-[#6b7280] text-sm">
|
||||
GPS-tracked tree planting in Kenya's ASAL regions with third-party verification. Real, measurable environmental impact.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-6 shadow-[0_2px_16px_rgba(0,0,0,0.08)]">
|
||||
<Building className="w-10 h-10 text-[#006838] mb-4" />
|
||||
<h3 className="font-bold text-[#111518] mb-2">Brand Alignment</h3>
|
||||
<p className="text-[#6b7280] text-sm">
|
||||
Position your brand alongside Kenya's only marathon with a verified environmental mission. Authentic ESG storytelling.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-6 shadow-[0_2px_16px_rgba(0,0,0,0.08)]">
|
||||
<BarChart3 className="w-10 h-10 text-[#006838] mb-4" />
|
||||
<h3 className="font-bold text-[#111518] mb-2">Annual Report</h3>
|
||||
<p className="text-[#6b7280] text-sm">
|
||||
Comprehensive environmental impact report suitable for corporate sustainability reporting and stakeholder communications.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Contact Form */}
|
||||
<section id="contact" className="bg-white py-16 md:py-24">
|
||||
<div className="max-w-2xl mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<span className="text-[#006838] font-semibold text-xs uppercase tracking-[0.12em] mb-2 block">
|
||||
Get Started
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-[#006838]">
|
||||
Ready to Discuss a Partnership?
|
||||
</h2>
|
||||
<p className="text-[#6b7280] mt-4">
|
||||
Fill in the form below and we'll respond within 2 business days.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#111518] mb-1">Company Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full px-4 py-3 rounded-lg border border-[#e5e7eb] focus:border-[#3bb34a] focus:ring-2 focus:ring-[#3bb34a]/20 outline-none transition-colors"
|
||||
placeholder="Your company name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#111518] mb-1">Contact Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full px-4 py-3 rounded-lg border border-[#e5e7eb] focus:border-[#3bb34a] focus:ring-2 focus:ring-[#3bb34a]/20 outline-none transition-colors"
|
||||
placeholder="Your name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#111518] mb-1">Email *</label>
|
||||
<input
|
||||
type="email"
|
||||
className="w-full px-4 py-3 rounded-lg border border-[#e5e7eb] focus:border-[#3bb34a] focus:ring-2 focus:ring-[#3bb34a]/20 outline-none transition-colors"
|
||||
placeholder="you@company.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#111518] mb-1">Phone</label>
|
||||
<input
|
||||
type="tel"
|
||||
className="w-full px-4 py-3 rounded-lg border border-[#e5e7eb] focus:border-[#3bb34a] focus:ring-2 focus:ring-[#3bb34a]/20 outline-none transition-colors"
|
||||
placeholder="+254"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#111518] mb-1">Tier of Interest</label>
|
||||
<select className="w-full px-4 py-3 rounded-lg border border-[#e5e7eb] focus:border-[#3bb34a] focus:ring-2 focus:ring-[#3bb34a]/20 outline-none transition-colors">
|
||||
<option value="">Select a tier</option>
|
||||
<option value="title">Title Sponsor</option>
|
||||
<option value="gold">Gold Sponsor</option>
|
||||
<option value="silver">Silver Sponsor</option>
|
||||
<option value="community">Community Sponsor</option>
|
||||
<option value="environmental">Environmental Partner</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#111518] mb-1">Message</label>
|
||||
<textarea
|
||||
rows={4}
|
||||
className="w-full px-4 py-3 rounded-lg border border-[#e5e7eb] focus:border-[#3bb34a] focus:ring-2 focus:ring-[#3bb34a]/20 outline-none transition-colors resize-none"
|
||||
placeholder="Tell us about your sponsorship goals..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-[#3bb34a] text-white font-semibold text-sm py-4 rounded-lg hover:bg-[#2a9a3a] transition-colors duration-200"
|
||||
>
|
||||
SEND ENQUIRY
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Current Sponsors */}
|
||||
<section className="bg-[#F5F7FC] py-16">
|
||||
<div className="max-w-5xl mx-auto px-4">
|
||||
<div className="text-center mb-10">
|
||||
<span className="text-[#006838] font-semibold text-xs uppercase tracking-[0.12em] mb-2 block">
|
||||
Our Current Partners
|
||||
</span>
|
||||
<h2 className="text-2xl font-bold text-[#111518]">
|
||||
Brands Running With Purpose
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-6">
|
||||
{[1, 2, 3, 4, 5].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="w-36 h-20 bg-white border border-[#e5e7eb] rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<span className="text-[#6b7280] text-xs">Sponsor Logo</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user