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,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>
);
}