Files
gridpilot.gg/apps/website/templates/RaceResultsTemplate.tsx
2026-01-15 18:52:03 +01:00

227 lines
7.9 KiB
TypeScript

'use client';
import React from 'react';
import { Breadcrumbs } from '@/ui/Breadcrumbs';
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Container } from '@/ui/Container';
import { Grid } from '@/ui/Grid';
import { Icon } from '@/ui/Icon';
import { Surface } from '@/ui/Surface';
import { ArrowLeft, Trophy, Zap, type LucideIcon } from 'lucide-react';
import type { RaceResultsViewData } from '@/lib/view-data/races/RaceResultsViewData';
import { RaceResultRow } from '@/ui/RaceResultRow';
import { RacePenaltyRow } from '@/ui/RacePenaltyRowWrapper';
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,
onImportResults,
importing,
importSuccess,
importError,
}: RaceResultsTemplateProps) {
const formatDate = (date: string) => {
return new Date(date).toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric',
});
};
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')}`;
};
const breadcrumbItems = [
{ label: 'Races', href: '/races' },
...(viewData.leagueName ? [{ label: viewData.leagueName, href: `/leagues/${viewData.leagueName}` }] : []),
...(viewData.raceTrack ? [{ label: viewData.raceTrack, href: `/races/${viewData.raceTrack}` }] : []),
{ label: 'Results' },
];
if (isLoading) {
return (
<Container size="lg" py={12}>
<Stack align="center">
<Text color="text-gray-400">Loading results...</Text>
</Stack>
</Container>
);
}
if (error && !viewData.raceTrack) {
return (
<Container size="md" py={12}>
<Card>
<Stack align="center" py={12} gap={4}>
<Text color="text-warning-amber">{error?.message || 'Race not found'}</Text>
<Button
variant="secondary"
onClick={onBack}
>
Back to Races
</Button>
</Stack>
</Card>
</Container>
);
}
const hasResults = viewData.results.length > 0;
return (
<Container size="lg" py={8}>
<Stack gap={6}>
<Stack direction="row" align="center" justify="between">
<Breadcrumbs items={breadcrumbItems} />
<Button
variant="secondary"
onClick={onBack}
icon={<Icon icon={ArrowLeft} size={4} />}
>
Back
</Button>
</Stack>
{/* Header */}
<Surface variant="muted" rounded="xl" border padding={6} bg="bg-neutral-800/50" borderColor="border-neutral-800">
<Stack direction="row" align="center" gap={4} mb={6}>
<Surface variant="muted" rounded="xl" padding={3} bg="bg-blue-500/20">
<Icon icon={Trophy} size={6} color="#3b82f6" />
</Surface>
<Box>
<Heading level={1}>Race Results</Heading>
<Text size="sm" color="text-gray-400" block mt={1}>
{viewData.raceTrack} {viewData.raceScheduledAt ? formatDate(viewData.raceScheduledAt) : ''}
</Text>
</Box>
</Stack>
{/* Stats */}
<Grid cols={4} gap={4}>
<StatItem label="Drivers" value={viewData.totalDrivers ?? 0} />
<StatItem label="League" value={viewData.leagueName ?? '—'} />
<StatItem label="SOF" value={viewData.raceSOF ?? '—'} icon={Zap} color="text-warning-amber" />
<StatItem label="Fastest Lap" value={viewData.fastestLapTime ? formatTime(viewData.fastestLapTime) : '—'} color="text-performance-green" />
</Grid>
</Surface>
{importSuccess && (
<Surface variant="muted" rounded="lg" border padding={4} bg="bg-green-500/10" borderColor="border-green-500/30">
<Text color="text-performance-green" weight="bold">Success!</Text>
<Text color="text-performance-green" size="sm" block mt={1}>Results imported and standings updated.</Text>
</Surface>
)}
{importError && (
<Surface variant="muted" rounded="lg" border padding={4} bg="bg-red-500/10" borderColor="border-red-500/30">
<Text color="text-error-red" weight="bold">Error:</Text>
<Text color="text-error-red" size="sm" block mt={1}>{importError}</Text>
</Surface>
)}
<Card>
{hasResults ? (
<Stack gap={6}>
{/* Results Table */}
<Stack gap={2}>
{viewData.results.map((result) => (
<RaceResultRow
key={result.driverId}
result={result as unknown as never}
points={viewData.pointsSystem[result.position.toString()] ?? 0}
/>
))}
</Stack>
{/* Penalties Section */}
{viewData.penalties.length > 0 && (
<Box pt={6} borderTop="1px solid" borderColor="border-neutral-800">
<Box mb={4}>
<Heading level={2}>Penalties</Heading>
</Box>
<Stack gap={2}>
{viewData.penalties.map((penalty, index) => (
<RacePenaltyRow key={index} penalty={penalty as unknown as never} />
))}
</Stack>
</Box>
)}
</Stack>
) : (
<Stack gap={6}>
<Box>
<Heading level={2}>Import Results</Heading>
<Text size="sm" color="text-gray-400" block mt={2}>
No results imported. Upload CSV to test the standings system.
</Text>
</Box>
{importing ? (
<Stack align="center" py={8}>
<Text color="text-gray-400">Importing results and updating standings...</Text>
</Stack>
) : (
<Stack gap={4}>
<Text size="sm" color="text-gray-400">
This is a placeholder for the import form. In the actual implementation,
this would render the ImportResultsForm component.
</Text>
<Box>
<Button
variant="primary"
onClick={() => onImportResults([])}
disabled={importing}
>
Import Results (Demo)
</Button>
</Box>
</Stack>
)}
</Stack>
)}
</Card>
</Stack>
</Container>
);
}
function StatItem({ label, value, icon, color = 'text-white' }: { label: string, value: string | number, icon?: LucideIcon, color?: string }) {
return (
<Surface variant="muted" rounded="lg" padding={3} bg="bg-neutral-900/60">
<Text size="xs" color="text-gray-500" block mb={1}>{label}</Text>
<Stack direction="row" align="center" gap={1.5}>
{icon && <Icon icon={icon} size={4} color={color === 'text-white' ? '#9ca3af' : color} />}
<Text weight="bold" color={color} size="lg">{value}</Text>
</Stack>
</Surface>
);
}