Files
gridpilot.gg/apps/website/templates/RaceDetailTemplate.tsx
2026-01-15 17:12:24 +01:00

326 lines
9.1 KiB
TypeScript

'use client';
import React from 'react';
import { Breadcrumbs } from '@/ui/Breadcrumbs';
import { Button } from '@/ui/Button';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Container } from '@/ui/Container';
import { Icon } from '@/ui/Icon';
import { Grid } from '@/ui/Grid';
import { GridItem } from '@/ui/GridItem';
import { Skeleton } from '@/ui/Skeleton';
import { InfoBox } from '@/ui/InfoBox';
import { RaceJoinButton } from '@/ui/RaceJoinButton';
import { RaceHero } from '@/ui/RaceHeroWrapper';
import { RaceUserResult } from '@/ui/RaceUserResultWrapper';
import { RaceEntryList } from '@/ui/RaceEntryList';
import { RaceDetailCard } from '@/ui/RaceDetailCard';
import { LeagueSummaryCard } from '@/ui/LeagueSummaryCardWrapper';
import {
AlertTriangle,
ArrowLeft,
CheckCircle2,
Clock,
PlayCircle,
Trophy,
XCircle,
Scale,
} from 'lucide-react';
import { Surface } from '@/ui/Surface';
import { Card } from '@/ui/Card';
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,
onDriverClick,
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}>
<Stack gap={6}>
<Breadcrumbs items={[{ label: 'Races', href: '/races' }, { label: 'Error' }]} />
<Card>
<Stack align="center" gap={4} py={12}>
<Surface variant="muted" rounded="full" padding={4}>
<Icon icon={AlertTriangle} size={8} color="#f59e0b" />
</Surface>
<Box>
<Text weight="medium" color="text-white" block mb={1}>{error instanceof Error ? error.message : error || 'Race not found'}</Text>
<Text size="sm" color="text-gray-500">
The race you&apos;re looking for doesn&apos;t exist or has been removed.
</Text>
</Box>
<Button
variant="secondary"
onClick={onBack}
>
Back to Races
</Button>
</Stack>
</Card>
</Stack>
</Container>
);
}
const { race, league, entryList, userResult } = viewData;
const statusConfig = {
scheduled: {
icon: Clock,
variant: 'primary' as const,
label: 'Scheduled',
description: 'This race is scheduled and waiting to start',
},
running: {
icon: PlayCircle,
variant: 'success' as const,
label: 'LIVE NOW',
description: 'This race is currently in progress',
},
completed: {
icon: CheckCircle2,
variant: 'default' as const,
label: 'Completed',
description: 'This race has finished',
},
cancelled: {
icon: XCircle,
variant: 'warning' as const,
label: 'Cancelled',
description: 'This race has been cancelled',
},
};
const config = statusConfig[race.status] || statusConfig.scheduled;
const breadcrumbItems = [
{ label: 'Races', href: '/races' },
...(league ? [{ label: league.name, href: `/leagues/${league.id}` }] : []),
{ label: race.track },
];
return (
<Container size="lg" py={8}>
<Stack gap={6}>
{/* Navigation Row */}
<Stack direction="row" align="center" justify="between">
<Breadcrumbs items={breadcrumbItems} />
<Button
variant="secondary"
onClick={onBack}
size="sm"
icon={<Icon icon={ArrowLeft} size={4} />}
>
Back
</Button>
</Stack>
{/* User Result */}
{userResult && (
<RaceUserResult
{...userResult}
animatedRatingChange={animatedRatingChange}
/>
)}
{/* Hero Header */}
<RaceHero
track={race.track}
scheduledAt={race.scheduledAt}
car={race.car}
status={race.status}
statusConfig={config}
/>
<Grid cols={12} gap={6}>
<GridItem lgSpan={8} colSpan={12}>
<Stack gap={6}>
<RaceDetailCard
track={race.track}
car={race.car}
sessionType={race.sessionType}
statusLabel={config.label}
statusColor={config.variant === 'success' ? '#10b981' : config.variant === 'primary' ? '#3b82f6' : '#9ca3af'}
/>
<RaceEntryList
entries={entryList}
onDriverClick={onDriverClick}
/>
</Stack>
</GridItem>
<GridItem lgSpan={4} colSpan={12}>
<Stack gap={6}>
{league && <LeagueSummaryCard league={league} />}
{/* Actions Card */}
<Card>
<Stack gap={4}>
<Text size="xl" weight="bold" color="text-white">Actions</Text>
<Stack gap={3}>
<RaceJoinButton
raceStatus={race.status}
isUserRegistered={viewData.registration.isUserRegistered}
canRegister={viewData.registration.canRegister}
onRegister={onRegister}
onWithdraw={onWithdraw}
onCancel={onCancel}
onReopen={onReopen}
onEndRace={onEndRace}
canReopenRace={viewData.canReopenRace}
isOwnerOrAdmin={isOwnerOrAdmin}
isLoading={mutationLoading}
/>
{race.status === 'completed' && (
<>
<Button variant="primary" fullWidth onClick={onResultsClick} icon={<Icon icon={Trophy} size={4} />}>
View Results
</Button>
{userResult && (
<Button variant="secondary" fullWidth onClick={onFileProtest} icon={<Icon icon={Scale} size={4} />}>
File Protest
</Button>
)}
<Button variant="secondary" fullWidth onClick={onStewardingClick} icon={<Icon icon={Scale} size={4} />}>
Stewarding
</Button>
</>
)}
</Stack>
</Stack>
</Card>
{/* Status Info */}
<InfoBox
icon={config.icon}
title={config.label}
description={config.description}
variant={config.variant}
/>
</Stack>
</GridItem>
</Grid>
</Stack>
</Container>
);
}