229 lines
8.1 KiB
TypeScript
229 lines
8.1 KiB
TypeScript
'use client';
|
|
|
|
import React from 'react';
|
|
import Breadcrumbs from '@/components/layout/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 } from 'lucide-react';
|
|
import type { RaceResultsViewData } from '@/lib/view-data/races/RaceResultsViewData';
|
|
import { RaceResultRow } from '@/components/races/RaceResultRow';
|
|
import { RacePenaltyRow } from '@/components/races/RacePenaltyRow';
|
|
|
|
export interface RaceResultsTemplateProps {
|
|
viewData: RaceResultsViewData;
|
|
currentDriverId: string;
|
|
isAdmin: boolean;
|
|
isLoading: boolean;
|
|
error?: Error | null;
|
|
// Actions
|
|
onBack: () => void;
|
|
onImportResults: (results: any[]) => 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,
|
|
currentDriverId,
|
|
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} style={{ background: 'linear-gradient(to right, rgba(38, 38, 38, 0.5), rgba(38, 38, 38, 0.3))', borderColor: '#262626' }}>
|
|
<Stack direction="row" align="center" gap={4} mb={6}>
|
|
<Surface variant="muted" rounded="xl" padding={3} style={{ backgroundColor: 'rgba(59, 130, 246, 0.2)' }}>
|
|
<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="#f59e0b" />
|
|
<StatItem label="Fastest Lap" value={viewData.fastestLapTime ? formatTime(viewData.fastestLapTime) : '—'} color="#10b981" />
|
|
</Grid>
|
|
</Surface>
|
|
|
|
{importSuccess && (
|
|
<Surface variant="muted" rounded="lg" border padding={4} style={{ backgroundColor: 'rgba(16, 185, 129, 0.1)', borderColor: 'rgba(16, 185, 129, 0.3)' }}>
|
|
<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} style={{ backgroundColor: 'rgba(239, 68, 68, 0.1)', borderColor: 'rgba(239, 68, 68, 0.3)' }}>
|
|
<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 any}
|
|
points={viewData.pointsSystem[result.position.toString()] ?? 0}
|
|
/>
|
|
))}
|
|
</Stack>
|
|
|
|
{/* Penalties Section */}
|
|
{viewData.penalties.length > 0 && (
|
|
<Box pt={6} style={{ borderTop: '1px solid #262626' }}>
|
|
<Box mb={4}>
|
|
<Heading level={2}>Penalties</Heading>
|
|
</Box>
|
|
<Stack gap={2}>
|
|
{viewData.penalties.map((penalty, index) => (
|
|
<RacePenaltyRow key={index} penalty={penalty as any} />
|
|
))}
|
|
</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?: any, color?: string }) {
|
|
return (
|
|
<Surface variant="muted" rounded="lg" padding={3} style={{ backgroundColor: 'rgba(15, 17, 21, 0.6)' }}>
|
|
<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 as any} style={{ fontSize: '1.125rem' }}>{value}</Text>
|
|
</Stack>
|
|
</Surface>
|
|
);
|
|
}
|