website refactor

This commit is contained in:
2026-01-15 19:55:46 +01:00
parent 5ef149b782
commit ce7be39155
154 changed files with 436 additions and 356 deletions

View File

@@ -0,0 +1,47 @@
import React from 'react';
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { RaceResultList } from '@/ui/RaceResultList';
import { RaceSummaryItem } from '@/ui/RaceSummaryItem';
import { Box } from '@/ui/Box';
type RaceWithResults = {
raceId: string;
track: string;
car: string;
winnerName: string;
scheduledAt: string | Date;
};
interface LatestResultsSidebarProps {
results: RaceWithResults[];
}
export function LatestResultsSidebar({ results }: LatestResultsSidebarProps) {
if (!results.length) {
return null;
}
return (
<Card bg="bg-iron-gray/80" p={4}>
<Heading level={3} mb={3}>
Latest results
</Heading>
<RaceResultList>
{results.slice(0, 4).map((result) => {
const scheduledAt = typeof result.scheduledAt === 'string' ? new Date(result.scheduledAt) : result.scheduledAt;
return (
<Box as="li" key={result.raceId}>
<RaceSummaryItem
track={result.track}
meta={`${result.winnerName}${result.car}`}
date={scheduledAt}
/>
</Box>
);
})}
</RaceResultList>
</Card>
);
}

View File

@@ -0,0 +1,59 @@
import type { RaceViewData } from '@/lib/view-data/RacesViewData';
import { Box } from '@/ui/Box';
import { LiveRaceItem } from '@/ui/LiveRaceItem';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
interface LiveRacesBannerProps {
liveRaces: RaceViewData[];
onRaceClick: (raceId: string) => void;
}
export function LiveRacesBanner({ liveRaces, onRaceClick }: LiveRacesBannerProps) {
if (liveRaces.length === 0) return null;
return (
<Box
position="relative"
overflow="hidden"
rounded="xl"
p={6}
border
borderColor="border-performance-green/30"
bg="linear-gradient(to right, rgba(16, 185, 129, 0.2), rgba(16, 185, 129, 0.1), transparent)"
>
<Box
position="absolute"
top="0"
right="0"
w="32"
h="32"
bg="bg-performance-green/20"
rounded="full"
blur="xl"
/>
<Box position="relative" zIndex={10}>
<Box mb={4}>
<Stack direction="row" align="center" gap={2} bg="bg-performance-green/20" px={3} py={1} rounded="full" w="fit">
<Box w="2" h="2" bg="bg-performance-green" rounded="full" />
<Text weight="semibold" size="sm" color="text-performance-green">LIVE NOW</Text>
</Stack>
</Box>
<Stack gap={3}>
{liveRaces.map((race) => (
<LiveRaceItem
key={race.id}
track={race.track}
leagueName={race.leagueName ?? 'Unknown League'}
onClick={() => onRaceClick(race.id)}
/>
))}
</Stack>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,30 @@
import { routes } from '@/lib/routing/RouteConfig';
import { NextRaceCard as UiNextRaceCard } from '@/ui/NextRaceCard';
interface NextRaceCardProps {
nextRace: {
id: string;
track: string;
car: string;
formattedDate: string;
formattedTime: string;
timeUntil: string;
isMyLeague: boolean;
};
}
export function NextRaceCard({ nextRace }: NextRaceCardProps) {
return (
<UiNextRaceCard
track={nextRace.track}
car={nextRace.car}
formattedDate={nextRace.formattedDate}
formattedTime={nextRace.formattedTime}
timeUntil={nextRace.timeUntil}
isMyLeague={nextRace.isMyLeague}
href={routes.race.detail(nextRace.id)}
/>
);
}

View File

@@ -0,0 +1,45 @@
import React from 'react';
import { LucideIcon } from 'lucide-react';
import { raceStatusConfig } from '@/lib/utilities/raceStatus';
import { RaceCard as UiRaceCard } from '@/ui/RaceCard';
interface RaceCardProps {
race: {
id: string;
track: string;
car: string;
scheduledAt: string;
status: string;
leagueId?: string;
leagueName: string;
strengthOfField?: number | null;
};
onClick?: () => void;
}
export function RaceCard({ race, onClick }: RaceCardProps) {
const config = raceStatusConfig[race.status as keyof typeof raceStatusConfig] || {
border: 'border-charcoal-outline',
bg: 'bg-charcoal-outline',
color: 'text-gray-400',
icon: null,
label: 'Scheduled',
};
return (
<UiRaceCard
track={race.track}
car={race.car}
scheduledAt={race.scheduledAt}
status={race.status}
leagueName={race.leagueName}
leagueId={race.leagueId}
strengthOfField={race.strengthOfField}
onClick={onClick}
statusConfig={{
...config,
icon: config.icon as LucideIcon | null,
}}
/>
);
}

View File

@@ -0,0 +1,61 @@
import { Card } from '@/ui/Card';
import { DriverEntryRow } from '@/components/drivers/DriverEntryRow';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
import { Users } from 'lucide-react';
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="rgb(59, 130, 246)" />}>Entry List</Heading>
<Text size="sm" color="text-gray-400">{entries.length} drivers</Text>
</Stack>
{entries.length === 0 ? (
<Stack align="center" py={8} gap={3}>
<Surface variant="muted" rounded="full" p={4}>
<Icon icon={Users} size={6} color="rgb(82, 82, 82)" />
</Surface>
<Text color="text-gray-400">No drivers registered yet</Text>
</Stack>
) : (
<Stack gap={1}>
{entries.map((driver, index) => (
<DriverEntryRow
key={driver.id}
index={index}
name={driver.name}
avatarUrl={driver.avatarUrl}
country={driver.country}
rating={driver.rating}
isCurrentUser={driver.isCurrentUser}
onClick={() => onDriverClick(driver.id)}
/>
))}
</Stack>
)}
</Stack>
</Card>
);
}

View File

@@ -0,0 +1,64 @@
import type { TimeFilter } from '@/templates/RacesTemplate';
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { FilterGroup } from '@/ui/FilterGroup';
import { Select } from '@/ui/Select';
import { Stack } from '@/ui/Stack';
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 }))
];
const timeOptions = [
{ id: 'upcoming', label: 'Upcoming' },
{ id: 'live', label: 'Live', indicatorColor: 'bg-performance-green' },
{ id: 'past', label: 'Past' },
{ id: 'all', label: 'All' },
];
return (
<Card p={4}>
<Stack direction="row" align="center" gap={4} wrap>
<FilterGroup
options={timeOptions}
activeId={timeFilter}
onSelect={(id) => setTimeFilter(id as TimeFilter)}
/>
<Select
value={leagueFilter}
onChange={(e) => setLeagueFilter(e.target.value)}
options={leagueOptions}
fullWidth={false}
/>
<Button
variant="secondary"
onClick={onShowMoreFilters}
>
More Filters
</Button>
</Stack>
</Card>
);
}

View File

@@ -0,0 +1,102 @@
import { routes } from '@/lib/routing/RouteConfig';
import type { RaceViewData } from '@/lib/view-data/RacesViewData';
import { Box } from '@/ui/Box';
import { Card } from '@/ui/Card';
import { DateHeader } from '@/ui/DateHeader';
import { Icon } from '@/ui/Icon';
import { RaceListItem } from '@/components/races/RaceListItem';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Calendar, CheckCircle2, Clock, PlayCircle, XCircle } from 'lucide-react';
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 py={12} textAlign="center">
<Stack align="center" gap={4}>
<Box p={4} bg="bg-iron-gray" rounded="full">
<Icon icon={Calendar} size={8} color="rgb(115, 115, 115)" />
</Box>
<Box>
<Text weight="medium" color="text-white" block mb={1}>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}>
<DateHeader
label={group.dateLabel}
count={group.races.length}
/>
<Stack gap={2}>
{group.races.map((race) => {
const config = statusConfig[race.status as keyof typeof statusConfig] || statusConfig.scheduled;
return (
<RaceListItem
key={race.id}
track={race.track}
car={race.car}
timeLabel={race.timeLabel}
relativeTimeLabel={race.relativeTimeLabel}
status={race.status}
leagueName={race.leagueName ?? 'Unknown League'}
leagueHref={routes.league.detail(race.leagueId ?? '')}
strengthOfField={race.strengthOfField}
onClick={() => onRaceClick(race.id)}
statusConfig={config}
/>
);
})}
</Stack>
</Stack>
))}
</Stack>
);
}

View File

@@ -0,0 +1,143 @@
import { ArrowRight, Car, ChevronRight, LucideIcon, Trophy, Zap } from 'lucide-react';
import { Badge } from '@/ui/Badge';
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Link } from '@/ui/Link';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
interface RaceListItemProps {
track: string;
car: string;
timeLabel?: string;
relativeTimeLabel?: string;
dateLabel?: string;
dayLabel?: string;
status: string;
leagueName?: string | null;
leagueHref?: string;
strengthOfField?: number | null;
onClick: () => void;
statusConfig: {
icon: LucideIcon;
variant: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
label: string;
};
}
export function RaceListItem({
track,
car,
timeLabel,
relativeTimeLabel,
dateLabel,
dayLabel,
status,
leagueName,
leagueHref,
strengthOfField,
onClick,
statusConfig,
}: RaceListItemProps) {
const StatusIcon = statusConfig.icon;
return (
<Box
onClick={onClick}
position="relative"
overflow="hidden"
rounded="xl"
bg="bg-iron-gray"
border
borderColor="border-charcoal-outline"
p={4}
cursor="pointer"
transition
hoverScale
group
>
{/* Live indicator */}
{status === 'running' && (
<Box
position="absolute"
top="0"
left="0"
right="0"
h="1"
style={{ background: 'linear-gradient(to right, #10b981, rgba(16, 185, 129, 0.5), #10b981)' }}
/>
)}
<Stack direction="row" align="center" gap={4}>
{/* Time/Date Column */}
<Box flexShrink={0} textAlign="center" minWidth="60px">
{dateLabel && (
<Text size="xs" color="text-gray-500" block style={{ textTransform: 'uppercase' }}>
{dateLabel}
</Text>
)}
<Text size={dayLabel ? "2xl" : "lg"} weight="bold" color="text-white" block>
{dayLabel || timeLabel}
</Text>
<Text size="xs" color={status === 'running' ? 'text-performance-green' : 'text-gray-400'} block>
{status === 'running' ? 'LIVE' : relativeTimeLabel || timeLabel}
</Text>
</Box>
{/* Divider */}
<Box w="px" h="10" alignSelf="stretch" bg="bg-charcoal-outline" />
{/* Main Content */}
<Box flexGrow={1} minWidth="0">
<Stack direction="row" align="start" justify="between" gap={4}>
<Box minWidth="0">
<Heading level={3} truncate>
{track}
</Heading>
<Stack direction="row" align="center" gap={3} mt={1}>
<Stack direction="row" align="center" gap={1}>
<Icon icon={Car} size={3.5} color="rgb(156, 163, 175)" />
<Text size="sm" color="text-gray-400">{car}</Text>
</Stack>
{strengthOfField && (
<Stack direction="row" align="center" gap={1}>
<Icon icon={Zap} size={3.5} color="rgb(245, 158, 11)" />
<Text size="sm" color="text-gray-400">SOF {strengthOfField}</Text>
</Stack>
)}
</Stack>
</Box>
{/* Status Badge */}
<Badge variant={statusConfig.variant}>
<Icon icon={StatusIcon} size={3.5} />
{statusConfig.label}
</Badge>
</Stack>
{/* League Link */}
{leagueName && leagueHref && (
<Box mt={3} pt={3} borderTop borderColor="border-charcoal-outline" bgOpacity={0.5}>
<Link
href={leagueHref}
onClick={(e) => e.stopPropagation()}
variant="primary"
size="sm"
>
<Icon icon={Trophy} size={3.5} mr={2} />
{leagueName}
<Icon icon={ArrowRight} size={3} ml={2} />
</Link>
</Box>
)}
</Box>
{/* Arrow */}
<Icon icon={ChevronRight} size={5} color="rgb(115, 115, 115)" flexShrink={0} />
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,74 @@
import { routes } from '@/lib/routing/RouteConfig';
import { RaceListItem as UiRaceListItem } from '@/components/races/RaceListItem';
import { CheckCircle2, Clock, PlayCircle, XCircle } from 'lucide-react';
interface Race {
id: string;
track: string;
car: string;
scheduledAt: string;
status: 'scheduled' | 'running' | 'completed' | 'cancelled';
sessionType: string;
leagueId?: string | null;
leagueName?: string | null;
strengthOfField?: number | null;
}
interface RaceListItemProps {
race: Race;
onClick: (id: string) => void;
}
export function RaceListItem({ race, onClick }: RaceListItemProps) {
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',
},
};
const config = statusConfig[race.status];
const formatTime = (date: string) => {
return new Date(date).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
});
};
const date = new Date(race.scheduledAt);
return (
<UiRaceListItem
track={race.track}
car={race.car}
dateLabel={date.toLocaleDateString('en-US', { month: 'short' })}
dayLabel={date.getDate().toString()}
timeLabel={formatTime(race.scheduledAt)}
status={race.status}
leagueName={race.leagueName}
leagueHref={race.leagueId ? routes.league.detail(race.leagueId) : undefined}
strengthOfField={race.strengthOfField}
onClick={() => onClick(race.id)}
statusConfig={config}
/>
);
}

View File

@@ -0,0 +1,39 @@
import { RaceResultViewModel } from '@/lib/view-models/RaceResultViewModel';
import { RaceResultCard as UiRaceResultCard } from '@/ui/RaceResultCard';
interface RaceResultCardProps {
race: {
id: string;
track: string;
car: string;
scheduledAt: string;
};
result: RaceResultViewModel;
league?: {
name: string;
};
showLeague?: boolean;
}
export function RaceResultCard({
race,
result,
league,
showLeague = true,
}: RaceResultCardProps) {
return (
<UiRaceResultCard
raceId={race.id}
track={race.track}
car={race.car}
scheduledAt={race.scheduledAt}
position={result.position}
startPosition={result.startPosition}
incidents={result.incidents}
leagueName={league?.name}
showLeague={showLeague}
/>
);
}

View File

@@ -0,0 +1,107 @@
import { CountryFlagDisplay } from '@/lib/display-objects/CountryFlagDisplay';
import { Box } from '@/ui/Box';
import { Image } from '@/ui/Image';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
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;
const getPositionColor = (pos: number) => {
if (pos === 1) return { bg: 'bg-yellow-500/20', color: 'text-yellow-400' };
if (pos === 2) return { bg: 'bg-gray-400/20', color: 'text-gray-300' };
if (pos === 3) return { bg: 'bg-amber-600/20', color: 'text-amber-600' };
return { bg: 'bg-iron-gray/50', color: 'text-gray-500' };
};
const posConfig = getPositionColor(position);
return (
<Surface
variant={isCurrentUser ? 'muted' : 'dark'}
rounded="xl"
border={isCurrentUser}
padding={3}
className={isCurrentUser ? 'border-primary-blue/40' : ''}
style={isCurrentUser ? { background: 'linear-gradient(to right, rgba(59, 130, 246, 0.2), rgba(59, 130, 246, 0.1), transparent)' } : {}}
>
<Stack direction="row" align="center" gap={3}>
{/* Position */}
<Box
width="10"
height="10"
rounded="lg"
display="flex"
center
className={`${posConfig.bg} ${posConfig.color}`}
>
<Text weight="bold">{position}</Text>
</Box>
{/* Avatar */}
<Box position="relative" flexShrink={0}>
<Box width="10" height="10" rounded="full" overflow="hidden" border={isCurrentUser} borderColor="border-primary-blue/50" className={isCurrentUser ? 'border-2' : ''}>
<Image src={driverAvatar} alt={driverName} width={40} height={40} fullWidth fullHeight objectFit="cover" />
</Box>
<Box position="absolute" bottom="-0.5" right="-0.5" width="5" height="5" rounded="full" bg="bg-deep-graphite" display="flex" center style={{ fontSize: '0.625rem' }}>
{CountryFlagDisplay.fromCountryCode(country).toString()}
</Box>
</Box>
{/* Driver Info */}
<Box flexGrow={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 && (
<Box px={2} py={0.5} rounded="full" bg="bg-primary-blue">
<Text size="xs" weight="bold" color="text-white">YOU</Text>
</Box>
)}
</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 textAlign="right" style={{ 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 */}
<Box p={2} rounded="lg" border={true} borderColor="border-warning-amber/20" bg="bg-warning-amber/10" textAlign="center" style={{ minWidth: '3.5rem' }}>
<Text size="xs" color="text-gray-500" block>PTS</Text>
<Text size="sm" weight="bold" color="text-warning-amber">{points}</Text>
</Box>
</Stack>
</Surface>
);
}

View File

@@ -0,0 +1,106 @@
import React from 'react';
import { Clock, Trophy, Users } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Card } from '@/ui/Card';
import { Stack } from '@/ui/Stack';
import { Icon } from '@/ui/Icon';
import { SidebarRaceItem } from '@/ui/SidebarRaceItem';
import { SidebarActionLink } from '@/ui/SidebarActionLink';
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} icon={<Icon icon={Clock} size={4} color="rgb(59, 130, 246)" />}>
Next Up
</Heading>
<Text size="xs" color="text-gray-500">This week</Text>
</Stack>
{upcomingRaces.length === 0 ? (
<Box py={4} textAlign="center">
<Text size="sm" color="text-gray-400">No races scheduled this week</Text>
</Box>
) : (
<Stack gap={3}>
{upcomingRaces.map((race) => (
<SidebarRaceItem
key={race.id}
race={{
id: race.id,
track: race.track,
scheduledAt: race.scheduledAt
}}
onClick={() => onRaceClick(race.id)}
/>
))}
</Stack>
)}
</Stack>
</Card>
{/* Recent Results */}
<Card>
<Stack gap={4}>
<Heading level={3} icon={<Icon icon={Trophy} size={4} color="rgb(245, 158, 11)" />}>
Recent Results
</Heading>
{recentResults.length === 0 ? (
<Box py={4} textAlign="center">
<Text size="sm" color="text-gray-400">No completed races yet</Text>
</Box>
) : (
<Stack gap={3}>
{recentResults.map((race) => (
<SidebarRaceItem
key={race.id}
race={{
id: race.id,
track: race.track,
scheduledAt: race.scheduledAt
}}
onClick={() => onRaceClick(race.id)}
/>
))}
</Stack>
)}
</Stack>
</Card>
{/* Quick Actions */}
<Card>
<Stack gap={4}>
<Heading level={3}>Quick Actions</Heading>
<Stack gap={2}>
<SidebarActionLink
href={routes.public.leagues}
icon={Users}
label="Browse Leagues"
/>
<SidebarActionLink
href={routes.public.leaderboards}
icon={Trophy}
label="View Leaderboards"
iconColor="text-warning-amber"
iconBgColor="bg-warning-amber/10"
/>
</Stack>
</Stack>
</Card>
</Stack>
);
}

View File

@@ -0,0 +1,62 @@
import { routes } from '@/lib/routing/RouteConfig';
import { Card } from '@/ui/Card';
import { MinimalEmptyState } from '@/components/shared/state/EmptyState';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Link } from '@/ui/Link';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { UpcomingRaceItem } from '@/ui/UpcomingRaceItem';
import { UpcomingRacesList } from '@/components/races/UpcomingRacesList';
import { Calendar } from 'lucide-react';
interface UpcomingRace {
id: string;
track: string;
car: string;
formattedDate: string;
formattedTime: string;
isMyLeague: boolean;
}
interface UpcomingRacesProps {
races: UpcomingRace[];
hasRaces: boolean;
}
export function UpcomingRaces({ races, hasRaces }: UpcomingRacesProps) {
return (
<Card>
<Stack direction="row" align="center" justify="between" mb={4}>
<Heading level={3} icon={<Icon icon={Calendar} size={5} color="var(--primary-blue)" />}>
Upcoming Races
</Heading>
<Link href={routes.public.races} variant="primary">
<Text size="xs">View all</Text>
</Link>
</Stack>
{hasRaces ? (
<UpcomingRacesList>
{races.slice(0, 5).map((race) => (
<UpcomingRaceItem
key={race.id}
track={race.track}
car={race.car}
formattedDate={race.formattedDate}
formattedTime={race.formattedTime}
isMyLeague={race.isMyLeague}
/>
))}
</UpcomingRacesList>
) : (
<MinimalEmptyState
icon={Calendar}
title="No upcoming races"
description="Check back later for new events"
/>
)}
</Card>
);
}

View File

@@ -0,0 +1,14 @@
import React, { ReactNode } from 'react';
import { Stack } from '@/ui/Stack';
interface UpcomingRacesListProps {
children: ReactNode;
}
export function UpcomingRacesList({ children }: UpcomingRacesListProps) {
return (
<Stack gap={3}>
{children}
</Stack>
);
}

View File

@@ -0,0 +1,53 @@
import React from 'react';
import { Card } from '@/ui/Card';
import { Button } from '@/ui/Button';
import { Stack } from '@/ui/Stack';
import { Heading } from '@/ui/Heading';
import { RaceSummaryItem } from '@/ui/RaceSummaryItem';
type UpcomingRace = {
id: string;
track: string;
car: string;
scheduledAt: string | Date;
};
interface UpcomingRacesSidebarProps {
races: UpcomingRace[];
}
export function UpcomingRacesSidebar({ races }: UpcomingRacesSidebarProps) {
if (!races.length) {
return null;
}
return (
<Card bg="bg-iron-gray/80" p={4}>
<Stack direction="row" align="baseline" justify="between" mb={3}>
<Heading level={3}>Upcoming races</Heading>
<Button
as="a"
href="/races"
variant="secondary"
size="sm"
>
View all
</Button>
</Stack>
<Stack gap={3}>
{races.slice(0, 4).map((race) => {
const scheduledAt = typeof race.scheduledAt === 'string' ? new Date(race.scheduledAt) : race.scheduledAt;
return (
<RaceSummaryItem
key={race.id}
track={race.track}
meta={race.car}
date={scheduledAt}
/>
);
})}
</Stack>
</Card>
);
}