257 lines
7.8 KiB
TypeScript
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>
|
|
);
|
|
}
|