website refactor
This commit is contained in:
@@ -1,44 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Breadcrumbs from '@/components/layout/Breadcrumbs';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Card from '@/components/ui/Card';
|
||||
import { ArrowLeft, Calendar, Trophy, Users, Zap } from 'lucide-react';
|
||||
|
||||
export interface ResultEntry {
|
||||
position: number;
|
||||
driverId: string;
|
||||
driverName: string;
|
||||
driverAvatar: string;
|
||||
country: string;
|
||||
car: string;
|
||||
laps: number;
|
||||
time: string;
|
||||
fastestLap: string;
|
||||
points: number;
|
||||
incidents: number;
|
||||
isCurrentUser: boolean;
|
||||
}
|
||||
|
||||
export interface PenaltyEntry {
|
||||
driverId: string;
|
||||
driverName: string;
|
||||
type: 'time_penalty' | 'grid_penalty' | 'points_deduction' | 'disqualification' | 'warning' | 'license_points';
|
||||
value: number;
|
||||
reason: string;
|
||||
notes?: string;
|
||||
}
|
||||
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 {
|
||||
raceTrack?: string;
|
||||
raceScheduledAt?: string;
|
||||
totalDrivers?: number;
|
||||
leagueName?: string;
|
||||
raceSOF?: number | null;
|
||||
results: ResultEntry[];
|
||||
penalties: PenaltyEntry[];
|
||||
pointsSystem: Record<string, number>;
|
||||
fastestLapTime: number;
|
||||
viewData: RaceResultsViewData;
|
||||
currentDriverId: string;
|
||||
isAdmin: boolean;
|
||||
isLoading: boolean;
|
||||
@@ -56,27 +36,15 @@ export interface RaceResultsTemplateProps {
|
||||
}
|
||||
|
||||
export function RaceResultsTemplate({
|
||||
raceTrack,
|
||||
raceScheduledAt,
|
||||
totalDrivers,
|
||||
leagueName,
|
||||
raceSOF,
|
||||
results,
|
||||
penalties,
|
||||
pointsSystem,
|
||||
fastestLapTime,
|
||||
viewData,
|
||||
currentDriverId,
|
||||
isAdmin,
|
||||
isLoading,
|
||||
error,
|
||||
onBack,
|
||||
onImportResults,
|
||||
onPenaltyClick,
|
||||
importing,
|
||||
importSuccess,
|
||||
importError,
|
||||
showImportForm,
|
||||
setShowImportForm,
|
||||
}: RaceResultsTemplateProps) {
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleDateString('en-US', {
|
||||
@@ -94,270 +62,167 @@ export function RaceResultsTemplate({
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const getCountryFlag = (countryCode: string): string => {
|
||||
const codePoints = countryCode
|
||||
.toUpperCase()
|
||||
.split('')
|
||||
.map(char => 127397 + char.charCodeAt(0));
|
||||
return String.fromCodePoint(...codePoints);
|
||||
};
|
||||
|
||||
const breadcrumbItems = [
|
||||
{ label: 'Races', href: '/races' },
|
||||
...(leagueName ? [{ label: leagueName, href: `/leagues/${leagueName}` }] : []),
|
||||
...(raceTrack ? [{ label: raceTrack, href: `/races/${raceTrack}` }] : []),
|
||||
...(viewData.leagueName ? [{ label: viewData.leagueName, href: `/leagues/${viewData.leagueName}` }] : []),
|
||||
...(viewData.raceTrack ? [{ label: viewData.raceTrack, href: `/races/${viewData.raceTrack}` }] : []),
|
||||
{ label: 'Results' },
|
||||
];
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-deep-graphite py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center text-gray-400">Loading results...</div>
|
||||
</div>
|
||||
</div>
|
||||
<Container size="lg" py={12}>
|
||||
<Stack align="center">
|
||||
<Text color="text-gray-400">Loading results...</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (error && !raceTrack) {
|
||||
if (error && !viewData.raceTrack) {
|
||||
return (
|
||||
<div className="min-h-screen bg-deep-graphite py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<Card className="text-center py-12">
|
||||
<div className="text-warning-amber mb-4">
|
||||
{error?.message || 'Race not found'}
|
||||
</div>
|
||||
<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>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const hasResults = results.length > 0;
|
||||
const hasResults = viewData.results.length > 0;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-deep-graphite py-8 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-6xl mx-auto space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<Breadcrumbs items={breadcrumbItems} className="text-sm text-gray-400" />
|
||||
<Container size="lg" py={8}>
|
||||
<Stack gap={6}>
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Breadcrumbs items={breadcrumbItems} />
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onBack}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
icon={<Icon icon={ArrowLeft} size={4} />}
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
{/* Header */}
|
||||
<Card className="bg-gradient-to-r from-iron-gray/50 to-iron-gray/30">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-primary-blue/20 flex items-center justify-center">
|
||||
<Trophy className="w-6 h-6 text-primary-blue" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">Race Results</h1>
|
||||
<p className="text-sm text-gray-400">
|
||||
{raceTrack} • {raceScheduledAt ? formatDate(raceScheduledAt) : ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<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 */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="p-3 bg-deep-graphite/60 rounded-lg">
|
||||
<p className="text-xs text-gray-400 mb-1">Drivers</p>
|
||||
<p className="text-lg font-bold text-white">{totalDrivers ?? 0}</p>
|
||||
</div>
|
||||
<div className="p-3 bg-deep-graphite/60 rounded-lg">
|
||||
<p className="text-xs text-gray-400 mb-1">League</p>
|
||||
<p className="text-sm font-medium text-white truncate">{leagueName ?? '—'}</p>
|
||||
</div>
|
||||
<div className="p-3 bg-deep-graphite/60 rounded-lg">
|
||||
<p className="text-xs text-gray-400 mb-1">SOF</p>
|
||||
<p className="text-lg font-bold text-warning-amber flex items-center gap-1">
|
||||
<Zap className="w-4 h-4" />
|
||||
{raceSOF ?? '—'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-deep-graphite/60 rounded-lg">
|
||||
<p className="text-xs text-gray-400 mb-1">Fastest Lap</p>
|
||||
<p className="text-lg font-bold text-performance-green">
|
||||
{fastestLapTime ? formatTime(fastestLapTime) : '—'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<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 && (
|
||||
<div className="p-4 bg-performance-green/10 border border-performance-green/30 rounded-lg text-performance-green">
|
||||
<strong>Success!</strong> Results imported and standings updated.
|
||||
</div>
|
||||
<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 && (
|
||||
<div className="p-4 bg-warning-amber/10 border border-warning-amber/30 rounded-lg text-warning-amber">
|
||||
<strong>Error:</strong> {importError}
|
||||
</div>
|
||||
<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 ? (
|
||||
<div className="space-y-4">
|
||||
<Stack gap={6}>
|
||||
{/* Results Table */}
|
||||
<div className="space-y-2">
|
||||
{results.map((result) => {
|
||||
const isCurrentUser = result.driverId === currentDriverId;
|
||||
const countryFlag = getCountryFlag(result.country);
|
||||
const points = pointsSystem[result.position.toString()] ?? 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={result.driverId}
|
||||
className={`
|
||||
flex items-center gap-3 p-3 rounded-xl
|
||||
${isCurrentUser ? 'bg-gradient-to-r from-primary-blue/20 via-primary-blue/10 to-transparent border border-primary-blue/40' : 'bg-deep-graphite'}
|
||||
`}
|
||||
>
|
||||
{/* Position */}
|
||||
<div className={`
|
||||
flex items-center justify-center w-10 h-10 rounded-lg font-bold
|
||||
${result.position === 1 ? 'bg-yellow-500/20 text-yellow-400' :
|
||||
result.position === 2 ? 'bg-gray-400/20 text-gray-300' :
|
||||
result.position === 3 ? 'bg-amber-600/20 text-amber-500' :
|
||||
'bg-iron-gray text-gray-500'}
|
||||
`}>
|
||||
{result.position}
|
||||
</div>
|
||||
|
||||
{/* Avatar */}
|
||||
<div className="relative flex-shrink-0">
|
||||
<img
|
||||
src={result.driverAvatar}
|
||||
alt={result.driverName}
|
||||
className={`w-10 h-10 rounded-full object-cover ${isCurrentUser ? 'ring-2 ring-primary-blue/50' : ''}`}
|
||||
/>
|
||||
<div className="absolute -bottom-0.5 -right-0.5 w-5 h-5 rounded-full bg-deep-graphite border-2 border-deep-graphite flex items-center justify-center text-xs shadow-sm">
|
||||
{countryFlag}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Driver Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className={`text-sm font-semibold truncate ${isCurrentUser ? 'text-primary-blue' : 'text-white'}`}>
|
||||
{result.driverName}
|
||||
</p>
|
||||
{isCurrentUser && (
|
||||
<span className="px-2 py-0.5 text-[10px] font-bold bg-primary-blue text-white rounded-full uppercase tracking-wide">
|
||||
You
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs text-gray-400 mt-0.5">
|
||||
<span>{result.car}</span>
|
||||
<span>•</span>
|
||||
<span>Laps: {result.laps}</span>
|
||||
<span>•</span>
|
||||
<span>Incidents: {result.incidents}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Times */}
|
||||
<div className="text-right min-w-[100px]">
|
||||
<p className="text-sm font-mono text-white">{result.time}</p>
|
||||
<p className="text-xs text-performance-green">FL: {result.fastestLap}</p>
|
||||
</div>
|
||||
|
||||
{/* Points */}
|
||||
<div className="flex-shrink-0">
|
||||
<div className="flex flex-col items-center px-3 py-1 rounded-lg bg-warning-amber/10 border border-warning-amber/20">
|
||||
<span className="text-xs text-gray-400">PTS</span>
|
||||
<span className="text-sm font-bold text-warning-amber">{points}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<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 */}
|
||||
{penalties.length > 0 && (
|
||||
<div className="mt-6 pt-6 border-t border-charcoal-outline">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Penalties</h3>
|
||||
<div className="space-y-2">
|
||||
{penalties.map((penalty, index) => (
|
||||
<div key={index} className="flex items-center gap-3 p-3 bg-deep-graphite rounded-lg">
|
||||
<div className="w-10 h-10 rounded-full bg-red-500/20 flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-red-400 font-bold text-sm">!</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-medium text-white">{penalty.driverName}</span>
|
||||
<span className="px-2 py-0.5 text-xs font-medium bg-red-500/20 text-red-400 rounded-full">
|
||||
{penalty.type.replace('_', ' ')}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400">{penalty.reason}</p>
|
||||
{penalty.notes && (
|
||||
<p className="text-sm text-gray-500 mt-1 italic">{penalty.notes}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-2xl font-bold text-red-400">
|
||||
{penalty.type === 'time_penalty' && `+${penalty.value}s`}
|
||||
{penalty.type === 'grid_penalty' && `+${penalty.value} grid`}
|
||||
{penalty.type === 'points_deduction' && `-${penalty.value} pts`}
|
||||
{penalty.type === 'disqualification' && 'DSQ'}
|
||||
{penalty.type === 'warning' && 'Warning'}
|
||||
{penalty.type === 'license_points' && `${penalty.value} LP`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{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} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
) : (
|
||||
<>
|
||||
<h2 className="text-xl font-semibold text-white mb-6">Import Results</h2>
|
||||
<p className="text-gray-400 text-sm mb-6">
|
||||
No results imported. Upload CSV to test the standings system.
|
||||
</p>
|
||||
<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 ? (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
Importing results and updating standings...
|
||||
</div>
|
||||
<Stack align="center" py={8}>
|
||||
<Text color="text-gray-400">Importing results and updating standings...</Text>
|
||||
</Stack>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-gray-400">
|
||||
<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.
|
||||
</p>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
// Mock import for demo
|
||||
onImportResults([]);
|
||||
}}
|
||||
disabled={importing}
|
||||
>
|
||||
Import Results (Demo)
|
||||
</Button>
|
||||
</div>
|
||||
</Text>
|
||||
<Box>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => onImportResults([])}
|
||||
disabled={importing}
|
||||
>
|
||||
Import Results (Demo)
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
</Stack>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user