180 lines
7.2 KiB
TypeScript
180 lines
7.2 KiB
TypeScript
'use client';
|
|
|
|
import { RaceDetailsHeader } from '@/components/races/RaceDetailsHeader';
|
|
import { RaceResultsTable } from '@/components/races/RaceResultsTable';
|
|
import type { RaceResultsViewData } from '@/lib/view-data/races/RaceResultsViewData';
|
|
import { Box } from '@/ui/Box';
|
|
import { Container } from '@/ui/Container';
|
|
import { Icon } from '@/ui/Icon';
|
|
import { Grid } from '@/ui/Grid';
|
|
import { GridItem } from '@/ui/GridItem';
|
|
import { Stack } from '@/ui/Stack';
|
|
import { Text } from '@/ui/Text';
|
|
import { AlertTriangle, Trophy, Zap, type LucideIcon } from 'lucide-react';
|
|
|
|
export interface RaceResultsTemplateProps {
|
|
viewData: RaceResultsViewData;
|
|
isAdmin: boolean;
|
|
isLoading: boolean;
|
|
error?: Error | null;
|
|
// Actions
|
|
onBack: () => void;
|
|
onImportResults: (results: unknown[]) => void;
|
|
onPenaltyClick: (driver: { id: string; name: string }) => void;
|
|
// UI State
|
|
importing: boolean;
|
|
importSuccess: boolean;
|
|
importError: string | null;
|
|
showImportForm: boolean;
|
|
setShowImportForm: (show: boolean) => void;
|
|
}
|
|
|
|
export function RaceResultsTemplate({
|
|
viewData,
|
|
isLoading,
|
|
error,
|
|
onBack,
|
|
importSuccess,
|
|
importError,
|
|
}: RaceResultsTemplateProps) {
|
|
const formatTime = (ms: number) => {
|
|
const minutes = Math.floor(ms / 60000);
|
|
const seconds = Math.floor((ms % 60000) / 1000);
|
|
const milliseconds = Math.floor((ms % 1000) / 10);
|
|
return `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(2, '0')}`;
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<Container size="lg" spacing="lg">
|
|
<Stack alignItems="center">
|
|
<Text color="text-gray-400">Loading results...</Text>
|
|
</Stack>
|
|
</Container>
|
|
);
|
|
}
|
|
|
|
if (error && !viewData.raceTrack) {
|
|
return (
|
|
<Container size="md" spacing="lg">
|
|
<Box bg="bg-surface-charcoal" border borderColor="border-outline-steel" p={12} textAlign="center" rounded="xl">
|
|
<Stack alignItems="center" gap={4}>
|
|
<Text color="text-warning-amber">{error?.message || 'Race not found'}</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>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box as="main" minHeight="screen" bg="bg-base-black">
|
|
<RaceDetailsHeader
|
|
title={viewData.raceTrack || 'Unknown Track'}
|
|
leagueName={viewData.leagueName || 'Official'}
|
|
trackName={viewData.raceTrack || 'Unknown Track'}
|
|
scheduledAt={viewData.raceScheduledAt || ''}
|
|
status="completed"
|
|
onBack={onBack}
|
|
/>
|
|
|
|
<Container size="lg" spacing="md">
|
|
<Stack gap={8}>
|
|
{importSuccess && (
|
|
<Box p={4} bg="bg-success-green" bgOpacity={0.1} border borderColor="border-success-green" rounded>
|
|
<Text color="text-success-green" weight="bold">Success!</Text>
|
|
<Text color="text-success-green" size="sm" block mt={1}>Results imported and standings updated.</Text>
|
|
</Box>
|
|
)}
|
|
|
|
{importError && (
|
|
<Box p={4} bg="bg-critical-red" bgOpacity={0.1} border borderColor="border-critical-red" rounded>
|
|
<Text color="text-critical-red" weight="bold">Error:</Text>
|
|
<Text color="text-critical-red" size="sm" block mt={1}>{importError}</Text>
|
|
</Box>
|
|
)}
|
|
|
|
<Grid cols={12} gap={6}>
|
|
<GridItem colSpan={12} lgSpan={8}>
|
|
<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} display="flex" alignItems="center" gap={2}>
|
|
<Icon icon={Trophy} size={4} color="var(--color-primary)" />
|
|
<Text as="h2" size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="widest">Final Standings</Text>
|
|
</Box>
|
|
<RaceResultsTable
|
|
results={viewData.results as unknown as never[]}
|
|
drivers={[]}
|
|
pointsSystem={viewData.pointsSystem as unknown as Record<number, number>}
|
|
fastestLapTime={viewData.fastestLapTime}
|
|
penalties={viewData.penalties as unknown as never[]}
|
|
/>
|
|
</Box>
|
|
</GridItem>
|
|
|
|
<GridItem colSpan={12} lgSpan={4}>
|
|
<Stack gap={6}>
|
|
<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 Stats</Text>
|
|
<Stack gap={4}>
|
|
<StatItem label="DRIVERS" value={viewData.totalDrivers ?? 0} />
|
|
<StatItem label="SOF" value={viewData.raceSOF ?? '—'} icon={Zap} color="text-warning-amber" />
|
|
<StatItem label="FASTEST LAP" value={viewData.fastestLapTime ? formatTime(viewData.fastestLapTime) : '—'} color="text-success-green" />
|
|
</Stack>
|
|
</Box>
|
|
|
|
{viewData.penalties.length > 0 && (
|
|
<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}>Penalties</Text>
|
|
<Stack gap={2}>
|
|
{viewData.penalties.map((penalty, index) => (
|
|
<Box key={index} p={3} bg="bg-base-black" bgOpacity={0.5} border borderColor="border-outline-steel">
|
|
<Stack direction="row" alignItems="center" gap={3}>
|
|
<Icon icon={AlertTriangle} size={4} color="var(--color-critical)" />
|
|
<Box>
|
|
<Text size="sm" weight="bold" color="text-white" block>{penalty.driverName || 'Unknown Driver'}</Text>
|
|
<Text size="xs" color="text-gray-500">{penalty.reason || penalty.type}</Text>
|
|
</Box>
|
|
</Stack>
|
|
</Box>
|
|
))}
|
|
</Stack>
|
|
</Box>
|
|
)}
|
|
</Stack>
|
|
</GridItem>
|
|
</Grid>
|
|
</Stack>
|
|
</Container>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
function StatItem({ label, value, icon, color = 'text-white' }: { label: string, value: string | number, icon?: LucideIcon, color?: string }) {
|
|
return (
|
|
<Box p={4} bg="bg-base-black" bgOpacity={0.5} border borderColor="border-outline-steel">
|
|
<Stack gap={1}>
|
|
<Stack direction="row" alignItems="center" gap={2}>
|
|
{icon && <Icon icon={icon} size={3} color={color === 'text-white' ? '#9ca3af' : color} />}
|
|
<Text size="xs" color="text-gray-500" weight="bold" uppercase>{label}</Text>
|
|
</Stack>
|
|
<Text size="xl" weight="bold" color={color}>{value}</Text>
|
|
</Stack>
|
|
</Box>
|
|
);
|
|
}
|