website refactor

This commit is contained in:
2026-01-14 23:31:57 +01:00
parent fbae5e6185
commit c1a86348d7
93 changed files with 7268 additions and 9088 deletions

View File

@@ -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>
);
}