website refactor
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
77
apps/website/components/races/NextUpRacePanel.tsx
Normal file
77
apps/website/components/races/NextUpRacePanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
88
apps/website/components/races/RaceListRow.tsx
Normal file
88
apps/website/components/races/RaceListRow.tsx
Normal 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';
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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}
|
||||
|
||||
69
apps/website/components/races/RacesCommandBar.tsx
Normal file
69
apps/website/components/races/RacesCommandBar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
38
apps/website/components/races/RacesDayGroup.tsx
Normal file
38
apps/website/components/races/RacesDayGroup.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
32
apps/website/components/races/RacesEmptyState.tsx
Normal file
32
apps/website/components/races/RacesEmptyState.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
80
apps/website/components/races/RacesLiveRail.tsx
Normal file
80
apps/website/components/races/RacesLiveRail.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
48
apps/website/components/shared/PageHeader.tsx
Normal file
48
apps/website/components/shared/PageHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user