website refactor
This commit is contained in:
182
apps/website/components/leagues/AdminQuickViewWidgets.tsx
Normal file
182
apps/website/components/leagues/AdminQuickViewWidgets.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
283
apps/website/components/leagues/EnhancedLeagueSchedulePanel.tsx
Normal file
283
apps/website/components/leagues/EnhancedLeagueSchedulePanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
185
apps/website/components/leagues/NextRaceCountdownWidget.tsx
Normal file
185
apps/website/components/leagues/NextRaceCountdownWidget.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
254
apps/website/components/leagues/RaceDetailModal.tsx
Normal file
254
apps/website/components/leagues/RaceDetailModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
91
apps/website/components/leagues/SeasonProgressWidget.tsx
Normal file
91
apps/website/components/leagues/SeasonProgressWidget.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user