website refactor

This commit is contained in:
2026-01-14 23:46:04 +01:00
parent c1a86348d7
commit 4a2d7d15a5
294 changed files with 5637 additions and 3418 deletions

View File

@@ -1,8 +1,8 @@
'use client';
import { useState } from 'react';
import Modal from '@/components/ui/Modal';
import Button from '@/components/ui/Button';
import Modal from '@/ui/Modal';
import Button from '@/ui/Button';
import type { FileProtestCommandDTO } from '@/lib/types/generated/FileProtestCommandDTO';
import { useFileProtest } from "@/lib/hooks/race/useFileProtest";
import {

View File

@@ -4,11 +4,10 @@ import { useState } from 'react';
import Button from '../ui/Button';
import { useInject } from '@/lib/di/hooks/useInject';
import { RACE_RESULTS_SERVICE_TOKEN } from '@/lib/di/tokens';
import type { ImportResultRowDTO } from '@/lib/services/races/RaceResultsService';
interface ImportResultsFormProps {
raceId: string;
onSuccess: (results: ImportResultRowDTO[]) => void;
onSuccess: (results: any[]) => void;
onError: (error: string) => void;
}
@@ -26,7 +25,7 @@ export default function ImportResultsForm({ raceId, onSuccess, onError }: Import
try {
const content = await file.text();
const results = raceResultsService.parseAndTransformCSV(content, raceId);
const results = (raceResultsService as any).parseAndTransformCSV(content, raceId);
onSuccess(results);
} catch (err) {
const errorMessage =

View File

@@ -1,4 +1,4 @@
import Card from '@/components/ui/Card';
import Card from '@/ui/Card';
type RaceWithResults = {
raceId: string;

View File

@@ -0,0 +1,68 @@
import React from 'react';
import { PlayCircle, ChevronRight } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import type { RaceViewData } from '@/lib/view-data/RacesViewData';
interface LiveRacesBannerProps {
liveRaces: RaceViewData[];
onRaceClick: (raceId: string) => void;
}
export function LiveRacesBanner({ liveRaces, onRaceClick }: LiveRacesBannerProps) {
if (liveRaces.length === 0) return null;
return (
<Box style={{
position: 'relative',
overflow: 'hidden',
borderRadius: '0.75rem',
background: 'linear-gradient(to right, rgba(16, 185, 129, 0.2), rgba(16, 185, 129, 0.1), transparent)',
border: '1px solid rgba(16, 185, 129, 0.3)',
padding: '1.5rem'
}}>
<Box style={{ position: 'absolute', top: 0, right: 0, width: '8rem', height: '8rem', backgroundColor: 'rgba(16, 185, 129, 0.2)', borderRadius: '9999px', filter: 'blur(24px)' }} />
<Box style={{ position: 'relative', zIndex: 10 }}>
<Box style={{ marginBottom: '1rem' }}>
<Stack direction="row" align="center" gap={2} style={{ backgroundColor: 'rgba(16, 185, 129, 0.2)', padding: '0.25rem 0.75rem', borderRadius: '9999px', width: 'fit-content' }}>
<Box style={{ width: '0.5rem', height: '0.5rem', backgroundColor: '#10b981', borderRadius: '9999px' }} />
<Text weight="semibold" size="sm" color="text-performance-green">LIVE NOW</Text>
</Stack>
</Box>
<Stack gap={3}>
{liveRaces.map((race) => (
<Box
key={race.id}
onClick={() => onRaceClick(race.id)}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '1rem',
backgroundColor: 'rgba(15, 17, 21, 0.8)',
borderRadius: '0.5rem',
border: '1px solid rgba(16, 185, 129, 0.2)',
cursor: 'pointer'
}}
>
<Stack direction="row" align="center" gap={4}>
<Box style={{ padding: '0.5rem', backgroundColor: 'rgba(16, 185, 129, 0.2)', borderRadius: '0.5rem' }}>
<PlayCircle style={{ width: '1.25rem', height: '1.25rem', color: '#10b981' }} />
</Box>
<Box>
<Heading level={3}>{race.track}</Heading>
<Text size="sm" color="text-gray-400">{race.leagueName}</Text>
</Box>
</Stack>
<ChevronRight style={{ width: '1.25rem', height: '1.25rem', color: '#9ca3af' }} />
</Box>
))}
</Stack>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,124 @@
'use client';
import React from 'react';
import { AlertCircle, AlertTriangle, Video } from 'lucide-react';
import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Link } from '@/ui/Link';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Surface } from '@/ui/Surface';
interface Protest {
id: string;
status: string;
protestingDriverId: string;
accusedDriverId: string;
filedAt: string;
incident: {
lap: number;
description: string;
};
proofVideoUrl?: string;
decisionNotes?: string;
}
interface Driver {
id: string;
name: string;
}
interface ProtestCardProps {
protest: Protest;
protester?: Driver;
accused?: Driver;
isAdmin: boolean;
onReview: (id: string) => void;
formatDate: (date: string) => string;
}
export function ProtestCard({ protest, protester, accused, isAdmin, onReview, formatDate }: ProtestCardProps) {
const daysSinceFiled = Math.floor(
(Date.now() - new Date(protest.filedAt).getTime()) / (1000 * 60 * 60 * 24)
);
const isUrgent = daysSinceFiled > 2 && protest.status === 'pending';
const getStatusBadge = (status: string) => {
const variants: Record<string, { bg: string, text: string, label: string }> = {
pending: { bg: 'rgba(245, 158, 11, 0.2)', text: '#f59e0b', label: 'Pending' },
under_review: { bg: 'rgba(245, 158, 11, 0.2)', text: '#f59e0b', label: 'Pending' },
upheld: { bg: 'rgba(239, 68, 68, 0.2)', text: '#ef4444', label: 'Upheld' },
dismissed: { bg: 'rgba(115, 115, 115, 0.2)', text: '#9ca3af', label: 'Dismissed' },
withdrawn: { bg: 'rgba(59, 130, 246, 0.2)', text: '#3b82f6', label: 'Withdrawn' },
};
const config = variants[status] || variants.pending;
return (
<Surface variant="muted" rounded="full" padding={1} style={{ backgroundColor: config.bg, paddingLeft: '0.5rem', paddingRight: '0.5rem' }}>
<Text size="xs" weight="medium" style={{ color: config.text }}>{config.label}</Text>
</Surface>
);
};
return (
<Card style={{ borderLeft: isUrgent ? '4px solid #ef4444' : undefined }}>
<Stack direction="row" align="start" justify="between" gap={4}>
<Box style={{ flex: 1, minWidth: 0 }}>
<Stack direction="row" align="center" gap={2} mb={2} wrap>
<Icon icon={AlertCircle} size={4} color="#9ca3af" />
<Link href={`/drivers/${protest.protestingDriverId}`}>
<Text weight="medium" color="text-white">{protester?.name || 'Unknown'}</Text>
</Link>
<Text size="sm" color="text-gray-500">vs</Text>
<Link href={`/drivers/${protest.accusedDriverId}`}>
<Text weight="medium" color="text-white">{accused?.name || 'Unknown'}</Text>
</Link>
{getStatusBadge(protest.status)}
{isUrgent && (
<Surface variant="muted" rounded="full" padding={1} style={{ backgroundColor: 'rgba(239, 68, 68, 0.2)', paddingLeft: '0.5rem', paddingRight: '0.5rem' }}>
<Stack direction="row" align="center" gap={1}>
<Icon icon={AlertTriangle} size={3} color="#ef4444" />
<Text size="xs" weight="medium" color="text-error-red">{daysSinceFiled}d old</Text>
</Stack>
</Surface>
)}
</Stack>
<Stack direction="row" align="center" gap={4} mb={2} wrap>
<Text size="sm" color="text-gray-400">Lap {protest.incident.lap}</Text>
<Text size="sm" color="text-gray-400"></Text>
<Text size="sm" color="text-gray-400">Filed {formatDate(protest.filedAt)}</Text>
{protest.proofVideoUrl && (
<>
<Text size="sm" color="text-gray-400"></Text>
<Link href={protest.proofVideoUrl} target="_blank">
<Stack direction="row" align="center" gap={1.5}>
<Icon icon={Video} size={3.5} color="#3b82f6" />
<Text size="sm" color="text-primary-blue">Video Evidence</Text>
</Stack>
</Link>
</>
)}
</Stack>
<Text size="sm" color="text-gray-300" block>{protest.incident.description}</Text>
{protest.decisionNotes && (
<Box mt={4} p={3} style={{ backgroundColor: 'rgba(38, 38, 38, 0.5)', borderRadius: '0.5rem', border: '1px solid #262626' }}>
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }} block mb={1}>Steward Decision</Text>
<Text size="sm" color="text-gray-300">{protest.decisionNotes}</Text>
</Box>
)}
</Box>
{isAdmin && protest.status === 'pending' && (
<Button
variant="primary"
onClick={() => onReview(protest.id)}
size="sm"
>
Review
</Button>
)}
</Stack>
</Card>
);
}

View File

@@ -1,6 +1,5 @@
import Link from 'next/link';
import { ChevronRight, Car, Zap, Trophy, ArrowRight } from 'lucide-react';
import { formatTime, getRelativeTime } from '@/lib/utilities/time';
import { raceStatusConfig } from '@/lib/utilities/raceStatus';
interface RaceCardProps {
@@ -19,6 +18,7 @@ interface RaceCardProps {
}
export function RaceCard({ race, onClick, className }: RaceCardProps) {
const scheduledAtDate = new Date(race.scheduledAt);
const config = raceStatusConfig[race.status as keyof typeof raceStatusConfig] || {
border: 'border-charcoal-outline',
bg: 'bg-charcoal-outline',
@@ -41,12 +41,12 @@ export function RaceCard({ race, onClick, className }: RaceCardProps) {
{/* Time Column */}
<div className="flex-shrink-0 text-center min-w-[60px]">
<p className="text-lg font-bold text-white">
{formatTime(race.scheduledAt)}
{scheduledAtDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</p>
<p className={`text-xs ${config.color}`}>
{race.status === 'running'
? 'LIVE'
: getRelativeTime(race.scheduledAt)}
: scheduledAtDate.toLocaleDateString()}
</p>
</div>

View File

@@ -0,0 +1,44 @@
'use client';
import React from 'react';
import { Flag } from 'lucide-react';
import { Card } from '@/ui/Card';
import { Stack } from '@/ui/Stack';
import { Heading } from '@/ui/Heading';
import { Grid } from '@/ui/Grid';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
interface RaceDetailCardProps {
track: string;
car: string;
sessionType: string;
statusLabel: string;
statusColor: string;
}
export function RaceDetailCard({ track, car, sessionType, statusLabel, statusColor }: RaceDetailCardProps) {
return (
<Card>
<Stack gap={4}>
<Heading level={2} icon={<Icon icon={Flag} size={5} color="#3b82f6" />}>Race Details</Heading>
<Grid cols={2} gap={4}>
<DetailItem label="Track" value={track} />
<DetailItem label="Car" value={car} />
<DetailItem label="Session Type" value={sessionType} capitalize />
<DetailItem label="Status" value={statusLabel} color={statusColor} />
</Grid>
</Stack>
</Card>
);
}
function DetailItem({ label, value, capitalize, color }: { label: string, value: string | number, capitalize?: boolean, color?: string }) {
return (
<Box p={4} style={{ backgroundColor: '#0f1115', borderRadius: '0.5rem' }}>
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }} block mb={1}>{label}</Text>
<Text weight="medium" color="text-white" style={{ textTransform: capitalize ? 'capitalize' : 'none', color: color || 'white' }}>{value}</Text>
</Box>
);
}

View File

@@ -0,0 +1,88 @@
'use client';
import React from 'react';
import { Users, Zap } from 'lucide-react';
import { Card } from '@/ui/Card';
import { Stack } from '@/ui/Stack';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Image } from '@/ui/Image';
import { Badge } from '@/ui/Badge';
import { Icon } from '@/ui/Icon';
import { Surface } from '@/ui/Surface';
import { CountryFlagDisplay } from '@/lib/display-objects/CountryFlagDisplay';
interface Entry {
id: string;
name: string;
avatarUrl: string;
country: string;
rating?: number | null;
isCurrentUser: boolean;
}
interface RaceEntryListProps {
entries: Entry[];
onDriverClick: (driverId: string) => void;
}
export function RaceEntryList({ entries, onDriverClick }: RaceEntryListProps) {
return (
<Card>
<Stack gap={4}>
<Stack direction="row" align="center" justify="between">
<Heading level={2} icon={<Icon icon={Users} size={5} color="#3b82f6" />}>Entry List</Heading>
<Text size="sm" color="text-gray-400">{entries.length} drivers</Text>
</Stack>
{entries.length === 0 ? (
<Stack center py={8} gap={3}>
<Surface variant="muted" rounded="full" padding={4}>
<Icon icon={Users} size={6} color="#525252" />
</Surface>
<Text color="text-gray-400">No drivers registered yet</Text>
</Stack>
) : (
<Stack gap={1}>
{entries.map((driver, index) => (
<Box
key={driver.id}
onClick={() => onDriverClick(driver.id)}
style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', padding: '0.75rem', borderRadius: '0.75rem', cursor: 'pointer', transition: 'all 0.2s', backgroundColor: driver.isCurrentUser ? 'rgba(59, 130, 246, 0.1)' : 'transparent', border: driver.isCurrentUser ? '1px solid rgba(59, 130, 246, 0.3)' : '1px solid transparent' }}
>
<Box style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '2rem', height: '2rem', borderRadius: '0.5rem', fontWeight: 'bold', fontSize: '0.875rem', backgroundColor: '#262626', color: '#737373' }}>
{index + 1}
</Box>
<Box style={{ position: 'relative', flexShrink: 0 }}>
<Box style={{ width: '2.5rem', height: '2.5rem', borderRadius: '9999px', overflow: 'hidden', border: driver.isCurrentUser ? '2px solid #3b82f6' : 'none' }}>
<Image src={driver.avatarUrl} alt={driver.name} width={40} height={40} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
</Box>
<Box style={{ position: 'absolute', bottom: '-0.125rem', right: '-0.125rem', width: '1.25rem', height: '1.25rem', borderRadius: '9999px', backgroundColor: '#0f1115', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '0.625rem' }}>
{CountryFlagDisplay.fromCountryCode(driver.country).toString()}
</Box>
</Box>
<Box style={{ flex: 1, minWidth: 0 }}>
<Stack direction="row" align="center" gap={2}>
<Text weight="semibold" size="sm" color={driver.isCurrentUser ? 'text-primary-blue' : 'text-white'} truncate>{driver.name}</Text>
{driver.isCurrentUser && <Badge variant="primary">You</Badge>}
</Stack>
<Text size="xs" color="text-gray-500">{driver.country}</Text>
</Box>
{driver.rating != null && (
<Badge variant="warning">
<Icon icon={Zap} size={3} />
{driver.rating}
</Badge>
)}
</Box>
))}
</Stack>
)}
</Stack>
</Card>
);
}

View File

@@ -0,0 +1,76 @@
import React from 'react';
import { Card } from '@/ui/Card';
import { Button } from '@/ui/Button';
import { Stack } from '@/ui/Stack';
import { Select } from '@/ui/Select';
import { Box } from '@/ui/Box';
import type { TimeFilter } from '@/templates/RacesTemplate';
interface RaceFilterBarProps {
timeFilter: TimeFilter;
setTimeFilter: (filter: TimeFilter) => void;
leagueFilter: string;
setLeagueFilter: (filter: string) => void;
leagues: Array<{ id: string; name: string }>;
onShowMoreFilters: () => void;
}
export function RaceFilterBar({
timeFilter,
setTimeFilter,
leagueFilter,
setLeagueFilter,
leagues,
onShowMoreFilters,
}: RaceFilterBarProps) {
const leagueOptions = [
{ value: 'all', label: 'All Leagues' },
...leagues.map(l => ({ value: l.id, label: l.name }))
];
return (
<Card style={{ padding: '1rem' }}>
<Stack direction="row" align="center" gap={4} wrap>
{/* Time Filter Tabs */}
<Stack direction="row" align="center" gap={1} style={{ backgroundColor: '#0f1115', padding: '0.25rem', borderRadius: '0.5rem' }}>
{(['upcoming', 'live', 'past', 'all'] as TimeFilter[]).map(filter => (
<Button
key={filter}
variant={timeFilter === filter ? 'primary' : 'ghost'}
onClick={() => setTimeFilter(filter)}
style={{ padding: '0.5rem 1rem' }}
>
{filter === 'live' && <Box as="span" style={{ display: 'inline-block', width: '0.5rem', height: '0.5rem', backgroundColor: '#10b981', borderRadius: '9999px', marginRight: '0.5rem' }} />}
{filter.charAt(0).toUpperCase() + filter.slice(1)}
</Button>
))}
</Stack>
{/* League Filter */}
<Box style={{ width: 'auto' }}>
<Select
value={leagueFilter}
onChange={(e) => setLeagueFilter(e.target.value)}
options={leagueOptions}
style={{
backgroundColor: '#0f1115',
border: '1px solid #262626',
borderRadius: '0.5rem',
color: 'white',
fontSize: '0.875rem',
width: 'auto'
}}
/>
</Box>
{/* Filter Button */}
<Button
variant="secondary"
onClick={onShowMoreFilters}
>
More Filters
</Button>
</Stack>
</Card>
);
}

View File

@@ -1,8 +1,8 @@
'use client';
import { X, Filter, Search } from 'lucide-react';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Button from '@/ui/Button';
import Card from '@/ui/Card';
export type TimeFilter = 'all' | 'upcoming' | 'live' | 'past';
export type StatusFilter = 'scheduled' | 'running' | 'completed' | 'cancelled' | 'all';

View File

@@ -0,0 +1,70 @@
'use client';
import React from 'react';
import { Calendar, Clock, Car, LucideIcon } from 'lucide-react';
import { Surface } from '@/ui/Surface';
import { Stack } from '@/ui/Stack';
import { Box } from '@/ui/Box';
import { Badge } from '@/ui/Badge';
import { Icon } from '@/ui/Icon';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { DecorativeBlur } from '@/ui/DecorativeBlur';
interface RaceHeroProps {
track: string;
scheduledAt: string;
car: string;
status: 'scheduled' | 'running' | 'completed' | 'cancelled';
statusConfig: {
icon: LucideIcon;
variant: 'primary' | 'success' | 'default' | 'warning';
label: string;
description: string;
};
}
export function RaceHero({ track, scheduledAt, car, status, statusConfig }: RaceHeroProps) {
const StatusIcon = statusConfig.icon;
return (
<Surface variant="muted" rounded="2xl" border padding={8} style={{ position: 'relative', overflow: 'hidden', backgroundColor: 'rgba(38, 38, 38, 0.3)' }}>
{status === 'running' && (
<Box style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '0.25rem', background: 'linear-gradient(to right, #10b981, rgba(16, 185, 129, 0.5), #10b981)' }} className="animate-pulse" />
)}
<DecorativeBlur color="blue" size="lg" position="top-right" opacity={5} />
<Box style={{ position: 'relative', zIndex: 10 }}>
<Stack direction="row" align="center" gap={3} mb={4}>
<Badge variant={statusConfig.variant}>
{status === 'running' && <Box style={{ width: '0.5rem', height: '0.5rem', borderRadius: '9999px', backgroundColor: '#10b981' }} className="animate-pulse" />}
<Icon icon={StatusIcon} size={4} />
{statusConfig.label}
</Badge>
{status === 'scheduled' && (
<Text size="sm" color="text-gray-400">
Starts in <Text color="text-white" weight="medium">TBD</Text>
</Text>
)}
</Stack>
<Heading level={1} style={{ fontSize: '2rem', marginBottom: '0.5rem' }}>{track}</Heading>
<Stack direction="row" align="center" gap={6} wrap className="text-gray-400">
<Stack direction="row" align="center" gap={2}>
<Icon icon={Calendar} size={4} />
<Text>{new Date(scheduledAt).toLocaleDateString()}</Text>
</Stack>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Clock} size={4} />
<Text>{new Date(scheduledAt).toLocaleTimeString()}</Text>
</Stack>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Car} size={4} />
<Text>{car}</Text>
</Stack>
</Stack>
</Box>
</Surface>
);
}

View File

@@ -1,7 +1,7 @@
'use client';
import { UserPlus, UserMinus, CheckCircle2, PlayCircle, XCircle } from 'lucide-react';
import Button from '@/components/ui/Button';
import Button from '@/ui/Button';
interface RaceJoinButtonProps {
raceStatus: 'scheduled' | 'running' | 'completed' | 'cancelled';

View File

@@ -0,0 +1,179 @@
import React from 'react';
import { Calendar, Clock, PlayCircle, CheckCircle2, XCircle, Car, Zap, Trophy, ArrowRight, ChevronRight } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Link } from '@/ui/Link';
import { Card } from '@/ui/Card';
import { Stack } from '@/ui/Stack';
import { Badge } from '@/ui/Badge';
import type { RaceViewData } from '@/lib/view-data/RacesViewData';
import { routes } from '@/lib/routing/RouteConfig';
interface RaceListProps {
racesByDate: Array<{
dateKey: string;
dateLabel: string;
races: RaceViewData[];
}>;
totalCount: number;
onRaceClick: (raceId: string) => void;
}
export function RaceList({ racesByDate, totalCount, onRaceClick }: RaceListProps) {
const statusConfig = {
scheduled: {
icon: Clock,
variant: 'primary' as const,
label: 'Scheduled',
},
running: {
icon: PlayCircle,
variant: 'success' as const,
label: 'LIVE',
},
completed: {
icon: CheckCircle2,
variant: 'default' as const,
label: 'Completed',
},
cancelled: {
icon: XCircle,
variant: 'warning' as const,
label: 'Cancelled',
},
};
if (racesByDate.length === 0) {
return (
<Card style={{ textAlign: 'center', padding: '3rem 0' }}>
<Stack align="center" gap={4}>
<Box style={{ padding: '1rem', backgroundColor: '#262626', borderRadius: '9999px' }}>
<Calendar style={{ width: '2rem', height: '2rem', color: '#737373' }} />
</Box>
<Box>
<Text weight="medium" color="text-white" style={{ marginBottom: '0.25rem' }}>No races found</Text>
<Text size="sm" color="text-gray-500">
{totalCount === 0
? 'No races have been scheduled yet'
: 'Try adjusting your filters'}
</Text>
</Box>
</Stack>
</Card>
);
}
return (
<Stack gap={4}>
{racesByDate.map((group) => (
<Stack key={group.dateKey} gap={3}>
{/* Date Header */}
<Stack direction="row" align="center" gap={3} style={{ padding: '0 0.5rem' }}>
<Box style={{ padding: '0.5rem', backgroundColor: 'rgba(59, 130, 246, 0.1)', borderRadius: '0.5rem' }}>
<Calendar style={{ width: '1rem', height: '1rem', color: '#3b82f6' }} />
</Box>
<Text weight="semibold" size="sm" color="text-white">
{group.dateLabel}
</Text>
<Text size="xs" color="text-gray-500">
{group.races.length} race{group.races.length !== 1 ? 's' : ''}
</Text>
</Stack>
{/* Races for this date */}
<Stack gap={2}>
{group.races.map((race) => {
const config = statusConfig[race.status as keyof typeof statusConfig] || statusConfig.scheduled;
const StatusIcon = config.icon;
return (
<Box
key={race.id}
onClick={() => onRaceClick(race.id)}
style={{
position: 'relative',
overflow: 'hidden',
borderRadius: '0.75rem',
backgroundColor: '#262626',
border: '1px solid rgba(38, 38, 38, 1)',
padding: '1rem',
cursor: 'pointer'
}}
>
{/* Live indicator */}
{race.status === 'running' && (
<Box style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '0.25rem', background: 'linear-gradient(to right, #10b981, rgba(16, 185, 129, 0.5), #10b981)' }} />
)}
<Stack direction="row" align="start" gap={4}>
{/* Time Column */}
<Box style={{ flexShrink: 0, textAlign: 'center', minWidth: '60px' }}>
<Text size="lg" weight="bold" color="text-white">
{race.timeLabel}
</Text>
<Text size="xs" style={{ color: race.status === 'running' ? '#10b981' : '#9ca3af' }}>
{race.status === 'running'
? 'LIVE'
: race.relativeTimeLabel}
</Text>
</Box>
{/* Divider */}
<Box style={{ width: '1px', alignSelf: 'stretch', backgroundColor: 'rgba(38, 38, 38, 1)' }} />
{/* Main Content */}
<Box style={{ flex: 1, minWidth: 0 }}>
<Stack direction="row" align="start" justify="between" gap={4}>
<Box style={{ minWidth: 0 }}>
<Heading level={3} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{race.track}
</Heading>
<Stack direction="row" align="center" gap={3} style={{ marginTop: '0.25rem' }}>
<Stack direction="row" align="center" gap={1}>
<Car style={{ width: '0.875rem', height: '0.875rem', color: '#9ca3af' }} />
<Text size="sm" color="text-gray-400">{race.car}</Text>
</Stack>
{race.strengthOfField && (
<Stack direction="row" align="center" gap={1}>
<Zap style={{ width: '0.875rem', height: '0.875rem', color: '#f59e0b' }} />
<Text size="sm" color="text-gray-400">SOF {race.strengthOfField}</Text>
</Stack>
)}
</Stack>
</Box>
{/* Status Badge */}
<Badge variant={config.variant}>
<StatusIcon style={{ width: '0.875rem', height: '0.875rem' }} />
{config.label}
</Badge>
</Stack>
{/* League Link */}
<Box style={{ marginTop: '0.75rem', paddingTop: '0.75rem', borderTop: '1px solid rgba(38, 38, 38, 0.5)' }}>
<Link
href={routes.league.detail(race.leagueId ?? '')}
onClick={(e) => e.stopPropagation()}
variant="primary"
style={{ fontSize: '0.875rem' }}
>
<Trophy style={{ width: '0.875rem', height: '0.875rem', marginRight: '0.5rem' }} />
{race.leagueName}
<ArrowRight style={{ width: '0.75rem', height: '0.75rem', marginLeft: '0.5rem' }} />
</Link>
</Box>
</Box>
{/* Arrow */}
<ChevronRight style={{ width: '1.25rem', height: '1.25rem', color: '#737373', flexShrink: 0 }} />
</Stack>
</Box>
);
})}
</Stack>
</Stack>
))}
</Stack>
);
}

View File

@@ -0,0 +1,145 @@
'use client';
import React from 'react';
import { Calendar, Clock, Car, Trophy, Zap, ChevronRight, PlayCircle, CheckCircle2, XCircle } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Link } from '@/ui/Link';
import { Icon } from '@/ui/Icon';
import { Surface } from '@/ui/Surface';
interface Race {
id: string;
track: string;
car: string;
scheduledAt: string;
status: 'scheduled' | 'running' | 'completed' | 'cancelled';
sessionType: string;
leagueId?: string;
leagueName?: string;
strengthOfField?: number | null;
}
interface RaceListItemProps {
race: Race;
onClick: (id: string) => void;
}
export function RaceListItem({ race, onClick }: RaceListItemProps) {
const statusConfig = {
scheduled: {
icon: Clock,
color: '#3b82f6',
bg: 'rgba(59, 130, 246, 0.1)',
border: 'rgba(59, 130, 246, 0.3)',
label: 'Scheduled',
},
running: {
icon: PlayCircle,
color: '#10b981',
bg: 'rgba(16, 185, 129, 0.1)',
border: 'rgba(16, 185, 129, 0.3)',
label: 'LIVE',
},
completed: {
icon: CheckCircle2,
color: '#9ca3af',
bg: 'rgba(156, 163, 175, 0.1)',
border: 'rgba(156, 163, 175, 0.3)',
label: 'Completed',
},
cancelled: {
icon: XCircle,
color: '#ef4444',
bg: 'rgba(239, 68, 68, 0.1)',
border: 'rgba(239, 68, 68, 0.3)',
label: 'Cancelled',
},
};
const config = statusConfig[race.status];
const StatusIcon = config.icon;
const formatTime = (date: string) => {
return new Date(date).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
});
};
return (
<Surface
variant="muted"
rounded="xl"
border
padding={4}
style={{
cursor: 'pointer',
borderColor: config.border,
position: 'relative',
overflow: 'hidden'
}}
onClick={() => onClick(race.id)}
>
{race.status === 'running' && (
<Box style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '0.25rem', background: 'linear-gradient(to right, #10b981, rgba(16, 185, 129, 0.5), #10b981)' }} className="animate-pulse" />
)}
<Stack direction="row" align="center" gap={4}>
{/* Date Column */}
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', minWidth: '5rem', textAlign: 'center' }}>
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase' }}>
{new Date(race.scheduledAt).toLocaleDateString('en-US', { month: 'short' })}
</Text>
<Text size="2xl" weight="bold" color="text-white">
{new Date(race.scheduledAt).getDate()}
</Text>
<Text size="xs" color="text-gray-500">
{formatTime(race.scheduledAt)}
</Text>
</Box>
{/* Divider */}
<Box style={{ width: '1px', height: '4rem', backgroundColor: '#262626' }} />
{/* Main Content */}
<Box style={{ flex: 1, minWidth: 0 }}>
<Text weight="semibold" color="text-white" block truncate>{race.track}</Text>
<Stack direction="row" align="center" gap={4} mt={1} wrap>
<Stack direction="row" align="center" gap={1.5}>
<Icon icon={Car} size={3.5} color="#9ca3af" />
<Text size="sm" color="text-gray-400">{race.car}</Text>
</Stack>
{race.strengthOfField && (
<Stack direction="row" align="center" gap={1.5}>
<Icon icon={Zap} size={3.5} color="#f59e0b" />
<Text size="sm" color="text-warning-amber">SOF {race.strengthOfField}</Text>
</Stack>
)}
</Stack>
{race.leagueName && (
<Box mt={2}>
<Link href={`/leagues/${race.leagueId}`} onClick={(e) => e.stopPropagation()}>
<Stack direction="row" align="center" gap={1.5}>
<Icon icon={Trophy} size={3.5} color="#3b82f6" />
<Text size="sm" color="text-primary-blue">{race.leagueName}</Text>
</Stack>
</Link>
</Box>
)}
</Box>
{/* Status Badge */}
<Surface variant="muted" rounded="full" border padding={1} style={{ backgroundColor: config.bg, borderColor: config.border, paddingLeft: '0.75rem', paddingRight: '0.75rem' }}>
<Stack direction="row" align="center" gap={1.5}>
<Icon icon={StatusIcon} size={3.5} color={config.color} />
<Text size="xs" weight="medium" style={{ color: config.color }}>{config.label}</Text>
</Stack>
</Surface>
<Icon icon={ChevronRight} size={5} color="#525252" />
</Stack>
</Surface>
);
}

View File

@@ -0,0 +1,59 @@
import React from 'react';
import { Flag, CalendarDays, Clock, Zap, Trophy, LucideIcon } from 'lucide-react';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Hero } from '@/ui/Hero';
import { Stack } from '@/ui/Stack';
import { Box } from '@/ui/Box';
interface RacePageHeaderProps {
totalCount: number;
scheduledCount: number;
runningCount: number;
completedCount: number;
}
export function RacePageHeader({
totalCount,
scheduledCount,
runningCount,
completedCount,
}: RacePageHeaderProps) {
return (
<Hero variant="primary">
<Stack gap={2}>
<Stack direction="row" align="center" gap={3}>
<Box style={{ padding: '0.5rem', backgroundColor: 'rgba(59, 130, 246, 0.1)', borderRadius: '0.5rem' }}>
<Flag style={{ width: '1.5rem', height: '1.5rem', color: '#3b82f6' }} />
</Box>
<Heading level={1}>Race Calendar</Heading>
</Stack>
<Text color="text-gray-400" style={{ maxWidth: '42rem' }}>
Track upcoming races, view live events, and explore results across all your leagues.
</Text>
</Stack>
{/* Quick Stats */}
<Box style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '1rem', marginTop: '1.5rem' }}>
<StatBox icon={CalendarDays} label="Total" value={totalCount} />
<StatBox icon={Clock} label="Scheduled" value={scheduledCount} color="#3b82f6" />
<StatBox icon={Zap} label="Live Now" value={runningCount} color="#10b981" />
<StatBox icon={Trophy} label="Completed" value={completedCount} />
</Box>
</Hero>
);
}
function StatBox({ icon: Icon, label, value, color }: { icon: LucideIcon, label: string, value: number, color?: string }) {
return (
<Box style={{ backgroundColor: 'rgba(15, 17, 21, 0.6)', backdropFilter: 'blur(8px)', borderRadius: '0.75rem', padding: '1rem', border: '1px solid rgba(38, 38, 38, 0.5)' }}>
<Stack gap={1}>
<Stack direction="row" align="center" gap={2}>
<Icon style={{ width: '1rem', height: '1rem', color: color || '#9ca3af' }} />
<Text size="sm" color="text-gray-400">{label}</Text>
</Stack>
<Text size="2xl" weight="bold" color="text-white">{value}</Text>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,58 @@
'use client';
import React from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Surface } from '@/ui/Surface';
interface PenaltyEntry {
driverId: string;
driverName: string;
type: 'time_penalty' | 'grid_penalty' | 'points_deduction' | 'disqualification' | 'warning' | 'license_points';
value: number;
reason: string;
notes?: string;
}
interface RacePenaltyRowProps {
penalty: PenaltyEntry;
}
export function RacePenaltyRow({ penalty }: RacePenaltyRowProps) {
return (
<Surface variant="dark" rounded="lg" padding={3}>
<Stack direction="row" align="center" gap={3}>
<Surface variant="muted" rounded="full" padding={1} style={{ width: '2.5rem', height: '2.5rem', backgroundColor: 'rgba(239, 68, 68, 0.2)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Text color="text-error-red" weight="bold">!</Text>
</Surface>
<Box style={{ flex: 1 }}>
<Stack direction="row" align="center" gap={2} mb={1}>
<Text weight="medium" color="text-white">{penalty.driverName}</Text>
<Surface variant="muted" rounded="full" padding={1} style={{ backgroundColor: 'rgba(239, 68, 68, 0.2)', paddingLeft: '0.5rem', paddingRight: '0.5rem' }}>
<Text size="xs" weight="medium" color="text-error-red" style={{ textTransform: 'capitalize' }}>
{penalty.type.replace('_', ' ')}
</Text>
</Surface>
</Stack>
<Text size="sm" color="text-gray-400" block>{penalty.reason}</Text>
{penalty.notes && (
<Box mt={1}>
<Text size="sm" color="text-gray-500" block style={{ fontStyle: 'italic' }}>{penalty.notes}</Text>
</Box>
)}
</Box>
<Box style={{ textAlign: 'right' }}>
<Text size="2xl" weight="bold" color="text-error-red">
{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`}
</Text>
</Box>
</Stack>
</Surface>
);
}

View File

@@ -0,0 +1,107 @@
'use client';
import React from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Image } from '@/ui/Image';
import { Surface } from '@/ui/Surface';
import { CountryFlagDisplay } from '@/lib/display-objects/CountryFlagDisplay';
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;
}
interface RaceResultRowProps {
result: ResultEntry;
points: number;
}
export function RaceResultRow({ result, points }: RaceResultRowProps) {
const { isCurrentUser, position, driverAvatar, driverName, country, car, laps, incidents, time, fastestLap } = result;
return (
<Surface
variant={isCurrentUser ? 'muted' : 'dark'}
rounded="xl"
border={isCurrentUser}
padding={3}
style={{
background: isCurrentUser ? 'linear-gradient(to right, rgba(59, 130, 246, 0.2), rgba(59, 130, 246, 0.1), transparent)' : undefined,
borderColor: isCurrentUser ? 'rgba(59, 130, 246, 0.4)' : undefined
}}
>
<Stack direction="row" align="center" gap={3}>
{/* Position */}
<Surface
variant="muted"
rounded="lg"
padding={1}
style={{
width: '2.5rem',
height: '2.5rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: position === 1 ? 'rgba(250, 204, 21, 0.2)' : position === 2 ? 'rgba(209, 213, 219, 0.2)' : position === 3 ? 'rgba(217, 119, 6, 0.2)' : 'rgba(38, 38, 38, 0.5)',
color: position === 1 ? '#facc15' : position === 2 ? '#d1d5db' : position === 3 ? '#d97706' : '#737373'
}}
>
<Text weight="bold">{position}</Text>
</Surface>
{/* Avatar */}
<Box style={{ position: 'relative', flexShrink: 0 }}>
<Box style={{ width: '2.5rem', height: '2.5rem', borderRadius: '9999px', overflow: 'hidden', border: isCurrentUser ? '2px solid rgba(59, 130, 246, 0.5)' : 'none' }}>
<Image src={driverAvatar} alt={driverName} width={40} height={40} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
</Box>
<Box style={{ position: 'absolute', bottom: '-0.125rem', right: '-0.125rem', width: '1.25rem', height: '1.25rem', borderRadius: '9999px', backgroundColor: '#0f1115', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '0.625rem' }}>
{CountryFlagDisplay.fromCountryCode(country).toString()}
</Box>
</Box>
{/* Driver Info */}
<Box style={{ flex: 1, minWidth: 0 }}>
<Stack direction="row" align="center" gap={2}>
<Text weight="semibold" size="sm" color={isCurrentUser ? 'text-primary-blue' : 'text-white'} truncate>{driverName}</Text>
{isCurrentUser && (
<Surface variant="muted" rounded="full" padding={1} style={{ backgroundColor: '#3b82f6', paddingLeft: '0.5rem', paddingRight: '0.5rem' }}>
<Text size="xs" weight="bold" color="text-white">YOU</Text>
</Surface>
)}
</Stack>
<Stack direction="row" align="center" gap={2} mt={1}>
<Text size="xs" color="text-gray-500">{car}</Text>
<Text size="xs" color="text-gray-500"></Text>
<Text size="xs" color="text-gray-500">Laps: {laps}</Text>
<Text size="xs" color="text-gray-500"></Text>
<Text size="xs" color="text-gray-500">Incidents: {incidents}</Text>
</Stack>
</Box>
{/* Times */}
<Box style={{ textAlign: 'right', minWidth: '100px' }}>
<Text size="sm" font="mono" color="text-white" block>{time}</Text>
<Text size="xs" color="text-performance-green" block mt={1}>FL: {fastestLap}</Text>
</Box>
{/* Points */}
<Surface variant="muted" rounded="lg" border padding={2} style={{ backgroundColor: 'rgba(245, 158, 11, 0.1)', borderColor: 'rgba(245, 158, 11, 0.2)', textAlign: 'center', minWidth: '3.5rem' }}>
<Text size="xs" color="text-gray-500" block>PTS</Text>
<Text size="sm" weight="bold" color="text-warning-amber">{points}</Text>
</Surface>
</Stack>
</Surface>
);
}

View File

@@ -0,0 +1,144 @@
import React from 'react';
import { Clock, Trophy, Users, ChevronRight, CheckCircle2 } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Link } from '@/ui/Link';
import { Card } from '@/ui/Card';
import { Stack } from '@/ui/Stack';
import type { RaceViewData } from '@/lib/view-data/RacesViewData';
import { routes } from '@/lib/routing/RouteConfig';
interface RaceSidebarProps {
upcomingRaces: RaceViewData[];
recentResults: RaceViewData[];
onRaceClick: (raceId: string) => void;
}
export function RaceSidebar({ upcomingRaces, recentResults, onRaceClick }: RaceSidebarProps) {
return (
<Stack gap={6}>
{/* Upcoming This Week */}
<Card>
<Stack gap={4}>
<Stack direction="row" align="center" justify="between">
<Heading level={3} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<Clock style={{ width: '1rem', height: '1rem', color: '#3b82f6' }} />
Next Up
</Heading>
<Text size="xs" color="text-gray-500">This week</Text>
</Stack>
{upcomingRaces.length === 0 ? (
<Text size="sm" color="text-gray-500" style={{ textAlign: 'center', padding: '1rem 0' }}>
No races scheduled this week
</Text>
) : (
<Stack gap={3}>
{upcomingRaces.map((race) => (
<Box
key={race.id}
onClick={() => onRaceClick(race.id)}
style={{
display: 'flex',
alignItems: 'center',
gap: '0.75rem',
padding: '0.5rem',
borderRadius: '0.5rem',
cursor: 'pointer',
transition: 'background-color 0.2s'
}}
>
<Box style={{ flexShrink: 0, width: '2.5rem', height: '2.5rem', backgroundColor: 'rgba(59, 130, 246, 0.1)', borderRadius: '0.5rem', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Text weight="bold" color="text-primary-blue" style={{ width: '100%', textAlign: 'center' }}>
{new Date(race.scheduledAt).getDate()}
</Text>
</Box>
<Box style={{ minWidth: 0, flex: 1 }}>
<Text size="sm" weight="medium" color="text-white" style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', display: 'block' }}>{race.track}</Text>
<Text size="xs" color="text-gray-500">{race.timeLabel}</Text>
</Box>
<ChevronRight style={{ width: '1rem', height: '1rem', color: '#737373' }} />
</Box>
))}
</Stack>
)}
</Stack>
</Card>
{/* Recent Results */}
<Card>
<Stack gap={4}>
<Heading level={3} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<Trophy style={{ width: '1rem', height: '1rem', color: '#f59e0b' }} />
Recent Results
</Heading>
{recentResults.length === 0 ? (
<Text size="sm" color="text-gray-500" style={{ textAlign: 'center', padding: '1rem 0' }}>
No completed races yet
</Text>
) : (
<Stack gap={3}>
{recentResults.map((race) => (
<Box
key={race.id}
onClick={() => onRaceClick(race.id)}
style={{
display: 'flex',
alignItems: 'center',
gap: '0.75rem',
padding: '0.5rem',
borderRadius: '0.5rem',
cursor: 'pointer',
transition: 'background-color 0.2s'
}}
>
<Box style={{ flexShrink: 0, width: '2.5rem', height: '2.5rem', backgroundColor: 'rgba(115, 115, 115, 0.1)', borderRadius: '0.5rem', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<CheckCircle2 style={{ width: '1.25rem', height: '1.25rem', color: '#737373' }} />
</Box>
<Box style={{ minWidth: 0, flex: 1 }}>
<Text size="sm" weight="medium" color="text-white" style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', display: 'block' }}>{race.track}</Text>
<Text size="xs" color="text-gray-500">{race.scheduledAtLabel}</Text>
</Box>
<ChevronRight style={{ width: '1rem', height: '1rem', color: '#737373' }} />
</Box>
))}
</Stack>
)}
</Stack>
</Card>
{/* Quick Actions */}
<Card>
<Stack gap={4}>
<Heading level={3}>Quick Actions</Heading>
<Stack gap={2}>
<Link
href={routes.public.leagues}
variant="ghost"
style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', padding: '0.75rem', borderRadius: '0.5rem', backgroundColor: '#0f1115' }}
>
<Box style={{ padding: '0.5rem', backgroundColor: 'rgba(59, 130, 246, 0.1)', borderRadius: '0.5rem' }}>
<Users style={{ width: '1rem', height: '1rem', color: '#3b82f6' }} />
</Box>
<Text size="sm" color="text-white">Browse Leagues</Text>
<ChevronRight style={{ width: '1rem', height: '1rem', color: '#737373', marginLeft: 'auto' }} />
</Link>
<Link
href={routes.public.leaderboards}
variant="ghost"
style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', padding: '0.75rem', borderRadius: '0.5rem', backgroundColor: '#0f1115' }}
>
<Box style={{ padding: '0.5rem', backgroundColor: 'rgba(245, 158, 11, 0.1)', borderRadius: '0.5rem' }}>
<Trophy style={{ width: '1rem', height: '1rem', color: '#f59e0b' }} />
</Box>
<Text size="sm" color="text-white">View Leaderboards</Text>
<ChevronRight style={{ width: '1rem', height: '1rem', color: '#737373', marginLeft: 'auto' }} />
</Link>
</Stack>
</Stack>
</Card>
</Stack>
);
}

View File

@@ -0,0 +1,92 @@
'use client';
import React from 'react';
import { Trophy } from 'lucide-react';
import { Surface } from '@/ui/Surface';
import { Stack } from '@/ui/Stack';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { DecorativeBlur } from '@/ui/DecorativeBlur';
interface RaceUserResultProps {
position: number;
startPosition: number;
positionChange: number;
incidents: number;
isClean: boolean;
isPodium: boolean;
ratingChange?: number;
animatedRatingChange: number;
}
export function RaceUserResult({
position,
startPosition,
positionChange,
incidents,
isClean,
isPodium,
ratingChange,
animatedRatingChange,
}: RaceUserResultProps) {
return (
<Surface
variant={position === 1 ? 'gradient-gold' : isPodium ? 'muted' : 'gradient-blue'}
rounded="2xl"
padding={1}
style={{ background: position === 1 ? 'linear-gradient(to right, #eab308, #facc15, #d97706)' : isPodium ? 'linear-gradient(to right, #9ca3af, #d1d5db, #6b7280)' : 'linear-gradient(to right, #3b82f6, #60a5fa, #2563eb)' }}
>
<Surface variant="dark" rounded="xl" padding={8} style={{ position: 'relative' }}>
<DecorativeBlur color="blue" size="lg" position="top-right" opacity={10} />
<Stack direction="row" align="center" justify="between" wrap gap={6} style={{ position: 'relative', zIndex: 10 }}>
<Stack direction="row" align="center" gap={5}>
<Box style={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '7rem', height: '7rem', borderRadius: '1.5rem', fontWeight: 900, fontSize: '3rem', background: position === 1 ? 'linear-gradient(to bottom right, #facc15, #d97706)' : position === 2 ? 'linear-gradient(to bottom right, #d1d5db, #6b7280)' : 'linear-gradient(to bottom right, #3b82f6, #2563eb)', color: position <= 2 ? '#0f1115' : 'white', boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1)' }}>
{position === 1 && (
<Trophy style={{ position: 'absolute', top: '-0.75rem', right: '-0.5rem', width: '2rem', height: '2rem', color: '#fef08a' }} />
)}
P{position}
</Box>
<Box>
<Text size="3xl" weight="bold" block mb={1} style={{ color: position === 1 ? '#facc15' : isPodium ? '#d1d5db' : 'white' }}>
{position === 1 ? '🏆 VICTORY!' : position === 2 ? '🥈 Second Place' : position === 3 ? '🥉 Podium Finish' : `P${position} Finish`}
</Text>
<Stack direction="row" align="center" gap={3} className="text-sm text-gray-400">
<Text>Started P{startPosition}</Text>
<Box style={{ width: '0.25rem', height: '0.25rem', borderRadius: '9999px', backgroundColor: '#525252' }} />
<Text color={isClean ? 'text-performance-green' : ''}>
{incidents}x incidents {isClean && '✨'}
</Text>
</Stack>
</Box>
</Stack>
<Stack direction="row" gap={3} wrap>
{positionChange !== 0 && (
<Surface variant="muted" rounded="2xl" border padding={3} style={{ minWidth: '100px', textAlign: 'center', background: positionChange > 0 ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)', borderColor: positionChange > 0 ? 'rgba(16, 185, 129, 0.3)' : 'rgba(239, 68, 68, 0.3)' }}>
<Stack align="center">
<Text size="2xl" weight="bold" style={{ color: positionChange > 0 ? '#10b981' : '#ef4444' }}>
{positionChange > 0 ? '↑' : '↓'}{Math.abs(positionChange)}
</Text>
<Text size="xs" color="text-gray-400">{positionChange > 0 ? 'Gained' : 'Lost'}</Text>
</Stack>
</Surface>
)}
{ratingChange !== undefined && (
<Surface variant="muted" rounded="2xl" border padding={3} style={{ minWidth: '100px', textAlign: 'center', background: ratingChange > 0 ? 'rgba(245, 158, 11, 0.1)' : 'rgba(239, 68, 68, 0.1)', borderColor: ratingChange > 0 ? 'rgba(245, 158, 11, 0.3)' : 'rgba(239, 68, 68, 0.3)' }}>
<Stack align="center">
<Text font="mono" size="2xl" weight="bold" style={{ color: ratingChange > 0 ? '#f59e0b' : '#ef4444' }}>
{animatedRatingChange > 0 ? '+' : ''}{animatedRatingChange}
</Text>
<Text size="xs" color="text-gray-400">Rating</Text>
</Stack>
</Surface>
)}
</Stack>
</Stack>
</Surface>
</Surface>
);
}

View File

@@ -1,5 +1,4 @@
import { ChevronRight } from 'lucide-react';
import { formatTime, formatDate } from '@/lib/utilities/time';
interface SidebarRaceItemProps {
race: {
@@ -26,7 +25,7 @@ export function SidebarRaceItem({ race, onClick, className }: SidebarRaceItemPro
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-white truncate">{race.track}</p>
<p className="text-xs text-gray-500">{formatTime(scheduledAtDate)}</p>
<p className="text-xs text-gray-500">{scheduledAtDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</p>
</div>
<ChevronRight className="w-4 h-4 text-gray-500" />
</div>

View File

@@ -1,5 +1,5 @@
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Card from '@/ui/Card';
import Button from '@/ui/Button';
type UpcomingRace = {
id: string;