Files
gridpilot.gg/apps/website/templates/RaceDetailTemplate.tsx
2026-01-18 23:24:30 +01:00

257 lines
7.8 KiB
TypeScript

'use client';
import { LeagueSummaryCard } from '@/components/leagues/LeagueSummaryCardWrapper';
import { EntrantsTable } from '@/components/races/EntrantsTable';
import { RaceActionBar } from '@/components/races/RaceActionBar';
import { RaceDetailsHeader } from '@/components/races/RaceDetailsHeader';
import { RaceUserResult } from '@/components/races/RaceUserResultWrapper';
import type { SessionStatus } from '@/components/races/SessionStatusBadge';
import { TrackConditionsPanel } from '@/components/races/TrackConditionsPanel';
import { Box } from '@/ui/Box';
import { Container } from '@/ui/Container';
import { Grid } from '@/ui/Grid';
import { GridItem } from '@/ui/GridItem';
import { Stack } from '@/ui/Stack';
import { Skeleton } from '@/ui/Skeleton';
import { Text } from '@/ui/Text';
export interface RaceDetailEntryViewModel {
id: string;
name: string;
avatarUrl: string;
country: string;
rating?: number | null;
isCurrentUser: boolean;
}
export interface RaceDetailUserResultViewModel {
position: number;
startPosition: number;
positionChange: number;
incidents: number;
isClean: boolean;
isPodium: boolean;
ratingChange?: number;
}
export interface RaceDetailLeague {
id: string;
name: string;
description?: string;
settings: {
maxDrivers: number;
qualifyingFormat: string;
};
}
export interface RaceDetailRace {
id: string;
track: string;
car: string;
scheduledAt: string;
status: 'scheduled' | 'running' | 'completed' | 'cancelled';
sessionType: string;
}
export interface RaceDetailRegistration {
isUserRegistered: boolean;
canRegister: boolean;
}
export interface RaceDetailViewData {
race: RaceDetailRace;
league?: RaceDetailLeague;
entryList: RaceDetailEntryViewModel[];
registration: RaceDetailRegistration;
userResult?: RaceDetailUserResultViewModel;
canReopenRace: boolean;
}
export interface RaceDetailTemplateProps {
viewData?: RaceDetailViewData;
isLoading: boolean;
error?: Error | null;
// Actions
onBack: () => void;
onRegister: () => void;
onWithdraw: () => void;
onCancel: () => void;
onReopen: () => void;
onEndRace: () => void;
onFileProtest: () => void;
onResultsClick: () => void;
onStewardingClick: () => void;
onLeagueClick: (leagueId: string) => void;
onDriverClick: (driverId: string) => void;
// User state
currentDriverId?: string;
isOwnerOrAdmin?: boolean;
// UI State
animatedRatingChange: number;
// Loading states
mutationLoading?: {
register?: boolean;
withdraw?: boolean;
cancel?: boolean;
reopen?: boolean;
complete?: boolean;
};
}
export function RaceDetailTemplate({
viewData,
isLoading,
error,
onBack,
onRegister,
onWithdraw,
onCancel,
onReopen,
onEndRace,
onFileProtest,
onResultsClick,
onStewardingClick,
isOwnerOrAdmin = false,
animatedRatingChange,
mutationLoading = {},
}: RaceDetailTemplateProps) {
if (isLoading) {
return (
<Container size="lg" py={8}>
<Stack gap={6}>
<Skeleton width="8rem" height="1.5rem" />
<Skeleton width="100%" height="12rem" />
<Grid cols={3} gap={6}>
<GridItem colSpan={2}>
<Skeleton width="100%" height="16rem" />
</GridItem>
<Skeleton width="100%" height="16rem" />
</Grid>
</Stack>
</Container>
);
}
if (error || !viewData || !viewData.race) {
return (
<Container size="md" py={8}>
<Box bg="bg-surface-charcoal" border borderColor="border-outline-steel" p={12} textAlign="center" rounded="xl">
<Stack alignItems="center" gap={4}>
<Text as="h2" size="xl" weight="bold" color="text-white">Race Not Found</Text>
<Text color="text-gray-400">{`The race you're looking for doesn't exist or has been removed.`}</Text>
<Box
as="button"
onClick={onBack}
mt={4}
px={6}
py={2}
bg="bg-primary-accent"
color="text-white"
weight="bold"
rounded="md"
hoverBg="bg-primary-accent"
bgOpacity={0.8}
transition
>
Back to Schedule
</Box>
</Stack>
</Box>
</Container>
);
}
const { race, league, entryList, userResult } = viewData;
return (
<Box as="main" minHeight="screen" bg="bg-base-black">
<RaceDetailsHeader
title={race.track}
leagueName={league?.name || 'Official'}
trackName={race.track}
scheduledAt={race.scheduledAt}
status={race.status as SessionStatus}
onBack={onBack}
/>
<Container size="lg" py={8}>
<Stack gap={8}>
{userResult && (
<RaceUserResult
{...userResult}
animatedRatingChange={animatedRatingChange}
/>
)}
<Box bg="bg-surface-charcoal" border borderColor="border-outline-steel" p={4}>
<RaceActionBar
status={race.status}
isUserRegistered={viewData.registration.isUserRegistered}
canRegister={viewData.registration.canRegister}
onRegister={onRegister}
onWithdraw={onWithdraw}
onResultsClick={onResultsClick}
onStewardingClick={onStewardingClick}
onFileProtest={onFileProtest}
isAdmin={isOwnerOrAdmin}
onCancel={onCancel}
onReopen={onReopen}
onEndRace={onEndRace}
isLoading={mutationLoading}
/>
</Box>
<Grid cols={12} gap={6}>
<GridItem lgSpan={8} colSpan={12}>
<Stack gap={6}>
<Box as="section" bg="bg-surface-charcoal" border borderColor="border-outline-steel" overflow="hidden">
<Box p={4} borderBottom borderColor="border-outline-steel" bg="bg-base-black" bgOpacity={0.2}>
<Text size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="widest">Entry List</Text>
</Box>
<EntrantsTable
entrants={entryList.map(entry => ({
id: entry.id,
name: entry.name,
carName: race.car,
rating: entry.rating || 0,
status: 'confirmed'
}))}
/>
</Box>
</Stack>
</GridItem>
<GridItem lgSpan={4} colSpan={12}>
<Stack gap={6}>
{league && <LeagueSummaryCard league={league} />}
<TrackConditionsPanel
airTemp="24°C"
trackTemp="31°C"
humidity="45%"
windSpeed="12 km/h NW"
weatherType="Partly Cloudy"
/>
<Box as="section" bg="bg-surface-charcoal" border borderColor="border-outline-steel" p={4}>
<Text as="h3" size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="widest" mb={4}>Session Info</Text>
<Stack gap={4}>
<Stack gap={1}>
<Text size="xs" color="text-gray-500" uppercase weight="bold">Format</Text>
<Text size="sm" color="text-white">{race.sessionType}</Text>
</Stack>
<Stack gap={1}>
<Text size="xs" color="text-gray-500" uppercase weight="bold">Car Class</Text>
<Text size="sm" color="text-white">{race.car}</Text>
</Stack>
</Stack>
</Box>
</Stack>
</GridItem>
</Grid>
</Stack>
</Container>
</Box>
);
}