website refactor

This commit is contained in:
2026-01-21 17:50:02 +01:00
parent 4b54c3603b
commit 02987f60c8
29 changed files with 1673 additions and 35 deletions

View File

@@ -0,0 +1,182 @@
'use client';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Link } from '@/ui/Link';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
import { AlertTriangle, DollarSign, Shield, Wallet } from 'lucide-react';
interface AdminQuickViewWidgetsProps {
leagueId: string;
walletBalance?: number;
pendingProtestsCount?: number;
pendingJoinRequestsCount?: number;
isOwnerOrAdmin: boolean;
}
export function AdminQuickViewWidgets({
leagueId,
walletBalance = 0,
pendingProtestsCount = 0,
pendingJoinRequestsCount = 0,
isOwnerOrAdmin,
}: AdminQuickViewWidgetsProps) {
if (!isOwnerOrAdmin) {
return null;
}
return (
<Stack gap={4}>
{/* Wallet Preview */}
<Surface
variant="muted"
rounded="xl"
border
padding={6}
style={{
background: 'linear-gradient(to bottom right, #262626, rgba(38, 38, 38, 0.8))',
borderColor: 'rgba(59, 130, 246, 0.3)',
}}
>
<Stack gap={4}>
<Stack direction="row" align="center" gap={3}>
<Stack
display="flex"
h="10"
w="10"
alignItems="center"
justifyContent="center"
rounded="lg"
bg="bg-primary-blue/10"
>
<Wallet size={20} color="var(--primary-blue)" />
</Stack>
<Stack gap={0}>
<Text size="sm" weight="bold" color="text-white" block>
Wallet Balance
</Text>
<Text size="2xl" weight="bold" color="text-primary-blue" font="mono" block>
${walletBalance.toFixed(2)}
</Text>
</Stack>
</Stack>
<Stack direction="row" gap={2}>
<Link href={`/leagues/${leagueId}/wallet`} style={{ flex: 1 }}>
<Button variant="primary" style={{ width: '100%' }}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={DollarSign} size={4} />
<Text>Manage Wallet</Text>
</Stack>
</Button>
</Link>
</Stack>
</Stack>
</Surface>
{/* Stewarding Quick-View */}
<Surface
variant="muted"
rounded="xl"
border
padding={6}
style={{
background: 'linear-gradient(to bottom right, #262626, rgba(38, 38, 38, 0.8))',
borderColor: 'rgba(239, 68, 68, 0.3)',
}}
>
<Stack gap={4}>
<Stack direction="row" align="center" gap={3}>
<Stack
display="flex"
h="10"
w="10"
alignItems="center"
justifyContent="center"
rounded="lg"
bg="bg-error-red/10"
>
<Shield size={20} color="var(--error-red)" />
</Stack>
<Stack gap={0}>
<Text size="sm" weight="bold" color="text-white" block>
Stewarding Queue
</Text>
<Text size="2xl" weight="bold" color="text-error-red" font="mono" block>
{pendingProtestsCount}
</Text>
</Stack>
</Stack>
{pendingProtestsCount > 0 ? (
<Stack direction="row" gap={2}>
<Link href={`/leagues/${leagueId}/stewarding`} style={{ flex: 1 }}>
<Button variant="danger" style={{ width: '100%' }}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={AlertTriangle} size={4} />
<Text>Review Protests</Text>
</Stack>
</Button>
</Link>
</Stack>
) : (
<Text size="xs" color="text-gray-500" italic>
No pending protests
</Text>
)}
</Stack>
</Surface>
{/* Join Requests Preview */}
{pendingJoinRequestsCount > 0 && (
<Surface
variant="muted"
rounded="xl"
border
padding={6}
style={{
background: 'linear-gradient(to bottom right, #262626, rgba(38, 38, 38, 0.8))',
borderColor: 'rgba(251, 191, 36, 0.3)',
}}
>
<Stack gap={4}>
<Stack direction="row" align="center" gap={3}>
<Stack
display="flex"
h="10"
w="10"
alignItems="center"
justifyContent="center"
rounded="lg"
bg="bg-warning-amber/10"
>
<Icon icon={Shield} size={20} color="var(--warning-amber)" />
</Stack>
<Stack gap={0}>
<Text size="sm" weight="bold" color="text-white" block>
Join Requests
</Text>
<Text size="2xl" weight="bold" color="text-warning-amber" font="mono" block>
{pendingJoinRequestsCount}
</Text>
</Stack>
</Stack>
<Stack direction="row" gap={2}>
<Link href={`/leagues/${leagueId}/admin`} style={{ flex: 1 }}>
<Button variant="warning" style={{ width: '100%' }}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Shield} size={4} />
<Text>Review Requests</Text>
</Stack>
</Button>
</Link>
</Stack>
</Stack>
</Surface>
)}
</Stack>
);
}

View File

@@ -0,0 +1,283 @@
'use client';
import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import { routes } from '@/lib/routing/RouteConfig';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Badge } from '@/ui/Badge';
import { Group } from '@/ui/Group';
import { Surface } from '@/ui/Surface';
import { ChevronDown, ChevronUp, Calendar, CheckCircle, Trophy, Edit, Clock } from 'lucide-react';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
interface RaceEvent {
id: string;
name: string;
track?: string;
car?: string;
sessionType?: string;
scheduledAt: string;
status: 'scheduled' | 'completed';
strengthOfField?: number;
isUserRegistered?: boolean;
canRegister?: boolean;
canEdit?: boolean;
canReschedule?: boolean;
}
interface EnhancedLeagueSchedulePanelProps {
events: RaceEvent[];
leagueId: string;
currentDriverId?: string;
isAdmin: boolean;
onRegister: (raceId: string) => void;
onWithdraw: (raceId: string) => void;
onEdit: (raceId: string) => void;
onReschedule: (raceId: string) => void;
onRaceDetail: (raceId: string) => void;
onResultsClick: (raceId: string) => void;
}
interface MonthGroup {
month: string;
year: number;
races: RaceEvent[];
}
export function EnhancedLeagueSchedulePanel({
events,
leagueId,
currentDriverId,
isAdmin,
onRegister,
onWithdraw,
onEdit,
onReschedule,
onRaceDetail,
onResultsClick,
}: EnhancedLeagueSchedulePanelProps) {
const router = useRouter();
const [expandedMonths, setExpandedMonths] = useState<Set<string>>(new Set());
// Group races by month
const groupRacesByMonth = (): MonthGroup[] => {
const groups = new Map<string, MonthGroup>();
events.forEach(event => {
const date = new Date(event.scheduledAt);
const monthKey = `${date.getFullYear()}-${date.getMonth()}`;
const monthName = date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
if (!groups.has(monthKey)) {
groups.set(monthKey, {
month: monthName,
year: date.getFullYear(),
races: [],
});
}
groups.get(monthKey)!.races.push(event);
});
return Array.from(groups.values()).sort((a, b) => {
if (a.year !== b.year) return b.year - a.year;
return b.month.localeCompare(a.month);
});
};
const toggleMonth = (monthKey: string) => {
setExpandedMonths(prev => {
const newSet = new Set(prev);
if (newSet.has(monthKey)) {
newSet.delete(monthKey);
} else {
newSet.add(monthKey);
}
return newSet;
});
};
const getRaceStatusBadge = (status: 'scheduled' | 'completed') => {
if (status === 'completed') {
return <Badge variant="success" size="sm">Completed</Badge>;
}
return <Badge variant="primary" size="sm">Scheduled</Badge>;
};
const formatTime = (scheduledAt: string) => {
return DateDisplay.formatDateTime(scheduledAt);
};
const groups = groupRacesByMonth();
if (events.length === 0) {
return (
<Box p={12} textAlign="center" border borderColor="zinc-800" bg="zinc-900/30">
<Text color="text-zinc-500" italic>No races scheduled for this season.</Text>
</Box>
);
}
return (
<Stack gap={4}>
{groups.map((group, groupIndex) => {
const monthKey = `${group.year}-${groupIndex}`;
const isExpanded = expandedMonths.has(monthKey);
return (
<Surface key={monthKey} border borderColor="border-outline-steel" overflow="hidden">
{/* Month Header */}
<Box
display="flex"
alignItems="center"
justifyContent="space-between"
p={4}
bg="bg-surface-charcoal"
borderBottom={isExpanded}
borderColor="border-outline-steel"
cursor="pointer"
onClick={() => toggleMonth(monthKey)}
>
<Group gap={3}>
<Icon icon={Calendar} size={4} color="text-primary-blue" />
<Text size="md" weight="bold" color="text-white">
{group.month}
</Text>
<Badge variant="outline" size="sm">
{group.races.length} {group.races.length === 1 ? 'Race' : 'Races'}
</Badge>
</Group>
<Icon icon={isExpanded ? ChevronUp : ChevronDown} size={4} color="text-zinc-400" />
</Box>
{/* Race List */}
{isExpanded && (
<Box p={4}>
<Stack gap={3}>
{group.races.map((race, raceIndex) => (
<Surface
key={race.id}
border
borderColor="border-outline-steel"
p={4}
bg="bg-base-black"
>
<Box display="flex" alignItems="center" justifyContent="space-between" gap={4}>
{/* Race Info */}
<Box flex={1}>
<Stack gap={2}>
<Group gap={2} align="center">
<Text size="sm" weight="bold" color="text-white">
{race.name || `Race ${race.id.substring(0, 4)}`}
</Text>
{getRaceStatusBadge(race.status)}
</Group>
<Group gap={3}>
<Text size="xs" color="text-zinc-400" uppercase letterSpacing="widest">
{race.track || 'TBA'}
</Text>
{race.car && (
<Text size="xs" color="text-zinc-500" uppercase letterSpacing="widest">
{race.car}
</Text>
)}
{race.sessionType && (
<Text size="xs" color="text-zinc-500" uppercase letterSpacing="widest">
{race.sessionType}
</Text>
)}
</Group>
<Group gap={2} align="center">
<Icon icon={Clock} size={3} color="text-zinc-500" />
<Text size="xs" color="text-zinc-400" font="mono">
{formatTime(race.scheduledAt)}
</Text>
</Group>
</Stack>
</Box>
{/* Action Buttons */}
<Box display="flex" gap={2} flexWrap="wrap">
{race.status === 'scheduled' && (
<>
{!race.isUserRegistered && race.canRegister && (
<Button
variant="primary"
size="sm"
onClick={() => onRegister(race.id)}
icon={<Icon icon={CheckCircle} size={3} />}
>
Register
</Button>
)}
{race.isUserRegistered && (
<Button
variant="secondary"
size="sm"
onClick={() => onWithdraw(race.id)}
icon={<Icon icon={ChevronDown} size={3} />}
>
Withdraw
</Button>
)}
{race.canEdit && (
<Button
variant="neutral"
size="sm"
onClick={() => onEdit(race.id)}
icon={<Icon icon={Edit} size={3} />}
>
Edit
</Button>
)}
{race.canReschedule && (
<Button
variant="neutral"
size="sm"
onClick={() => onReschedule(race.id)}
icon={<Icon icon={Clock} size={3} />}
>
Reschedule
</Button>
)}
</>
)}
{race.status === 'completed' && (
<>
<Button
variant="primary"
size="sm"
onClick={() => onResultsClick(race.id)}
icon={<Icon icon={Trophy} size={3} />}
>
View Results
</Button>
</>
)}
{/* Always show detail button */}
<Button
variant="secondary"
size="sm"
onClick={() => onRaceDetail(race.id)}
>
Details
</Button>
</Box>
</Box>
</Surface>
))}
</Stack>
</Box>
)}
</Surface>
);
})}
</Stack>
);
}

View File

@@ -7,6 +7,10 @@ import { useRouter } from 'next/navigation';
import { routes } from '@/lib/routing/RouteConfig';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Group } from '@/ui/Group';
import { Badge } from '@/ui/Badge';
import { Icon } from '@/ui/Icon';
import { Droplet, XCircle } from 'lucide-react';
interface StandingEntry {
position: number;
@@ -19,6 +23,9 @@ interface StandingEntry {
races: number;
avgFinish: number | null;
gap: string;
positionChange: number;
lastRacePoints: number;
droppedRaceIds: string[];
}
interface LeagueStandingsTableProps {
@@ -44,6 +51,7 @@ export function LeagueStandingsTable({ standings }: LeagueStandingsTableProps) {
key={entry.driverId || entry.driverName}
id={entry.driverId || ''}
rank={entry.position}
rankDelta={entry.positionChange}
name={entry.driverName}
avatarUrl="" // Not provided in StandingEntry
nationality="INT"

View File

@@ -0,0 +1,185 @@
'use client';
import { Badge } from '@/ui/Badge';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Link } from '@/ui/Link';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
import { Calendar, Clock, MapPin, type LucideIcon } from 'lucide-react';
import { useEffect, useState } from 'react';
interface NextRaceCountdownWidgetProps {
raceId: string;
raceName: string;
date: string;
track?: string;
car?: string;
isRegistered?: boolean;
}
interface CountdownState {
days: number;
hours: number;
minutes: number;
seconds: number;
}
export function NextRaceCountdownWidget({
raceId,
raceName,
date,
track,
car,
isRegistered = false,
}: NextRaceCountdownWidgetProps) {
const [countdown, setCountdown] = useState<CountdownState | null>(null);
const [isExpired, setIsExpired] = useState(false);
useEffect(() => {
const calculateCountdown = () => {
const now = new Date();
const raceDate = new Date(date);
const diff = raceDate.getTime() - now.getTime();
if (diff <= 0) {
setIsExpired(true);
setCountdown({ days: 0, hours: 0, minutes: 0, seconds: 0 });
return;
}
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
setCountdown({ days, hours, minutes, seconds });
setIsExpired(false);
};
calculateCountdown();
const interval = setInterval(calculateCountdown, 1000);
return () => clearInterval(interval);
}, [date]);
const formatTime = (value: number) => value.toString().padStart(2, '0');
return (
<Surface
variant="muted"
rounded="xl"
border
padding={6}
style={{
position: 'relative',
overflow: 'hidden',
background: 'linear-gradient(to bottom right, #262626, rgba(38, 38, 38, 0.8))',
borderColor: 'rgba(59, 130, 246, 0.3)',
}}
>
<Stack
position="absolute"
top="0"
right="0"
w="40"
h="40"
style={{
background: 'linear-gradient(to bottom left, rgba(59, 130, 246, 0.2), transparent)',
borderBottomLeftRadius: '9999px',
}}
/>
<Stack position="relative" gap={4}>
{/* Header */}
<Stack direction="row" align="center" gap={2}>
<Badge variant="primary" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }}>
Next Race
</Badge>
{isRegistered && (
<Badge variant="success">
Registered
</Badge>
)}
</Stack>
{/* Race Info */}
<Stack gap={2}>
<Text size="xl" weight="bold" color="text-white">
{raceName}
</Text>
{track && (
<Stack direction="row" align="center" gap={1.5}>
<Icon icon={MapPin as LucideIcon} size={4} color="var(--text-gray-500)" />
<Text size="sm" color="text-gray-400">
{track}
</Text>
</Stack>
)}
{car && (
<Stack direction="row" align="center" gap={1.5}>
<Icon icon={Calendar as LucideIcon} size={4} color="var(--text-gray-500)" />
<Text size="sm" color="text-gray-400">
{car}
</Text>
</Stack>
)}
</Stack>
{/* Countdown Timer */}
<Stack gap={2}>
<Text
size="xs"
color="text-gray-500"
style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }}
block
>
{isExpired ? 'Race Started' : 'Starts in'}
</Text>
{countdown && (
<Stack direction="row" gap={2} align="center">
<Stack align="center" gap={0.5}>
<Text size="2xl" weight="bold" color="text-primary-blue" font="mono">
{formatTime(countdown.days)}
</Text>
<Text size="xs" color="text-gray-500">Days</Text>
</Stack>
<Text size="2xl" weight="bold" color="text-gray-600">:</Text>
<Stack align="center" gap={0.5}>
<Text size="2xl" weight="bold" color="text-primary-blue" font="mono">
{formatTime(countdown.hours)}
</Text>
<Text size="xs" color="text-gray-500">Hours</Text>
</Stack>
<Text size="2xl" weight="bold" color="text-gray-600">:</Text>
<Stack align="center" gap={0.5}>
<Text size="2xl" weight="bold" color="text-primary-blue" font="mono">
{formatTime(countdown.minutes)}
</Text>
<Text size="xs" color="text-gray-500">Mins</Text>
</Stack>
<Text size="2xl" weight="bold" color="text-gray-600">:</Text>
<Stack align="center" gap={0.5}>
<Text size="2xl" weight="bold" color="text-primary-blue" font="mono">
{formatTime(countdown.seconds)}
</Text>
<Text size="xs" color="text-gray-500">Secs</Text>
</Stack>
</Stack>
)}
</Stack>
{/* Actions */}
<Stack direction="row" gap={3} mt={2}>
<Link href={`/races/${raceId}`} style={{ flex: 1 }}>
<Button
variant="primary"
style={{ width: '100%' }}
>
{isRegistered ? 'View Details' : 'Register'}
</Button>
</Link>
</Stack>
</Stack>
</Surface>
);
}

View File

@@ -0,0 +1,254 @@
'use client';
import React from 'react';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Group } from '@/ui/Group';
import { Surface } from '@/ui/Surface';
import { Icon } from '@/ui/Icon';
import { Button } from '@/ui/Button';
import { Badge } from '@/ui/Badge';
import {
Calendar,
Clock,
Car,
MapPin,
Thermometer,
Droplets,
Wind,
Cloud,
X,
Trophy,
CheckCircle
} from 'lucide-react';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
interface RaceDetailModalProps {
race: {
id: string;
name: string;
track?: string;
car?: string;
sessionType?: string;
scheduledAt: string;
status: 'scheduled' | 'completed';
strengthOfField?: number;
isUserRegistered?: boolean;
canRegister?: boolean;
};
isOpen: boolean;
onClose: () => void;
onRegister?: () => void;
onWithdraw?: () => void;
onResultsClick?: () => void;
}
export function RaceDetailModal({
race,
isOpen,
onClose,
onRegister,
onWithdraw,
onResultsClick,
}: RaceDetailModalProps) {
if (!isOpen) return null;
const formatTime = (scheduledAt: string) => {
return DateDisplay.formatDateTime(scheduledAt);
};
const getStatusBadge = (status: 'scheduled' | 'completed') => {
if (status === 'completed') {
return <Badge variant="success" size="sm">Completed</Badge>;
}
return <Badge variant="primary" size="sm">Scheduled</Badge>;
};
return (
<Box
position="fixed"
top={0}
left={0}
right={0}
bottom={0}
bg="bg-base-black/80"
display="flex"
alignItems="center"
justifyContent="center"
zIndex={1000}
onClick={onClose}
>
<Box
maxWidth="lg"
width="100%"
mx={4}
onClick={(e) => e.stopPropagation()}
>
<Surface border borderColor="border-outline-steel" overflow="hidden">
{/* Header */}
<Box
display="flex"
alignItems="center"
justifyContent="space-between"
p={4}
bg="bg-surface-charcoal"
borderBottom
borderColor="border-outline-steel"
>
<Group gap={3}>
<Text size="lg" weight="bold" color="text-white">
{race.name || `Race ${race.id.substring(0, 4)}`}
</Text>
{getStatusBadge(race.status)}
</Group>
<Button
variant="ghost"
size="sm"
onClick={onClose}
icon={<Icon icon={X} size={4} />}
>
Close
</Button>
</Box>
{/* Content */}
<Box p={4}>
<Stack gap={4}>
{/* Basic Info */}
<Surface border borderColor="border-outline-steel" p={4}>
<Text as="h3" size="sm" weight="bold" color="text-gray-500" uppercase letterSpacing="widest" mb={3}>
Race Details
</Text>
<Stack gap={3}>
<Group gap={2} align="center">
<Icon icon={MapPin} size={4} color="text-primary-blue" />
<Text size="md" color="text-white" weight="bold">
{race.track || 'TBA'}
</Text>
</Group>
<Group gap={2} align="center">
<Icon icon={Car} size={4} color="text-primary-blue" />
<Text size="md" color="text-white">
{race.car || 'TBA'}
</Text>
</Group>
<Group gap={2} align="center">
<Icon icon={Calendar} size={4} color="text-primary-blue" />
<Text size="md" color="text-white">
{formatTime(race.scheduledAt)}
</Text>
</Group>
{race.sessionType && (
<Group gap={2} align="center">
<Icon icon={Clock} size={4} color="text-primary-blue" />
<Text size="md" color="text-white">
{race.sessionType}
</Text>
</Group>
)}
</Stack>
</Surface>
{/* Weather Info (Mock Data) */}
<Surface border borderColor="border-outline-steel" p={4}>
<Text as="h3" size="sm" weight="bold" color="text-gray-500" uppercase letterSpacing="widest" mb={3}>
Weather Conditions
</Text>
<Stack gap={3}>
<Group gap={2} align="center">
<Icon icon={Thermometer} size={4} color="text-primary-blue" />
<Text size="md" color="text-white">Air: 24°C</Text>
</Group>
<Group gap={2} align="center">
<Icon icon={Thermometer} size={4} color="text-primary-blue" />
<Text size="md" color="text-white">Track: 31°C</Text>
</Group>
<Group gap={2} align="center">
<Icon icon={Droplets} size={4} color="text-primary-blue" />
<Text size="md" color="text-white">Humidity: 45%</Text>
</Group>
<Group gap={2} align="center">
<Icon icon={Wind} size={4} color="text-primary-blue" />
<Text size="md" color="text-white">Wind: 12 km/h NW</Text>
</Group>
<Group gap={2} align="center">
<Icon icon={Cloud} size={4} color="text-primary-blue" />
<Text size="md" color="text-white">Partly Cloudy</Text>
</Group>
</Stack>
</Surface>
{/* Car Classes */}
<Surface border borderColor="border-outline-steel" p={4}>
<Text as="h3" size="sm" weight="bold" color="text-gray-500" uppercase letterSpacing="widest" mb={3}>
Car Classes
</Text>
<Group gap={2} wrap>
<Badge variant="outline" size="sm">GT3</Badge>
<Badge variant="outline" size="sm">GT4</Badge>
<Badge variant="outline" size="sm">TCR</Badge>
</Group>
</Surface>
{/* Strength of Field */}
{race.strengthOfField && (
<Surface border borderColor="border-outline-steel" p={4}>
<Text as="h3" size="sm" weight="bold" color="text-gray-500" uppercase letterSpacing="widest" mb={3}>
Strength of Field
</Text>
<Group gap={2} align="center">
<Icon icon={Trophy} size={4} color="text-primary-blue" />
<Text size="md" color="text-white">
{race.strengthOfField.toFixed(1)} / 10.0
</Text>
</Group>
</Surface>
)}
{/* Action Buttons */}
{race.status === 'scheduled' && (
<Box display="flex" gap={2} flexWrap="wrap">
{!race.isUserRegistered && race.canRegister && onRegister && (
<Button
variant="primary"
size="md"
onClick={onRegister}
icon={<Icon icon={CheckCircle} size={4} />}
fullWidth
>
Register
</Button>
)}
{race.isUserRegistered && onWithdraw && (
<Button
variant="secondary"
size="md"
onClick={onWithdraw}
icon={<Icon icon={X} size={4} />}
fullWidth
>
Withdraw
</Button>
)}
</Box>
)}
{race.status === 'completed' && onResultsClick && (
<Button
variant="primary"
size="md"
onClick={onResultsClick}
icon={<Icon icon={Trophy} size={4} />}
fullWidth
>
View Results
</Button>
)}
</Stack>
</Box>
</Surface>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,91 @@
'use client';
import { ProgressBar } from '@/ui/ProgressBar';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
import { Trophy } from 'lucide-react';
interface SeasonProgressWidgetProps {
completedRaces: number;
totalRaces: number;
percentage: number;
}
export function SeasonProgressWidget({
completedRaces,
totalRaces,
percentage,
}: SeasonProgressWidgetProps) {
return (
<Surface
variant="muted"
rounded="xl"
border
padding={6}
style={{
background: 'linear-gradient(to bottom right, #262626, rgba(38, 38, 38, 0.8))',
borderColor: 'rgba(34, 197, 94, 0.3)',
}}
>
<Stack gap={4}>
{/* Header */}
<Stack direction="row" align="center" gap={3}>
<Stack
display="flex"
h="10"
w="10"
alignItems="center"
justifyContent="center"
rounded="lg"
bg="bg-performance-green/10"
>
<Trophy size={20} color="var(--performance-green)" />
</Stack>
<Stack gap={0}>
<Text size="sm" weight="bold" color="text-white" block>
Season Progress
</Text>
<Text size="xs" color="text-gray-400" block>
Race {completedRaces} of {totalRaces}
</Text>
</Stack>
</Stack>
{/* Progress Bar */}
<Stack gap={2}>
<ProgressBar
value={percentage}
intent="success"
size="lg"
/>
<Stack direction="row" justify="between" align="center">
<Text size="xs" color="text-gray-500">
{percentage}% Complete
</Text>
<Text size="xs" color="text-performance-green" weight="bold">
{completedRaces}/{totalRaces} Races
</Text>
</Stack>
</Stack>
{/* Visual Indicator */}
<Stack
rounded="lg"
bg="bg-performance-green/10"
border
borderColor="border-performance-green/30"
p={3}
>
<Text size="xs" color="text-performance-green" weight="medium" block>
{percentage >= 100
? 'Season Complete! 🏆'
: percentage >= 50
? 'Over halfway there! 🚀'
: 'Season underway! 🏁'}
</Text>
</Stack>
</Stack>
</Surface>
);
}