Files
gridpilot.gg/apps/website/templates/RaceResultsTemplate.tsx
2026-01-05 19:35:49 +01:00

363 lines
14 KiB
TypeScript

'use client';
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;
}
export interface RaceResultsTemplateProps {
raceTrack?: string;
raceScheduledAt?: string;
totalDrivers?: number;
leagueName?: string;
raceSOF?: number | null;
results: ResultEntry[];
penalties: PenaltyEntry[];
pointsSystem: Record<string, number>;
fastestLapTime: number;
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({
raceTrack,
raceScheduledAt,
totalDrivers,
leagueName,
raceSOF,
results,
penalties,
pointsSystem,
fastestLapTime,
currentDriverId,
isAdmin,
isLoading,
error,
onBack,
onImportResults,
onPenaltyClick,
importing,
importSuccess,
importError,
showImportForm,
setShowImportForm,
}: 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 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}` }] : []),
{ 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>
);
}
if (error && !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>
<Button
variant="secondary"
onClick={onBack}
>
Back to Races
</Button>
</Card>
</div>
</div>
);
}
const hasResults = 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" />
<Button
variant="secondary"
onClick={onBack}
className="flex items-center gap-2 text-sm"
>
<ArrowLeft className="w-4 h-4" />
Back
</Button>
</div>
{/* 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>
{/* 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>
{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>
)}
{importError && (
<div className="p-4 bg-warning-amber/10 border border-warning-amber/30 rounded-lg text-warning-amber">
<strong>Error:</strong> {importError}
</div>
)}
<Card>
{hasResults ? (
<div className="space-y-4">
{/* 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>
{/* 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>
))}
</div>
</div>
)}
</div>
) : (
<>
<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>
{importing ? (
<div className="text-center py-8 text-gray-400">
Importing results and updating standings...
</div>
) : (
<div className="space-y-4">
<p className="text-sm 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>
)}
</>
)}
</Card>
</div>
</div>
);
}