website refactor

This commit is contained in:
2026-01-20 18:28:11 +01:00
parent b39b098e6b
commit 444afda435
24 changed files with 971 additions and 277 deletions

View File

@@ -1,72 +1,67 @@
'use client';
import { useState } from 'react';
import { useState, useMemo } from 'react';
import { useRouter } from 'next/navigation';
import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper';
import { RacesAllTemplate } from '@/templates/RacesAllTemplate';
import { RacesIndexTemplate } from '@/templates/RacesIndexTemplate';
import { useAllRacesPageData } from '@/hooks/race/useAllRacesPageData';
import { type RacesViewData, type RaceViewData } from '@/lib/view-data/RacesViewData';
import { Flag } from 'lucide-react';
import { routes } from '@/lib/routing/RouteConfig';
import type { RacesViewData, RaceViewData } from '@/lib/view-data/RacesViewData';
import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts';
const ITEMS_PER_PAGE = 10;
import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper';
import { Flag } from 'lucide-react';
export function RacesAllPageClient({ viewData: initialViewData }: ClientWrapperProps<RacesViewData>) {
const router = useRouter();
// Client-side state for filters and pagination
const [currentPage, setCurrentPage] = useState(1);
const [statusFilter, setStatusFilter] = useState<'scheduled' | 'running' | 'completed' | 'cancelled' | 'all'>('all');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [leagueFilter, setLeagueFilter] = useState<string>('all');
const [searchQuery, setSearchQuery] = useState('');
const [showFilters, setShowFilters] = useState(false);
const [timeFilter, setTimeFilter] = useState<string>('upcoming');
const [showFilterModal, setShowFilterModal] = useState(false);
// Use React Query hook
const { data: pageData, isLoading, error, refetch } = useAllRacesPageData(initialViewData);
// Transform data
const races: RaceViewData[] = pageData?.races ?? [];
const filteredRaces = useMemo(() => {
const now = new Date();
const races: RaceViewData[] = pageData?.races ?? [];
return races.filter((race) => {
if (statusFilter !== 'all' && race.status.toLowerCase() !== statusFilter.toLowerCase()) return false;
if (leagueFilter !== 'all' && race.leagueId !== leagueFilter) return false;
const scheduledAt = new Date(race.scheduledAt);
const isActuallyUpcoming = scheduledAt > now && race.status.toLowerCase() === 'scheduled';
const isActuallyLive = race.status.toLowerCase() === 'running';
const isActuallyPast = scheduledAt < now || race.status.toLowerCase() === 'completed' || race.status.toLowerCase() === 'cancelled';
// Filter and paginate (Note: This should be done by API per contract)
const filteredRaces = races.filter((race: RaceViewData) => {
if (statusFilter !== 'all' && race.status !== statusFilter) {
return false;
}
if (timeFilter === 'upcoming' && !isActuallyUpcoming) return false;
if (timeFilter === 'live' && !isActuallyLive) return false;
if (timeFilter === 'past' && !isActuallyPast) return false;
return true;
});
}, [pageData?.races, statusFilter, leagueFilter, timeFilter]);
if (leagueFilter !== 'all' && race.leagueId !== leagueFilter) {
return false;
}
const nextUpRace = useMemo(() => {
const now = new Date();
return filteredRaces.find(r => new Date(r.scheduledAt) > now && r.status.toLowerCase() === 'scheduled');
}, [filteredRaces]);
if (searchQuery) {
const query = searchQuery.toLowerCase();
const matchesTrack = race.track.toLowerCase().includes(query);
const matchesCar = race.car.toLowerCase().includes(query);
const matchesLeague = race.leagueName?.toLowerCase().includes(query);
if (!matchesTrack && !matchesCar && !matchesLeague) {
return false;
const racesByDate = useMemo(() => {
const grouped = new Map<string, typeof filteredRaces[0][]>();
filteredRaces.forEach((race) => {
const dateKey = race.scheduledAt.split('T')[0]!;
if (!grouped.has(dateKey)) {
grouped.set(dateKey, []);
}
}
grouped.get(dateKey)!.push(race);
});
return true;
});
const totalPages = Math.ceil(filteredRaces.length / ITEMS_PER_PAGE);
const paginatedRaces = filteredRaces.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE);
// Actions
const handleRaceClick = (raceId: string) => {
router.push(routes.race.detail(raceId));
};
const handleLeagueClick = (leagueId: string) => {
router.push(routes.league.detail(leagueId));
};
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
return Array.from(grouped.entries())
.sort(([a], [b]) => timeFilter === 'past' ? b.localeCompare(a) : a.localeCompare(b))
.map(([dateKey, dayRaces]) => ({
dateKey,
dateLabel: dayRaces[0]?.scheduledAtLabel || '',
races: dayRaces,
}));
}, [filteredRaces, timeFilter]);
return (
<StatefulPageWrapper
@@ -75,27 +70,22 @@ export function RacesAllPageClient({ viewData: initialViewData }: ClientWrapperP
error={error as Error | null}
retry={refetch}
Template={() => pageData ? (
<RacesAllTemplate
viewData={pageData}
races={paginatedRaces}
totalFilteredCount={filteredRaces.length}
isLoading={false}
currentPage={currentPage}
totalPages={totalPages}
itemsPerPage={ITEMS_PER_PAGE}
onPageChange={handlePageChange}
<RacesIndexTemplate
viewData={{
...pageData,
races: filteredRaces,
racesByDate,
nextUpRace,
}}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
leagueFilter={leagueFilter}
setLeagueFilter={setLeagueFilter}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
showFilters={showFilters}
setShowFilters={setShowFilters}
timeFilter={timeFilter}
setTimeFilter={setTimeFilter}
showFilterModal={showFilterModal}
setShowFilterModal={setShowFilterModal}
onRaceClick={handleRaceClick}
onLeagueClick={handleLeagueClick}
onRaceClick={(id) => router.push(`/races/${id}`)}
/>
) : null}
loading={{ variant: 'skeleton', message: 'Loading races...' }}

View File

@@ -2,30 +2,54 @@
import { useState, useMemo } from 'react';
import { useRouter } from 'next/navigation';
import { RacesTemplate, type TimeFilter, type RaceStatusFilter } from '@/templates/RacesTemplate';
import { RacesIndexTemplate } from '@/templates/RacesIndexTemplate';
import type { RacesViewData } from '@/lib/view-data/RacesViewData';
import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts';
export function RacesPageClient({ viewData }: ClientWrapperProps<RacesViewData>) {
const router = useRouter();
const [statusFilter, setStatusFilter] = useState<RaceStatusFilter>('all');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [leagueFilter, setLeagueFilter] = useState<string>('all');
const [timeFilter, setTimeFilter] = useState<TimeFilter>('upcoming');
const [timeFilter, setTimeFilter] = useState<string>('upcoming');
const [showFilterModal, setShowFilterModal] = useState(false);
const filteredRaces = useMemo(() => {
const now = new Date();
return viewData.races.filter((race) => {
if (statusFilter !== 'all' && race.status !== statusFilter) return false;
// Status filter: case-insensitive match
if (statusFilter !== 'all' && race.status.toLowerCase() !== statusFilter.toLowerCase()) return false;
// League filter
if (leagueFilter !== 'all' && race.leagueId !== leagueFilter) return false;
if (timeFilter === 'upcoming' && !race.isUpcoming) return false;
if (timeFilter === 'live' && !race.isLive) return false;
if (timeFilter === 'past' && !race.isPast) return false;
// Time filter: ensure we are checking the correct flags
// Note: we also check the actual date to be safe against stale API flags
const scheduledAt = new Date(race.scheduledAt);
const isActuallyUpcoming = scheduledAt > now && race.status.toLowerCase() === 'scheduled';
const isActuallyLive = race.status.toLowerCase() === 'running';
const isActuallyPast = scheduledAt < now || race.status.toLowerCase() === 'completed' || race.status.toLowerCase() === 'cancelled';
if (timeFilter === 'upcoming' && !isActuallyUpcoming) return false;
if (timeFilter === 'live' && !isActuallyLive) return false;
if (timeFilter === 'past' && !isActuallyPast) return false;
return true;
});
}, [viewData.races, statusFilter, leagueFilter, timeFilter]);
const nextUpRace = useMemo(() => {
const now = new Date();
// Find the first upcoming race in the filtered list
return filteredRaces.find(r => new Date(r.scheduledAt) > now && r.status.toLowerCase() === 'scheduled');
}, [filteredRaces]);
const racesByDate = useMemo(() => {
const grouped = new Map<string, typeof filteredRaces[0][]>();
// If we have a "Next Up" race and we are in upcoming view, we might want to exclude it from the list
// to avoid duplication, but usually it's better to keep the list complete.
// For this redesign, we keep the list complete for scanning.
filteredRaces.forEach((race) => {
const dateKey = race.scheduledAt.split('T')[0]!;
if (!grouped.has(dateKey)) {
@@ -33,19 +57,23 @@ export function RacesPageClient({ viewData }: ClientWrapperProps<RacesViewData>)
}
grouped.get(dateKey)!.push(race);
});
return Array.from(grouped.entries()).map(([dateKey, dayRaces]) => ({
dateKey,
dateLabel: dayRaces[0]?.scheduledAtLabel || '',
races: dayRaces,
}));
}, [filteredRaces]);
return Array.from(grouped.entries())
.sort(([a], [b]) => timeFilter === 'past' ? b.localeCompare(a) : a.localeCompare(b))
.map(([dateKey, dayRaces]) => ({
dateKey,
dateLabel: dayRaces[0]?.scheduledAtLabel || '',
races: dayRaces,
}));
}, [filteredRaces, timeFilter]);
return (
<RacesTemplate
<RacesIndexTemplate
viewData={{
...viewData,
races: filteredRaces,
racesByDate,
nextUpRace,
}}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
@@ -56,9 +84,6 @@ export function RacesPageClient({ viewData }: ClientWrapperProps<RacesViewData>)
showFilterModal={showFilterModal}
setShowFilterModal={setShowFilterModal}
onRaceClick={(id) => router.push(`/races/${id}`)}
onLeagueClick={(id) => router.push(`/leagues/${id}`)}
onWithdraw={(id) => console.log('Withdraw', id)}
onCancel={(id) => console.log('Cancel', id)}
/>
);
}

View File

@@ -1,9 +1,10 @@
'use client';
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Panel } from '@/ui/Panel';
import { ChevronRight, PlayCircle } from 'lucide-react';
interface LiveRaceItemProps {
@@ -14,30 +15,21 @@ interface LiveRaceItemProps {
export function LiveRaceItem({ track, leagueName, onClick }: LiveRaceItemProps) {
return (
<Box
<Panel
variant="precision"
padding="sm"
onClick={onClick}
display="flex"
alignItems="center"
justifyContent="between"
p={4}
bg="bg-deep-graphite/80"
rounded="lg"
border
borderColor="border-performance-green/20"
cursor="pointer"
hoverBorderColor="performance-green/40"
transition
>
<Box display="flex" alignItems="center" gap={4}>
<Box p={2} bg="bg-performance-green/20" rounded="lg">
<Icon icon={PlayCircle} size={5} color="rgb(16, 185, 129)" />
</Box>
<Box>
<Heading level={3}>{track}</Heading>
<Text size="sm" color="text-gray-400">{leagueName}</Text>
</Box>
</Box>
<Icon icon={ChevronRight} size={5} color="rgb(156, 163, 175)" />
</Box>
<Stack direction="row" align="center" justify="between" fullWidth>
<Stack direction="row" align="center" gap={4}>
<Icon icon={PlayCircle} size={5} intent="success" animate="pulse" />
<Stack gap={0.5}>
<Heading level={5} weight="bold">{track}</Heading>
<Text size="xs" variant="low" uppercase letterSpacing="widest">{leagueName}</Text>
</Stack>
</Stack>
<Icon icon={ChevronRight} size={4} intent="low" />
</Stack>
</Panel>
);
}

View File

@@ -1,9 +1,12 @@
'use client';
import { LiveRaceItem } from '@/components/races/LiveRaceItem';
import type { RaceViewData } from '@/lib/view-data/RacesViewData';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Panel } from '@/ui/Panel';
import { Icon } from '@/ui/Icon';
import { Zap } from 'lucide-react';
interface LiveRacesBannerProps {
liveRaces: RaceViewData[];
@@ -14,45 +17,26 @@ export function LiveRacesBanner({ liveRaces, onRaceClick }: LiveRacesBannerProps
if (liveRaces.length === 0) return null;
return (
<Stack
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)"
>
<Stack
position="absolute"
top="0"
right="0"
w="32"
h="32"
bg="bg-performance-green/20"
rounded="full"
blur="xl"
/>
<Stack position="relative" zIndex={10}>
<Stack mb={4}>
<Stack direction="row" align="center" gap={2} bg="bg-performance-green/20" px={3} py={1} rounded="full" w="fit">
<Stack w="2" h="2" bg="bg-performance-green" rounded="full" />
<Text weight="semibold" size="sm" color="text-performance-green">LIVE NOW</Text>
</Stack>
<Panel variant="glass" padding="md">
<Stack gap={4}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Zap} size={4} intent="success" animate="pulse" />
<Text weight="bold" size="sm" variant="success" uppercase letterSpacing="widest">
Live Sessions
</Text>
</Stack>
<Stack gap={3}>
<Stack gap={2}>
{liveRaces.map((race) => (
<LiveRaceItem
key={race.id}
track={race.track}
leagueName={race.leagueName ?? 'Unknown League'}
leagueName={race.leagueName ?? 'Official'}
onClick={() => onRaceClick(race.id)}
/>
))}
</Stack>
</Stack>
</Stack>
</Panel>
);
}

View File

@@ -0,0 +1,77 @@
'use client';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Surface } from '@/ui/Surface';
import { Icon } from '@/ui/Icon';
import { Clock, MapPin, Car, ChevronRight } from 'lucide-react';
import { Button } from '@/ui/Button';
import Link from 'next/link';
interface NextUpRacePanelProps {
race: any;
onRaceClick: (id: string) => void;
}
export function NextUpRacePanel({ race, onRaceClick }: NextUpRacePanelProps) {
if (!race) return null;
return (
<Stack gap={3}>
<Text size="xs" variant="low" weight="bold" uppercase letterSpacing="widest" paddingX={1}>
Next Up
</Text>
<Surface
as={Link}
href={`/races/${race.id}`}
variant="precision"
padding="lg"
onClick={(e: React.MouseEvent) => {
e.preventDefault();
onRaceClick(race.id);
}}
cursor="pointer"
hoverBg="rgba(255, 255, 255, 0.02)"
display="block"
style={{ textDecoration: 'none', color: 'inherit' }}
>
<Stack direction={{ base: 'col', md: 'row' }} justify="between" align={{ base: 'start', md: 'center' }} gap={6}>
<Stack gap={4} flex={1}>
<Stack gap={1}>
<Text size="xs" variant="primary" weight="bold" uppercase letterSpacing="widest">
{race.leagueName}
</Text>
<Text size="2xl" weight="bold">
{race.track}
</Text>
</Stack>
<Stack direction="row" gap={6} wrap>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Clock} size={4} intent="low" />
<Text size="sm" weight="medium">{race.timeLabel}</Text>
</Stack>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Car} size={4} intent="low" />
<Text size="sm" weight="medium">{race.car}</Text>
</Stack>
</Stack>
</Stack>
<Button
variant="primary"
onClick={(e) => {
e.stopPropagation();
onRaceClick(race.id);
}}
icon={<Icon icon={ChevronRight} size={4} />}
>
View Details
</Button>
</Stack>
</Surface>
</Stack>
);
}

View File

@@ -1,4 +1,4 @@
'use client';
import type { TimeFilter } from '@/templates/RacesTemplate';
import { Button } from '@/ui/Button';
@@ -6,6 +6,8 @@ import { Card } from '@/ui/Card';
import { FilterGroup } from '@/ui/FilterGroup';
import { Select } from '@/ui/Select';
import { Stack } from '@/ui/Stack';
import { Icon } from '@/ui/Icon';
import { SlidersHorizontal } from 'lucide-react';
interface RaceFilterBarProps {
timeFilter: TimeFilter;
@@ -31,32 +33,36 @@ export function RaceFilterBar({
const timeOptions = [
{ id: 'upcoming', label: 'Upcoming' },
{ id: 'live', label: 'Live', indicatorColor: 'bg-performance-green' },
{ id: 'live', label: 'Live' },
{ 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)}
/>
<Card variant="precision" padding="sm">
<Stack direction="row" align="center" justify="between" wrap gap={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}
/>
<Select
value={leagueFilter}
onChange={(e) => setLeagueFilter(e.target.value)}
options={leagueOptions}
fullWidth={false}
/>
</Stack>
<Button
variant="secondary"
size="sm"
onClick={onShowMoreFilters}
icon={<Icon icon={SlidersHorizontal} size={3} />}
>
More Filters
Filters
</Button>
</Stack>
</Card>

View File

@@ -7,6 +7,7 @@ import { Modal } from '@/components/shared/Modal';
import { Stack } from '@/ui/Stack';
import { Select } from '@/ui/Select';
import { Text } from '@/ui/Text';
import { StatusDot } from '@/ui/StatusDot';
import { Filter, Search } from 'lucide-react';
export type TimeFilter = 'all' | 'upcoming' | 'live' | 'past';
@@ -48,9 +49,9 @@ export function RaceFilterModal({
isOpen={isOpen}
onOpenChange={(open) => !open && onClose()}
title="Filters"
icon={<Icon icon={Filter} size={5} color="text-primary-accent" />}
icon={<Icon icon={Filter} size={5} intent="primary" />}
>
<Stack gap={4}>
<Stack gap={6}>
{/* Search */}
{showSearch && (
<Input
@@ -59,15 +60,15 @@ export function RaceFilterModal({
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Track, car, or league..."
icon={<Icon icon={Search} size={4} color="text-gray-500" />}
icon={<Icon icon={Search} size={4} intent="low" />}
/>
)}
{/* Time Filter */}
{showTimeFilter && (
<Stack>
<Text as="label" size="sm" color="text-gray-400" block mb={2}>Time</Text>
<Stack display="flex" flexWrap="wrap" gap={2}>
<Stack gap={2}>
<Text as="label" size="xs" variant="low" weight="bold" uppercase letterSpacing="widest">Time</Text>
<Stack direction="row" wrap gap={2}>
{(['upcoming', 'live', 'past', 'all'] as TimeFilter[]).map(filter => (
<Button
key={filter}
@@ -75,7 +76,11 @@ export function RaceFilterModal({
size="sm"
onClick={() => setTimeFilter(filter)}
>
{filter === 'live' && <Stack as="span" width="2" height="2" bg="bg-success-green" rounded="full" mr={1.5} animate="pulse" />}
{filter === 'live' && (
<Stack mr={2}>
<StatusDot intent="success" size={1.5} pulse />
</Stack>
)}
{filter.charAt(0).toUpperCase() + filter.slice(1)}
</Button>
))}
@@ -111,7 +116,7 @@ export function RaceFilterModal({
{/* Clear Filters */}
{(statusFilter !== 'all' || leagueFilter !== 'all' || searchQuery || (showTimeFilter && timeFilter !== 'upcoming')) && (
<Button
variant="secondary"
variant="ghost"
onClick={() => {
setStatusFilter('all');
setLeagueFilter('all');

View File

@@ -0,0 +1,88 @@
'use client';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { RaceRow } from '@/ui/RaceRow';
import { RaceRowCell } from '@/ui/RaceRowCell';
import { StatusBadge } from '@/ui/StatusBadge';
import { Icon } from '@/ui/Icon';
import { Clock, MapPin, Car as CarIcon } from 'lucide-react';
import Link from 'next/link';
interface RaceListRowProps {
race: {
id: string;
track: string;
car: string;
leagueName: string;
timeLabel: string;
status: string;
isLive: boolean;
isPast: boolean;
};
onClick: (id: string) => void;
}
export function RaceListRow({ race, onClick }: RaceListRowProps) {
const status = race.isLive ? 'live' : (race.isPast ? 'past' : 'upcoming');
const emphasis = race.isPast ? 'low' : 'medium';
return (
<RaceRow
as={Link}
href={`/races/${race.id}`}
onClick={(e: React.MouseEvent) => {
e.preventDefault();
onClick(race.id);
}}
status={status}
emphasis={emphasis}
>
<RaceRowCell width="5rem" align="center">
<Stack gap={0.5} align="center">
<Text size="sm" weight="bold" variant={race.isLive ? 'success' : 'high'}>
{race.timeLabel}
</Text>
{race.isLive && (
<Text size="xs" variant="success" weight="bold" uppercase>Live</Text>
)}
</Stack>
</RaceRowCell>
<RaceRowCell label="Track">
<Stack direction="row" align="center" gap={2}>
<Icon icon={MapPin} size={3} intent="low" />
<Text size="sm" weight="medium" truncate>{race.track}</Text>
</Stack>
</RaceRowCell>
<RaceRowCell label="League" hideOnMobile>
<Text size="sm" variant="low" truncate>{race.leagueName}</Text>
</RaceRowCell>
<RaceRowCell label="Car" hideOnMobile>
<Stack direction="row" align="center" gap={2}>
<Icon icon={CarIcon} size={3} intent="low" />
<Text size="sm" variant="low" truncate>{race.car}</Text>
</Stack>
</RaceRowCell>
<RaceRowCell width="6rem" align="right">
<StatusBadge variant={getStatusVariant(race.status)}>
{race.status}
</StatusBadge>
</RaceRowCell>
</RaceRow>
);
}
function getStatusVariant(status: string): any {
switch (status.toLowerCase()) {
case 'running': return 'success';
case 'completed': return 'neutral';
case 'cancelled': return 'error';
case 'scheduled': return 'info';
default: return 'neutral';
}
}

View File

@@ -4,7 +4,7 @@ import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Grid } from '@/ui/Grid';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { Panel } from '@/ui/Panel';
import { Text } from '@/ui/Text';
import { CalendarDays, Clock, Flag, LucideIcon, Trophy, Zap } from 'lucide-react';
@@ -22,57 +22,50 @@ export function RacePageHeader({
completedCount,
}: RacePageHeaderProps) {
return (
<Surface
bg="bg-surface-charcoal"
rounded="xl"
border
borderColor="border-outline-steel"
padding={6}
position="relative"
overflow="hidden"
<Panel
variant="precision"
padding="lg"
>
{/* Background Accent */}
<Stack
position="absolute"
top={0}
left={0}
right={0}
height="1"
bg="bg-primary-accent"
/>
<Stack gap={6}>
<Stack gap={8}>
<Stack gap={2}>
<Stack direction="row" align="center" gap={3}>
<Icon icon={Flag} size={6} color="var(--primary-accent)" />
<Heading level={1}>RACE DASHBOARD</Heading>
<Icon icon={Flag} size={6} intent="primary" />
<Heading level={1} uppercase weight="bold">Race Dashboard</Heading>
</Stack>
<Text color="text-gray-400" size="sm">
<Text variant="low" size="sm">
Precision tracking for upcoming sessions and live events.
</Text>
</Stack>
<Grid cols={2} mdCols={4} gap={4}>
<StatItem icon={CalendarDays} label="TOTAL SESSIONS" value={totalCount} />
<StatItem icon={Clock} label="SCHEDULED" value={scheduledCount} color="text-primary-accent" />
<StatItem icon={Zap} label="LIVE NOW" value={runningCount} color="text-success-green" />
<StatItem icon={Trophy} label="COMPLETED" value={completedCount} color="text-gray-400" />
<Grid cols={2} mdCols={4} gap={6}>
<StatItem icon={CalendarDays} label="Total Sessions" value={totalCount} />
<StatItem icon={Clock} label="Scheduled" value={scheduledCount} variant="primary" />
<StatItem icon={Zap} label="Live Now" value={runningCount} variant="success" />
<StatItem icon={Trophy} label="Completed" value={completedCount} variant="low" />
</Grid>
</Stack>
</Surface>
</Panel>
);
}
function StatItem({ icon, label, value, color = 'text-white' }: { icon: LucideIcon, label: string, value: number, color?: string }) {
function StatItem({
icon,
label,
value,
variant = 'high'
}: {
icon: LucideIcon,
label: string,
value: number,
variant?: 'high' | 'low' | 'primary' | 'success' | 'warning' | 'critical'
}) {
return (
<Stack p={4} bg="bg-base-black" bgOpacity={0.5} border borderColor="border-outline-steel">
<Stack gap={1}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={icon} size={3} color={color === 'text-white' ? '#9ca3af' : undefined} groupHoverTextColor={color !== 'text-white' ? color : undefined} />
<Text size="xs" color="text-gray-500" weight="bold" uppercase>{label}</Text>
</Stack>
<Text size="2xl" weight="bold" color={color}>{value}</Text>
<Stack gap={2}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={icon} size={3} intent={variant === 'high' ? 'low' : variant} />
<Text size="xs" variant="low" weight="bold" uppercase letterSpacing="widest">{label}</Text>
</Stack>
<Text size="3xl" weight="bold" variant={variant} mono>{value}</Text>
</Stack>
);
}

View File

@@ -4,6 +4,7 @@ import React from 'react';
import { Text } from '@/ui/Text';
import { Table, TableHead, TableBody, TableRow, TableHeader, TableCell } from '@/ui/Table';
import { SessionStatusBadge, type SessionStatus } from './SessionStatusBadge';
import { Stack } from '@/ui/Stack';
interface RaceRow {
id: string;
@@ -25,8 +26,7 @@ export function RaceScheduleTable({ races, onRaceClick }: RaceScheduleTableProps
<TableHead>
<TableRow>
<TableHeader>Time</TableHeader>
<TableHeader>Track</TableHeader>
<TableHeader>Car</TableHeader>
<TableHeader>Session Details</TableHeader>
<TableHeader>League</TableHeader>
<TableHeader textAlign="right">Status</TableHeader>
</TableRow>
@@ -39,21 +39,23 @@ export function RaceScheduleTable({ races, onRaceClick }: RaceScheduleTableProps
clickable
>
<TableCell>
<Text size="xs" variant="telemetry" weight="bold">{race.time}</Text>
<Text size="xs" variant="telemetry" weight="bold" mono>{race.time}</Text>
</TableCell>
<TableCell>
<Text size="sm" weight="bold" variant="high">
{race.track}
</Text>
<Stack gap={0.5}>
<Text size="sm" weight="bold" variant="high">
{race.track}
</Text>
<Text size="xs" variant="low" uppercase letterSpacing="widest">{race.car}</Text>
</Stack>
</TableCell>
<TableCell>
<Text size="xs" variant="low">{race.car}</Text>
</TableCell>
<TableCell>
<Text size="xs" variant="low">{race.leagueName || 'Official'}</Text>
<Text size="xs" variant="low" weight="medium">{race.leagueName || 'Official'}</Text>
</TableCell>
<TableCell textAlign="right">
<SessionStatusBadge status={race.status} />
<Stack direction="row" justify="end">
<SessionStatusBadge status={race.status} />
</Stack>
</TableCell>
</TableRow>
))}

View File

@@ -4,12 +4,11 @@ import { SidebarRaceItem } from '@/components/races/SidebarRaceItem';
import { routes } from '@/lib/routing/RouteConfig';
import type { RaceViewData } from '@/lib/view-data/RacesViewData';
import { Panel } from '@/ui/Panel';
import { Icon } from '@/ui/Icon';
import { SidebarActionLink } from '@/ui/SidebarActionLink';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Box } from '@/ui/Box';
import { Clock, Trophy, Users } from 'lucide-react';
import { Trophy, Users } from 'lucide-react';
import React from 'react';
interface RaceSidebarProps {
@@ -23,15 +22,16 @@ export function RaceSidebar({ upcomingRaces, recentResults, onRaceClick }: RaceS
<Stack gap={6}>
{/* Upcoming This Week */}
<Panel
variant="precision"
title="Next Up"
description="This week"
description="Scheduled sessions"
>
{upcomingRaces.length === 0 ? (
<Box paddingY={4} textAlign="center">
<Text size="sm" variant="low">No races scheduled this week</Text>
</Box>
) : (
<Stack gap={3}>
<Stack gap={1}>
{upcomingRaces.map((race) => (
<SidebarRaceItem
key={race.id}
@@ -49,14 +49,16 @@ export function RaceSidebar({ upcomingRaces, recentResults, onRaceClick }: RaceS
{/* Recent Results */}
<Panel
variant="precision"
title="Recent Results"
description="Latest finishes"
>
{recentResults.length === 0 ? (
<Box paddingY={4} textAlign="center">
<Text size="sm" variant="low">No completed races yet</Text>
</Box>
) : (
<Stack gap={3}>
<Stack gap={1}>
{recentResults.map((race) => (
<SidebarRaceItem
key={race.id}
@@ -73,7 +75,7 @@ export function RaceSidebar({ upcomingRaces, recentResults, onRaceClick }: RaceS
</Panel>
{/* Quick Actions */}
<Panel title="Quick Actions">
<Panel variant="precision" title="Quick Actions">
<Stack gap={2}>
<SidebarActionLink
href={routes.public.leagues}

View File

@@ -0,0 +1,69 @@
'use client';
import { Stack } from '@/ui/Stack';
import { Button } from '@/ui/Button';
import { Select } from '@/ui/Select';
import { Input } from '@/ui/Input';
import { Icon } from '@/ui/Icon';
import { Search, SlidersHorizontal } from 'lucide-react';
import { Surface } from '@/ui/Surface';
import { SegmentedControl } from '@/ui/SegmentedControl';
interface RacesCommandBarProps {
timeFilter: string;
setTimeFilter: (filter: any) => void;
leagueFilter: string;
setLeagueFilter: (filter: string) => void;
leagues: Array<{ id: string; name: string }>;
onShowMoreFilters: () => void;
}
export function RacesCommandBar({
timeFilter,
setTimeFilter,
leagueFilter,
setLeagueFilter,
leagues,
onShowMoreFilters,
}: RacesCommandBarProps) {
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' },
{ id: 'past', label: 'Past' },
{ id: 'all', label: 'All' },
];
return (
<Surface variant="precision" padding="sm">
<Stack direction={{ base: 'col', md: 'row' }} align="center" justify="between" gap={4}>
<Stack direction="row" align="center" gap={4} wrap>
<SegmentedControl
options={timeOptions}
activeId={timeFilter}
onChange={(id) => setTimeFilter(id)}
/>
<Select
value={leagueFilter}
onChange={(e) => setLeagueFilter(e.target.value)}
options={leagueOptions}
/>
</Stack>
<Button
variant="secondary"
size="sm"
onClick={onShowMoreFilters}
icon={<Icon icon={SlidersHorizontal} size={3} />}
>
Advanced Filters
</Button>
</Stack>
</Surface>
);
}

View File

@@ -0,0 +1,38 @@
'use client';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { RaceListRow } from './RaceListRow';
interface RacesDayGroupProps {
dateLabel: string;
races: any[];
onRaceClick: (id: string) => void;
}
export function RacesDayGroup({ dateLabel, races, onRaceClick }: RacesDayGroupProps) {
return (
<Stack gap={3}>
<Box
paddingX={4}
paddingY={2}
borderBottom
borderColor="var(--ui-color-border-muted)"
>
<Text size="xs" variant="low" weight="bold" uppercase letterSpacing="widest">
{dateLabel}
</Text>
</Box>
<Stack gap={1}>
{races.map(race => (
<RaceListRow
key={race.id}
race={race}
onClick={onRaceClick}
/>
))}
</Stack>
</Stack>
);
}

View File

@@ -0,0 +1,32 @@
'use client';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Surface } from '@/ui/Surface';
import { Icon } from '@/ui/Icon';
import { Search } from 'lucide-react';
export function RacesEmptyState() {
return (
<Surface variant="precision" padding="xl">
<Stack align="center" justify="center" gap={4} paddingY={12}>
<Box
width="3rem"
height="3rem"
display="flex"
alignItems="center"
justifyContent="center"
rounded="full"
bg="var(--ui-color-bg-surface-muted)"
>
<Icon icon={Search} size={6} intent="low" />
</Box>
<Stack gap={1} align="center">
<Text size="lg" weight="bold">No races found.</Text>
<Text variant="low" size="sm">No races match the current filters.</Text>
</Stack>
</Stack>
</Surface>
);
}

View File

@@ -0,0 +1,80 @@
'use thought';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Surface } from '@/ui/Surface';
import { Icon } from '@/ui/Icon';
import { Zap, ChevronRight } from 'lucide-react';
import Link from 'next/link';
interface RacesLiveRailProps {
liveRaces: any[];
onRaceClick: (id: string) => void;
}
export function RacesLiveRail({ liveRaces, onRaceClick }: RacesLiveRailProps) {
if (liveRaces.length === 0) return null;
return (
<Stack gap={3}>
<Stack direction="row" align="center" gap={2} paddingX={1}>
<Icon icon={Zap} size={3} intent="success" />
<Text size="xs" variant="success" weight="bold" uppercase letterSpacing="widest">
Live Now
</Text>
</Stack>
<Box
display="flex"
gap={4}
overflowX="auto"
paddingBottom={2}
hideScrollbar
>
{liveRaces.map(race => (
<Surface
key={race.id}
as={Link}
href={`/races/${race.id}`}
variant="precision"
padding="sm"
onClick={(e: React.MouseEvent) => {
e.preventDefault();
onRaceClick(race.id);
}}
cursor="pointer"
minWidth="280px"
position="relative"
hoverBg="rgba(255, 255, 255, 0.02)"
display="block"
style={{ textDecoration: 'none', color: 'inherit' }}
>
<Box
position="absolute"
top={0}
left={0}
right={0}
height="2px"
bg="var(--ui-color-intent-success)"
/>
<Stack gap={2}>
<Text size="xs" variant="low" weight="bold" uppercase truncate>
{race.leagueName}
</Text>
<Text size="sm" weight="bold" truncate>
{race.track}
</Text>
<Stack direction="row" align="center" justify="between">
<Text size="xs" variant="success" weight="bold">
{race.timeLabel}
</Text>
<Icon icon={ChevronRight} size={3} intent="low" />
</Stack>
</Stack>
</Surface>
))}
</Box>
</Stack>
);
}

View File

@@ -1,7 +1,9 @@
'use client';
import React from 'react';
import { Badge } from '@/ui/Badge';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { StatusDot } from '@/ui/StatusDot';
export type SessionStatus = 'scheduled' | 'running' | 'completed' | 'cancelled' | 'delayed';
@@ -10,34 +12,37 @@ interface SessionStatusBadgeProps {
}
export function SessionStatusBadge({ status }: SessionStatusBadgeProps) {
const config: Record<SessionStatus, { label: string; variant: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info' }> = {
const config: Record<SessionStatus, { label: string; intent: 'primary' | 'success' | 'telemetry' | 'critical' | 'warning' }> = {
scheduled: {
label: 'SCHEDULED',
variant: 'primary',
label: 'Scheduled',
intent: 'primary',
},
running: {
label: 'LIVE',
variant: 'success',
label: 'Live',
intent: 'success',
},
completed: {
label: 'COMPLETED',
variant: 'default',
label: 'Finished',
intent: 'telemetry',
},
cancelled: {
label: 'CANCELLED',
variant: 'danger',
label: 'Cancelled',
intent: 'critical',
},
delayed: {
label: 'DELAYED',
variant: 'warning',
label: 'Delayed',
intent: 'warning',
},
};
const { label, variant } = config[status] || config.scheduled;
const { label, intent } = config[status] || config.scheduled;
return (
<Badge variant={variant} size="sm">
{label}
</Badge>
<Stack direction="row" align="center" gap={2}>
<StatusDot intent={intent} size={1.5} pulse={status === 'running'} />
<Text size="xs" weight="bold" uppercase variant={intent === 'telemetry' ? 'low' : intent} letterSpacing="widest">
{label}
</Text>
</Stack>
);
}

View File

@@ -1,5 +1,8 @@
import { SidebarItem } from '@/ui/SidebarItem';
'use client';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Box } from '@/ui/Box';
import React from 'react';
interface SidebarRaceItemProps {
@@ -15,18 +18,38 @@ export function SidebarRaceItem({ race, onClick }: SidebarRaceItemProps) {
const scheduledAtDate = new Date(race.scheduledAt);
return (
<SidebarItem
<Box
onClick={onClick}
icon={
<Text size="sm" weight="bold" variant="primary">
cursor="pointer"
p={3}
rounded="md"
bg="transparent"
hoverBg="white/[0.03]"
transition
display="flex"
alignItems="center"
gap={3}
>
<Stack
width="10"
height="10"
align="center"
justify="center"
bg="var(--ui-color-bg-base)"
border
borderColor="var(--ui-color-border-default)"
rounded="md"
>
<Text size="xs" weight="bold" variant="primary" mono>
{scheduledAtDate.getDate()}
</Text>
}
>
<Text size="sm" weight="medium" variant="high" block truncate>{race.track}</Text>
<Text size="xs" variant="low" block>
{scheduledAtDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</Text>
</SidebarItem>
</Stack>
<Stack gap={0.5} flexGrow={1}>
<Text size="sm" weight="bold" variant="high" block truncate>{race.track}</Text>
<Text size="xs" variant="telemetry" mono block>
{scheduledAtDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</Text>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,48 @@
import React, { ReactNode } from 'react';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
interface PageHeaderProps {
title: string;
subtitle?: string;
action?: ReactNode;
}
/**
* Generic PageHeader component following the Teams page style.
* Used to maintain visual consistency across main directory pages.
*/
export function PageHeader({ title, subtitle, action }: PageHeaderProps) {
return (
<Box
marginBottom={12}
display="flex"
flexDirection={{ base: 'col', md: 'row' }}
alignItems={{ base: 'start', md: 'end' }}
justifyContent="between"
gap={6}
borderBottom
borderColor="var(--ui-color-border-muted)"
paddingBottom={8}
>
<Box display="flex" flexDirection="col" gap={2}>
<Box display="flex" alignItems="center" gap={3}>
<Box width="4px" height="32px" bg="var(--ui-color-intent-primary)" />
<Heading level={1} weight="bold" uppercase>{title}</Heading>
</Box>
{subtitle && (
<Text variant="low" size="lg" uppercase weight="bold" letterSpacing="widest">
{subtitle}
</Text>
)}
</Box>
{action && (
<Box display="flex" alignItems="center">
{action}
</Box>
)}
</Box>
);
}

View File

@@ -0,0 +1,118 @@
'use client';
import React from 'react';
import { Container } from '@/ui/Container';
import { Stack } from '@/ui/Stack';
import { RacesLiveRail } from '@/components/races/RacesLiveRail';
import { RacesCommandBar } from '@/components/races/RacesCommandBar';
import { NextUpRacePanel } from '@/components/races/NextUpRacePanel';
import { RacesDayGroup } from '@/components/races/RacesDayGroup';
import { RacesEmptyState } from '@/components/races/RacesEmptyState';
import { RaceFilterModal } from '@/components/races/RaceFilterModal';
import { PageHeader } from '@/components/shared/PageHeader';
import type { RacesViewData } from '@/lib/view-data/RacesViewData';
export interface RacesIndexTemplateProps {
viewData: RacesViewData & {
racesByDate: Array<{
dateKey: string;
dateLabel: string;
races: any[];
}>;
nextUpRace?: any;
};
// Filters
statusFilter: string;
setStatusFilter: (filter: any) => void;
leagueFilter: string;
setLeagueFilter: (filter: string) => void;
timeFilter: string;
setTimeFilter: (filter: any) => void;
// Actions
onRaceClick: (raceId: string) => void;
// UI State
showFilterModal: boolean;
setShowFilterModal: (show: boolean) => void;
}
export function RacesIndexTemplate({
viewData,
statusFilter,
setStatusFilter,
leagueFilter,
setLeagueFilter,
timeFilter,
setTimeFilter,
onRaceClick,
showFilterModal,
setShowFilterModal,
}: RacesIndexTemplateProps) {
const hasRaces = viewData.racesByDate.length > 0;
return (
<Container size="lg">
<Stack gap={8} paddingY={12}>
<PageHeader
title="Races"
subtitle="Live Sessions & Upcoming Events"
/>
{/* 1. Status Rail: Live sessions first */}
<RacesLiveRail
liveRaces={viewData.liveRaces}
onRaceClick={onRaceClick}
/>
{/* 2. Command Bar: Fast filters */}
<RacesCommandBar
timeFilter={timeFilter}
setTimeFilter={setTimeFilter}
leagueFilter={leagueFilter}
setLeagueFilter={setLeagueFilter}
leagues={viewData.leagues}
onShowMoreFilters={() => setShowFilterModal(true)}
/>
{/* 3. Next Up: High signal panel */}
{timeFilter === 'upcoming' && viewData.nextUpRace && (
<NextUpRacePanel
race={viewData.nextUpRace}
onRaceClick={onRaceClick}
/>
)}
{/* 4. Browse by Day: Grouped schedule */}
{hasRaces ? (
<Stack gap={8}>
{viewData.racesByDate.map((group) => (
<RacesDayGroup
key={group.dateKey}
dateLabel={group.dateLabel}
races={group.races}
onRaceClick={onRaceClick}
/>
))}
</Stack>
) : (
<RacesEmptyState />
)}
<RaceFilterModal
isOpen={showFilterModal}
onClose={() => setShowFilterModal(false)}
statusFilter={statusFilter as any}
setStatusFilter={setStatusFilter}
leagueFilter={leagueFilter}
setLeagueFilter={setLeagueFilter}
timeFilter={timeFilter as any}
setTimeFilter={setTimeFilter}
searchQuery=""
setSearchQuery={() => {}}
leagues={viewData.leagues}
showSearch={true}
showTimeFilter={false}
/>
</Stack>
</Container>
);
}

View File

@@ -11,9 +11,7 @@ import type { RacesViewData } from '@/lib/view-data/RacesViewData';
import { Container } from '@/ui/Container';
import { Grid } from '@/ui/Grid';
import { GridItem } from '@/ui/GridItem';
import { Group } from '@/ui/Group';
import { Text } from '@/ui/Text';
import { Panel } from '@/ui/Panel';
import { Stack } from '@/ui/Stack';
import React from 'react';
export type TimeFilter = 'all' | 'upcoming' | 'live' | 'past';
@@ -51,8 +49,8 @@ export function RacesTemplate({
setShowFilterModal,
}: RacesTemplateProps) {
return (
<Container size="lg" py={8}>
<Group direction="column" gap={8} fullWidth>
<Container size="lg">
<Stack gap={8}>
<RacePageHeader
totalCount={viewData.totalCount}
scheduledCount={viewData.scheduledCount}
@@ -67,7 +65,7 @@ export function RacesTemplate({
<Grid cols={12} gap={6}>
<GridItem colSpan={12} lgSpan={8}>
<Group direction="column" gap={6} fullWidth>
<Stack gap={6}>
<RaceFilterBar
timeFilter={timeFilter}
setTimeFilter={setTimeFilter}
@@ -77,24 +75,18 @@ export function RacesTemplate({
onShowMoreFilters={() => setShowFilterModal(true)}
/>
<Panel
title="Race Schedule"
variant="dark"
padding={0}
>
<RaceScheduleTable
races={viewData.races.map(race => ({
id: race.id,
track: race.track,
car: race.car,
leagueName: race.leagueName,
time: race.timeLabel,
status: race.status as SessionStatus
}))}
onRaceClick={onRaceClick}
/>
</Panel>
</Group>
<RaceScheduleTable
races={viewData.races.map(race => ({
id: race.id,
track: race.track,
car: race.car,
leagueName: race.leagueName,
time: race.timeLabel,
status: race.status as SessionStatus
}))}
onRaceClick={onRaceClick}
/>
</Stack>
</GridItem>
<GridItem colSpan={12} lgSpan={4}>
@@ -121,7 +113,7 @@ export function RacesTemplate({
showSearch={false}
showTimeFilter={false}
/>
</Group>
</Stack>
</Container>
);
}

View File

@@ -0,0 +1,72 @@
import { ChevronRight } from 'lucide-react';
import { ReactNode } from 'react';
import { Box } from './Box';
import { Surface } from './Surface';
import { Icon } from './Icon';
export interface RaceRowProps {
children: ReactNode;
onClick: (e: React.MouseEvent) => void;
status?: 'live' | 'upcoming' | 'past';
emphasis?: 'low' | 'medium' | 'high';
as?: any;
href?: string;
}
/**
* RaceRow is a semantic UI component for displaying a race in a list.
* It encapsulates the visual style of a race entry per the GridPilot theme.
*/
export const RaceRow = ({
children,
onClick,
status = 'upcoming',
emphasis = 'medium',
as,
href
}: RaceRowProps) => {
const isLive = status === 'live';
return (
<Surface
as={as}
href={href}
variant="precision"
onClick={onClick}
padding="none"
cursor="pointer"
position="relative"
overflow="hidden"
transition="all 0.2s ease-in-out"
hoverBg="rgba(255, 255, 255, 0.02)"
display="block"
style={{ textDecoration: 'none', color: 'inherit' }}
>
{isLive && (
<Box
position="absolute"
top={0}
left={0}
bottom={0}
width="2px"
bg="var(--ui-color-intent-success)"
/>
)}
<Box
display="flex"
alignItems="center"
gap={4}
paddingX={4}
paddingY={3}
opacity={emphasis === 'low' ? 0.6 : 1}
>
<Box display="flex" alignItems="center" gap={4} flex={1}>
{children}
</Box>
<Box flexShrink={0} display="flex" alignItems="center">
<Icon icon={ChevronRight} size={4} intent="low" />
</Box>
</Box>
</Surface>
);
};

View File

@@ -0,0 +1,52 @@
import { ReactNode } from 'react';
import { Box } from './Box';
import { Text } from './Text';
export interface RaceRowCellProps {
children: ReactNode;
label?: string;
width?: string | number;
align?: 'left' | 'center' | 'right';
hideOnMobile?: boolean;
}
export const RaceRowCell = ({
children,
label,
width,
align = 'left',
hideOnMobile = false
}: RaceRowCellProps) => {
const alignmentClasses = {
left: 'items-start text-left',
center: 'items-center text-center',
right: 'items-end text-right'
}[align];
return (
<Box
display={hideOnMobile ? { base: 'none', md: 'flex' } : 'flex'}
flexDirection="col"
width={width}
flexShrink={width ? 0 : 1}
minWidth="0"
className={alignmentClasses}
>
{label && (
<Text
size="xs"
variant="low"
uppercase
weight="bold"
letterSpacing="widest"
marginBottom={0.5}
>
{label}
</Text>
)}
<Box display="flex" alignItems="center" gap={2} minWidth="0" justifyContent={align === 'right' ? 'end' : (align === 'center' ? 'center' : 'start')}>
{children}
</Box>
</Box>
);
};