326 lines
9.1 KiB
TypeScript
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're looking for doesn'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>
|
|
);
|
|
}
|