'use client'; import { useState, useEffect } from 'react'; import { useRouter, useParams } from 'next/navigation'; import Link from 'next/link'; import Button from '@/components/ui/Button'; import Card from '@/components/ui/Card'; import Heading from '@/components/ui/Heading'; import Breadcrumbs from '@/components/layout/Breadcrumbs'; import FileProtestModal from '@/components/races/FileProtestModal'; import SponsorInsightsCard, { useSponsorMode, MetricBuilders, SlotTemplates } from '@/components/sponsors/SponsorInsightsCard'; import { getGetRaceDetailUseCase, getRegisterForRaceUseCase, getWithdrawFromRaceUseCase, getCancelRaceUseCase } from '@/lib/di-container'; import { useEffectiveDriverId } from '@/lib/currentDriver'; import type { RaceDetailViewModel, RaceDetailEntryViewModel, RaceDetailUserResultViewModel, } from '@gridpilot/racing/application/presenters/IRaceDetailPresenter'; import { Calendar, Clock, Car, Trophy, Users, Zap, PlayCircle, CheckCircle2, XCircle, Flag, UserPlus, UserMinus, AlertTriangle, ArrowRight, ArrowLeft, Scale, } from 'lucide-react'; export default function RaceDetailPage() { const router = useRouter(); const params = useParams(); const raceId = params.id as string; const [viewModel, setViewModel] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [cancelling, setCancelling] = useState(false); const [registering, setRegistering] = useState(false); const [ratingChange, setRatingChange] = useState(null); const [animatedRatingChange, setAnimatedRatingChange] = useState(0); const [showProtestModal, setShowProtestModal] = useState(false); const currentDriverId = useEffectiveDriverId(); const isSponsorMode = useSponsorMode(); const loadRaceData = async () => { setLoading(true); setError(null); try { const useCase = getGetRaceDetailUseCase(); await useCase.execute({ raceId, driverId: currentDriverId }); const vm = useCase.presenter.getViewModel(); if (!vm) { throw new Error('Race detail not available'); } setViewModel(vm); const userResultRatingChange = vm.userResult?.ratingChange ?? null; setRatingChange(userResultRatingChange); if (userResultRatingChange === null) { setAnimatedRatingChange(0); } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load race'); setViewModel(null); } finally { setLoading(false); } }; useEffect(() => { loadRaceData(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [raceId]); // Animate rating change when it changes useEffect(() => { if (ratingChange !== null) { let start = 0; const end = ratingChange; const duration = 1000; const startTime = performance.now(); const animate = (currentTime: number) => { const elapsed = currentTime - startTime; const progress = Math.min(elapsed / duration, 1); const eased = 1 - Math.pow(1 - progress, 3); const current = Math.round(start + (end - start) * eased); setAnimatedRatingChange(current); if (progress < 1) { requestAnimationFrame(animate); } }; requestAnimationFrame(animate); } }, [ratingChange]); const handleCancelRace = async () => { const race = viewModel?.race; if (!race || race.status !== 'scheduled') return; const confirmed = window.confirm( 'Are you sure you want to cancel this race? This action cannot be undone.', ); if (!confirmed) return; setCancelling(true); try { const useCase = getCancelRaceUseCase(); await useCase.execute({ raceId: race.id }); await loadRaceData(); } catch (err) { alert(err instanceof Error ? err.message : 'Failed to cancel race'); } finally { setCancelling(false); } }; const handleRegister = async () => { const race = viewModel?.race; const league = viewModel?.league; if (!race || !league) return; const confirmed = window.confirm( `Register for ${race.track}?\n\nYou'll be added to the entry list for this race.`, ); if (!confirmed) return; setRegistering(true); try { const useCase = getRegisterForRaceUseCase(); await useCase.execute({ raceId: race.id, leagueId: league.id, driverId: currentDriverId, }); await loadRaceData(); } catch (err) { alert(err instanceof Error ? err.message : 'Failed to register for race'); } finally { setRegistering(false); } }; const handleWithdraw = async () => { const race = viewModel?.race; const league = viewModel?.league; if (!race || !league) return; const confirmed = window.confirm( 'Withdraw from this race?\n\nYou can register again later if you change your mind.', ); if (!confirmed) return; setRegistering(true); try { const useCase = getWithdrawFromRaceUseCase(); await useCase.execute({ raceId: race.id, driverId: currentDriverId, }); await loadRaceData(); } catch (err) { alert(err instanceof Error ? err.message : 'Failed to withdraw from race'); } finally { setRegistering(false); } }; const formatDate = (date: Date) => { return new Date(date).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric', }); }; const formatTime = (date: Date) => { return new Date(date).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', timeZoneName: 'short', }); }; const getTimeUntil = (date: Date) => { const now = new Date(); const target = new Date(date); const diffMs = target.getTime() - now.getTime(); if (diffMs < 0) return null; const days = Math.floor(diffMs / (1000 * 60 * 60 * 24)); const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)); if (days > 0) return `${days}d ${hours}h`; if (hours > 0) return `${hours}h ${minutes}m`; return `${minutes}m`; }; const statusConfig = { scheduled: { icon: Clock, color: 'text-primary-blue', bg: 'bg-primary-blue/10', border: 'border-primary-blue/30', label: 'Scheduled', description: 'This race is scheduled and waiting to start', }, running: { icon: PlayCircle, color: 'text-performance-green', bg: 'bg-performance-green/10', border: 'border-performance-green/30', label: 'LIVE NOW', description: 'This race is currently in progress', }, completed: { icon: CheckCircle2, color: 'text-gray-400', bg: 'bg-gray-500/10', border: 'border-gray-500/30', label: 'Completed', description: 'This race has finished', }, cancelled: { icon: XCircle, color: 'text-warning-amber', bg: 'bg-warning-amber/10', border: 'border-warning-amber/30', label: 'Cancelled', description: 'This race has been cancelled', }, } as const; if (loading) { return (
); } if (error || !viewModel || !viewModel.race) { return (

{error || 'Race not found'}

The race you're looking for doesn't exist or has been removed.

); } const race = viewModel.race; const league = viewModel.league; const entryList: RaceDetailEntryViewModel[] = viewModel.entryList; const registration = viewModel.registration; const userResult: RaceDetailUserResultViewModel | null = viewModel.userResult; const raceSOF = race.strengthOfField; const config = statusConfig[race.status]; const StatusIcon = config.icon; const timeUntil = race.status === 'scheduled' ? getTimeUntil(new Date(race.scheduledAt)) : null; const breadcrumbItems = [ { label: 'Races', href: '/races' }, ...(league ? [{ label: league.name, href: `/leagues/${league.id}` }] : []), { label: race.track }, ]; const getCountryFlag = (countryCode: string): string => { const codePoints = countryCode .toUpperCase() .split('') .map(char => 127397 + char.charCodeAt(0)); return String.fromCodePoint(...codePoints); }; const sponsorInsights = { tier: 'gold' as const, trustScore: 92, discordMembers: league ? 1847 : undefined, monthlyActivity: 156, }; const raceMetrics = [ MetricBuilders.views(entryList.length * 12), MetricBuilders.engagement(78), { label: 'SOF', value: raceSOF != null ? raceSOF.toString() : '—', icon: Zap, color: 'text-warning-amber' as const }, MetricBuilders.reach(entryList.length * 45), ]; return (
{/* Navigation Row: Breadcrumbs left, Back button right */}
{/* Sponsor Insights Card - Consistent placement at top */} {isSponsorMode && race && league && ( )} {/* User Result - Premium Achievement Card */} {userResult && (
{/* Decorative elements */}
{/* Victory confetti effect for P1 */} {userResult.position === 1 && (
)}
{/* Main content grid */}
{/* Left: Position and achievement */}
{/* Giant position badge */}
{userResult.position === 1 && ( )} P{userResult.position}
{/* Achievement text */}

{userResult.position === 1 ? '🏆 VICTORY!' : userResult.position === 2 ? '🥈 Second Place' : userResult.position === 3 ? '🥉 Podium Finish' : userResult.position <= 5 ? '⭐ Top 5 Finish' : userResult.position <= 10 ? 'Points Finish' : `P${userResult.position} Finish`}

Started P{userResult.startPosition} {userResult.incidents}x incidents {userResult.isClean && ' ✨'}
{/* Right: Stats cards */}
{/* Position change */} {userResult.positionChange !== 0 && (
0 ? 'bg-gradient-to-br from-performance-green/30 to-performance-green/10 border border-performance-green/40' : 'bg-gradient-to-br from-red-500/30 to-red-500/10 border border-red-500/40' } `} >
0 ? 'text-performance-green' : 'text-red-400' } `} > {userResult.positionChange > 0 ? ( ) : ( )} {Math.abs(userResult.positionChange)}
{userResult.positionChange > 0 ? 'Gained' : 'Lost'}
)} {/* Rating change */} {ratingChange !== null && (
0 ? 'bg-gradient-to-br from-warning-amber/30 to-warning-amber/10 border border-warning-amber/40' : 'bg-gradient-to-br from-red-500/30 to-red-500/10 border border-red-500/40' } `} >
0 ? 'text-warning-amber' : 'text-red-400'} `} > {animatedRatingChange > 0 ? '+' : ''} {animatedRatingChange}
iRating
)} {/* Clean race bonus */} {userResult.isClean && (
Clean Race
)}
)} {/* Hero Header */}
{/* Live indicator */} {race.status === 'running' && (
)}
{/* Status Badge */}
{race.status === 'running' && ( )} {config.label}
{timeUntil && ( Starts in {timeUntil} )}
{/* Title */} {race.track} {/* Meta */}
{formatDate(new Date(race.scheduledAt))} {formatTime(new Date(race.scheduledAt))} {race.car}
{/* Prominent SOF Badge - Electric Design */} {raceSOF != null && (
{/* Glow effect */}
{/* Electric bolt with animation */}
Strength of Field
{raceSOF} SOF
)}
{/* Main Content */}
{/* Race Details */}

Race Details

Track

{race.track}

Car

{race.car}

Session Type

{race.sessionType}

Status

{config.label}

Strength of Field

{raceSOF ?? '—'}

{race.registeredCount !== undefined && (

Registered

{race.registeredCount} {race.maxParticipants && ` / ${race.maxParticipants}`}

)}
{/* Entry List */}

Entry List

{entryList.length} driver{entryList.length !== 1 ? 's' : ''}
{entryList.length === 0 ? (

No drivers registered yet

Be the first to sign up!

) : (
{entryList.map((driver, index) => { const isCurrentUser = driver.isCurrentUser; const countryFlag = getCountryFlag(driver.country); return (
router.push(`/drivers/${driver.id}`)} className={` flex items-center gap-3 p-3 rounded-xl cursor-pointer transition-all duration-200 ${ isCurrentUser ? 'bg-gradient-to-r from-primary-blue/20 via-primary-blue/10 to-transparent border border-primary-blue/40 shadow-lg shadow-primary-blue/10' : 'bg-deep-graphite hover:bg-charcoal-outline/50 border border-transparent' } `} > {/* Position number */}
{index + 1}
{/* Avatar with nation flag */}
{driver.name} {/* Nation flag */}
{countryFlag}
{/* Driver info */}

{driver.name}

{isCurrentUser && ( You )}

{driver.country}

{/* Rating badge */} {driver.rating != null && (
{driver.rating}
)}
); })}
)}
{/* Sidebar */}
{/* League Card - Premium Design */} {league && (
{league.name}

League

{league.name}

{league.description && (

{league.description}

)}

Max Drivers

{league.settings.maxDrivers ?? 32}

Format

{league.settings.qualifyingFormat ?? 'Open'}

View League
)} {/* Quick Actions Card */}

Actions

{/* Registration Actions */} {race.status === 'scheduled' && registration.canRegister && !registration.isUserRegistered && ( )} {race.status === 'scheduled' && registration.isUserRegistered && ( <>
You're Registered
)} {race.status === 'completed' && ( <> {userResult && ( )} )} {race.status === 'scheduled' && ( )}
{/* Status Info */}

{config.label}

{config.description}

{/* Protest Filing Modal */} setShowProtestModal(false)} raceId={race.id} leagueId={league ? league.id : ''} protestingDriverId={currentDriverId} participants={entryList.map(d => ({ id: d.id, name: d.name }))} />
); }