website refactor

This commit is contained in:
2026-01-20 23:50:29 +01:00
parent 7cbec00474
commit 4516427a19
30 changed files with 735 additions and 772 deletions

View File

@@ -1,14 +1,17 @@
import { RankBadge } from '@/components/leaderboards/RankBadge';
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
import { SkillLevelDisplay } from '@/lib/display-objects/SkillLevelDisplay';
import { Box } from '@/ui/Box';
import { Image } from '@/ui/Image';
import { Avatar } from '@/ui/Avatar';
import { LeaderboardList } from '@/ui/LeaderboardList';
import { LeaderboardPreviewShell } from '@/ui/LeaderboardPreviewShell';
import { LeaderboardRow } from '@/ui/LeaderboardRow';
import { Group } from '@/ui/Group';
import { Text } from '@/ui/Text';
import { Trophy } from 'lucide-react';
interface DriverLeaderboardPreviewProps {
title?: string;
subtitle?: string;
drivers: {
id: string;
name: string;
@@ -24,16 +27,22 @@ interface DriverLeaderboardPreviewProps {
onNavigateToDrivers: () => void;
}
export function DriverLeaderboardPreview({ drivers, onDriverClick, onNavigateToDrivers }: DriverLeaderboardPreviewProps) {
export function DriverLeaderboardPreview({
title = "Driver Rankings",
subtitle = "Top Performers",
drivers,
onDriverClick,
onNavigateToDrivers
}: DriverLeaderboardPreviewProps) {
const top10 = drivers; // Already sliced in builder
return (
<LeaderboardPreviewShell
title="Driver Rankings"
subtitle="Top Performers"
title={title}
subtitle={subtitle}
onViewFull={onNavigateToDrivers}
icon={Trophy}
iconColor="var(--primary-blue)"
iconColor="var(--ui-color-intent-primary)"
iconBgGradient="linear-gradient(to bottom right, rgba(25, 140, 255, 0.2), rgba(25, 140, 255, 0.1))"
viewFullLabel="View All"
>
@@ -42,71 +51,52 @@ export function DriverLeaderboardPreview({ drivers, onDriverClick, onNavigateToD
const position = index + 1;
return (
<Box
<LeaderboardRow
key={driver.id}
as="button"
type="button"
onClick={() => onDriverClick(driver.id)}
display="flex"
alignItems="center"
gap={4}
px={5}
py={3}
w="full"
textAlign="left"
transition
hoverBg="bg-white/[0.02]"
group
>
<Box w="8" display="flex" justifyContent="center">
<RankBadge rank={position} />
</Box>
<Box
position="relative"
w="9"
h="9"
rounded="full"
overflow="hidden"
border
borderColor="border-charcoal-outline"
groupHoverBorderColor="primary-blue/50"
transition
>
<Image src={driver.avatarUrl} alt={driver.name} fullWidth fullHeight objectFit="cover" />
</Box>
<Box flexGrow={1} minWidth="0">
<Text
weight="semibold"
color="text-white"
truncate
groupHoverTextColor="text-primary-blue"
transition
block
>
{driver.name}
</Text>
<Box display="flex" alignItems="center" gap={2}>
<Text size="xs" color="text-gray-500">{driver.nationality}</Text>
<Box w="1" h="1" rounded="full" bg="bg-gray-700" />
<Box as="span" color={SkillLevelDisplay.getColor(driver.skillLevel)}>
<Text size="xs" weight="medium">{SkillLevelDisplay.getLabel(driver.skillLevel)}</Text>
</Box>
</Box>
</Box>
<Box display="flex" alignItems="center" gap={6}>
<Box textAlign="right">
<Text color="text-primary-blue" font="mono" weight="bold" block size="sm">{RatingDisplay.format(driver.rating)}</Text>
<Text fontSize="10px" color="text-gray-500" block uppercase letterSpacing="wider" weight="bold">Rating</Text>
</Box>
<Box textAlign="right" minWidth="12">
<Text color="text-performance-green" font="mono" weight="bold" block size="sm">{driver.wins}</Text>
<Text fontSize="10px" color="text-gray-500" block uppercase letterSpacing="wider" weight="bold">Wins</Text>
</Box>
</Box>
</Box>
rank={<RankBadge rank={position} />}
identity={
<Group gap={4}>
<Avatar src={driver.avatarUrl} alt={driver.name} size="sm" />
<Group direction="column" align="start" gap={0}>
<Text
weight="bold"
variant="high"
truncate
block
>
{driver.name}
</Text>
<Group gap={2}>
<Text size="xs" variant="low" uppercase weight="bold" letterSpacing="wider">{driver.nationality}</Text>
<Text size="xs" weight="bold" color={SkillLevelDisplay.getColor(driver.skillLevel)} uppercase letterSpacing="wider">
{SkillLevelDisplay.getLabel(driver.skillLevel)}
</Text>
</Group>
</Group>
</Group>
}
stats={
<Group gap={8}>
<Group direction="column" align="end" gap={0}>
<Text variant="primary" font="mono" weight="bold" block size="md" align="right">
{RatingDisplay.format(driver.rating)}
</Text>
<Text size="xs" variant="low" block uppercase letterSpacing="widest" weight="bold" fontSize="9px" align="right">
Rating
</Text>
</Group>
<Group direction="column" align="end" gap={0}>
<Text variant="success" font="mono" weight="bold" block size="md" align="right">
{driver.wins}
</Text>
<Text size="xs" variant="low" block uppercase letterSpacing="widest" weight="bold" fontSize="9px" align="right">
Wins
</Text>
</Group>
</Group>
}
/>
);
})}
</LeaderboardList>

View File

@@ -2,7 +2,6 @@ import { Icon } from '@/ui/Icon';
import { Input } from '@/ui/Input';
import { ControlBar } from '@/ui/ControlBar';
import { Button } from '@/ui/Button';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Filter, Search } from 'lucide-react';
import React from 'react';
@@ -21,32 +20,30 @@ export function LeaderboardFiltersBar({
children,
}: LeaderboardFiltersBarProps) {
return (
<Box marginBottom={6}>
<ControlBar
leftContent={
<Box maxWidth="32rem" fullWidth>
<Input
type="text"
value={searchQuery}
onChange={(e) => onSearchChange?.(e.target.value)}
placeholder={placeholder}
icon={<Icon icon={Search} size={4} intent="low" />}
fullWidth
/>
</Box>
}
>
<Group gap={4}>
{children}
<Button
variant="secondary"
size="sm"
icon={<Icon icon={Filter} size={3.5} intent="low" />}
>
Filters
</Button>
<ControlBar
leftContent={
<Group fullWidth>
<Input
type="text"
value={searchQuery}
onChange={(e) => onSearchChange?.(e.target.value)}
placeholder={placeholder}
icon={<Icon icon={Search} size={4} intent="low" />}
fullWidth
/>
</Group>
</ControlBar>
</Box>
}
>
<Group gap={4}>
{children}
<Button
variant="secondary"
size="sm"
icon={<Icon icon={Filter} size={3.5} intent="low" />}
>
Filters
</Button>
</Group>
</ControlBar>
);
}

View File

@@ -1,7 +1,7 @@
import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack';
import { Group } from '@/ui/Group';
import { Text } from '@/ui/Text';
import { ArrowLeft, LucideIcon } from 'lucide-react';
import React from 'react';
@@ -24,9 +24,9 @@ export function LeaderboardHeader({
children,
}: LeaderboardHeaderProps) {
return (
<Stack mb={8}>
<Group direction="column" align="stretch" gap={8}>
{onBack && (
<Stack mb={6}>
<Group>
<Button
variant="secondary"
onClick={onBack}
@@ -34,38 +34,32 @@ export function LeaderboardHeader({
>
{backLabel}
</Button>
</Stack>
</Group>
)}
<Stack direction="row" align="center" justify="between" gap={4}>
<Stack direction="row" align="center" gap={4}>
<Group justify="between" gap={4}>
<Group gap={4}>
{icon && (
<Stack
p={3}
bg="linear-gradient(to bottom right, rgba(25, 140, 255, 0.15), rgba(25, 140, 255, 0.05))"
border
borderColor="border-primary-blue/20"
rounded="xl"
display="flex"
alignItems="center"
justifyContent="center"
<Group
gap={0}
justify="center"
>
<Icon icon={icon} size={6} color="text-primary-blue" />
</Stack>
<Icon icon={icon} size={6} intent="primary" />
</Group>
)}
<Stack>
<Heading level={1} weight="bold" letterSpacing="tight">{title}</Heading>
<Group direction="column" align="start" gap={1}>
<Heading level={1} weight="bold">{title}</Heading>
{description && (
<Text color="text-gray-400" block mt={1} size="sm">
<Text variant="low" size="sm">
{description}
</Text>
)}
</Stack>
</Stack>
<Stack>
</Group>
</Group>
<Group>
{children}
</Stack>
</Stack>
</Stack>
</Group>
</Group>
</Group>
);
}

View File

@@ -1,8 +1,7 @@
import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { Group } from '@/ui/Group';
import { Text } from '@/ui/Text';
import { ArrowLeft, LucideIcon } from 'lucide-react';
import React from 'react';
@@ -25,9 +24,9 @@ export function LeaderboardHeaderPanel({
children,
}: LeaderboardHeaderPanelProps) {
return (
<Stack mb={8}>
<Group direction="column" align="stretch" gap={8}>
{onBack && (
<Stack mb={6}>
<Group>
<Button
variant="secondary"
onClick={onBack}
@@ -35,34 +34,29 @@ export function LeaderboardHeaderPanel({
>
{backLabel}
</Button>
</Stack>
</Group>
)}
<Stack direction="row" align="center" justify="between" gap={4}>
<Stack direction="row" align="center" gap={4}>
<Group justify="between" gap={4}>
<Group gap={4}>
{icon && (
<Surface
variant="muted"
rounded="xl"
padding={3}
bg="linear-gradient(to bottom right, rgba(25, 140, 255, 0.2), rgba(25, 140, 255, 0.05))"
border
borderColor="border-primary-blue/20"
<Group
justify="center"
>
<Icon icon={icon} size={7} color="text-primary-blue" />
</Surface>
<Icon icon={icon} size={7} intent="primary" />
</Group>
)}
<Stack>
<Group direction="column" align="start" gap={1}>
<Heading level={1}>{title}</Heading>
{description && (
<Text color="text-gray-400" block mt={1}>
<Text variant="low" block>
{description}
</Text>
)}
</Stack>
</Stack>
</Group>
</Group>
{children}
</Stack>
</Stack>
</Group>
</Group>
);
}

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Table, TableBody, TableHead, TableHeader, TableRow } from '@/ui/Table';
import { RankingRow } from './RankingRow';
import { LeaderboardList } from '@/ui/LeaderboardList';
import { LeaderboardTableShell } from '@/ui/LeaderboardTableShell';
interface LeaderboardDriver {
@@ -22,23 +22,17 @@ interface LeaderboardTableProps {
}
export function LeaderboardTable({ drivers, onDriverClick }: LeaderboardTableProps) {
const columns = [
{ key: 'rank', label: 'Rank', width: '8rem' },
{ key: 'driver', label: 'Driver' },
{ key: 'races', label: 'Races', align: 'center' as const },
{ key: 'rating', label: 'Rating', align: 'center' as const },
{ key: 'wins', label: 'Wins', align: 'center' as const },
];
return (
<LeaderboardTableShell columns={columns}>
{drivers.map((driver) => (
<RankingRow
key={driver.id}
{...driver}
onClick={() => onDriverClick?.(driver.id)}
/>
))}
<LeaderboardTableShell>
<LeaderboardList>
{drivers.map((driver) => (
<RankingRow
key={driver.id}
{...driver}
onClick={() => onDriverClick?.(driver.id)}
/>
))}
</LeaderboardList>
</LeaderboardTableShell>
);
}

View File

@@ -1,38 +1,10 @@
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Crown } from 'lucide-react';
import { RankMedal } from '@/ui/RankMedal';
import React from 'react';
interface MedalBadgeProps {
position: number;
}
export function MedalBadge({ position }: MedalBadgeProps) {
const getMedalColor = (pos: number) => {
switch (pos) {
case 1: return 'var(--warning-amber)';
case 2: return 'var(--iron-gray)';
case 3: return 'var(--amber-600)';
default: return 'var(--charcoal-outline)';
}
};
const isMedal = position <= 3;
return (
<Stack
h="10"
w="10"
align="center"
justify="center"
rounded="full"
bg={isMedal ? 'bg-gradient-to-br from-yellow-400/20 to-amber-600/10' : 'bg-iron-gray'}
>
{isMedal ? (
<Icon icon={Crown} size={5} color={getMedalColor(position)} />
) : (
<Text size="lg" weight="bold" color="text-gray-400">#{position}</Text>
)}
</Stack>
);
return <RankMedal rank={position} size="md" />;
}

View File

@@ -1,4 +1,4 @@
import { Badge } from '@/ui/Badge';
import { RankMedal } from '@/ui/RankMedal';
import { Text } from '@/ui/Text';
import { Group } from '@/ui/Group';
import React from 'react';
@@ -9,32 +9,15 @@ interface RankBadgeProps {
}
export function RankBadge({ rank, size = 'md' }: RankBadgeProps) {
const badgeSize = size === 'lg' ? 'md' : size;
const getVariant = (rank: number): 'warning' | 'primary' | 'info' | 'default' => {
if (rank <= 3) return 'warning';
if (rank <= 10) return 'primary';
if (rank <= 50) return 'info';
return 'default';
};
const getMedalEmoji = (rank: number) => {
switch (rank) {
case 1: return '🥇';
case 2: return '🥈';
case 3: return '🥉';
default: return null;
}
};
const medal = getMedalEmoji(rank);
if (rank <= 3) {
return <RankMedal rank={rank} size={size} />;
}
return (
<Badge variant={getVariant(rank)} size={badgeSize}>
<Group gap={1}>
{medal && <Text size="xs">{medal}</Text>}
<Text size="xs" weight="bold">#{rank}</Text>
</Group>
</Badge>
<Group justify="center" align="center">
<Text size={size === 'lg' ? 'md' : 'xs'} weight="bold" variant="low">
#{rank}
</Text>
</Group>
);
}

View File

@@ -1,54 +1,18 @@
import { MedalDisplay } from '@/lib/display-objects/MedalDisplay';
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { Surface } from '@/ui/Surface';
import { Crown, Medal } from 'lucide-react';
import { RankMedal as UiRankMedal, RankMedalProps } from '@/ui/RankMedal';
import React from 'react';
interface RankMedalProps {
rank: number;
size?: 'sm' | 'md' | 'lg';
showIcon?: boolean;
}
export function RankMedal({ rank, size = 'md', showIcon = true }: RankMedalProps) {
const isTop3 = rank <= 3;
const variant = MedalDisplay.getVariant(rank);
const sizePx = {
sm: '1.75rem',
md: '2rem',
lg: '2.5rem',
};
const textSizeMap = {
sm: 'xs',
md: 'xs',
lg: 'sm',
} as const;
const iconSize = {
sm: 3,
md: 3.5,
lg: 4.5,
};
export function RankMedal(props: RankMedalProps) {
const variant = MedalDisplay.getVariant(props.rank);
const bg = MedalDisplay.getBg(props.rank);
const color = MedalDisplay.getColor(props.rank);
return (
<Surface
variant="muted"
rounded="full"
border
height={sizePx[size]}
width={sizePx[size]}
display="flex"
alignItems="center"
justifyContent="center"
>
{isTop3 && showIcon ? (
<Icon icon={rank === 1 ? Crown : Medal} size={iconSize[size]} intent={variant as any} />
) : (
<Text weight="bold" size={textSizeMap[size]} variant={variant as any}>{rank}</Text>
)}
</Surface>
<UiRankMedal
{...props}
variant={variant as any}
bg={bg}
color={color}
/>
);
}

View File

@@ -1,10 +1,12 @@
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
import { Image } from '@/ui/Image';
import { Stack } from '@/ui/Stack';
import { TableCell, TableRow } from '@/ui/Table';
import { Avatar } from '@/ui/Avatar';
import { Group } from '@/ui/Group';
import { Text } from '@/ui/Text';
import { DeltaChip } from './DeltaChip';
import { RankMedal } from './RankMedal';
import { RankBadge } from './RankBadge';
import { LeaderboardRow } from '@/ui/LeaderboardRow';
import { SkillLevelDisplay } from '@/lib/display-objects/SkillLevelDisplay';
import React from 'react';
interface RankingRowProps {
id: string;
@@ -33,82 +35,69 @@ export function RankingRow({
onClick,
}: RankingRowProps) {
return (
<TableRow
clickable={!!onClick}
<LeaderboardRow
onClick={onClick}
group
>
<TableCell>
<Stack direction="row" align="center" gap={4}>
<Stack w="8" display="flex" justifyContent="center">
<RankMedal rank={rank} size="md" />
</Stack>
rank={
<Group gap={4}>
<RankBadge rank={rank} />
{rankDelta !== undefined && (
<Stack w="10">
<DeltaChip value={rankDelta} type="rank" />
</Stack>
<DeltaChip value={rankDelta} type="rank" />
)}
</Stack>
</TableCell>
<TableCell>
<Stack display="flex" alignItems="center" gap={3}>
<Stack
position="relative"
w="10"
h="10"
rounded="full"
overflow="hidden"
border
borderColor="border-charcoal-outline"
groupHoverBorderColor="primary-blue/50"
transition
>
<Image
src={avatarUrl}
alt={name}
width={40}
height={40}
fullWidth
fullHeight
objectFit="cover"
/>
</Stack>
<Stack minWidth="0">
</Group>
}
identity={
<Group gap={4}>
<Avatar
src={avatarUrl}
alt={name}
size="md"
/>
<Group direction="column" align="start" gap={0}>
<Text
weight="semibold"
color="text-white"
weight="bold"
variant="high"
block
truncate
groupHoverTextColor="text-primary-blue"
transition
>
{name}
</Text>
<Stack direction="row" align="center" gap={2} mt={0.5}>
<Text size="xs" color="text-gray-500">{nationality}</Text>
<Stack w="1" h="1" rounded="full" bg="bg-gray-700" />
<Text size="xs" color="text-gray-500">{skillLevel}</Text>
</Stack>
</Stack>
</Stack>
</TableCell>
<TableCell textAlign="center">
<Text color="text-gray-400" font="mono">{racesCompleted}</Text>
</TableCell>
<TableCell textAlign="center">
<Text font="mono" weight="bold" color="text-primary-blue">
{RatingDisplay.format(rating)}
</Text>
</TableCell>
<TableCell textAlign="center">
<Text font="mono" weight="bold" color="text-performance-green">
{wins}
</Text>
</TableCell>
</TableRow>
<Group gap={2}>
<Text size="xs" variant="low" uppercase weight="bold" letterSpacing="wider">{nationality}</Text>
<Text size="xs" weight="bold" color={SkillLevelDisplay.getColor(skillLevel)} uppercase letterSpacing="wider">
{SkillLevelDisplay.getLabel(skillLevel)}
</Text>
</Group>
</Group>
</Group>
}
stats={
<Group gap={8}>
<Group direction="column" align="end" gap={0}>
<Text variant="low" font="mono" weight="bold" block size="md">
{racesCompleted}
</Text>
<Text size="xs" variant="low" block uppercase letterSpacing="widest" weight="bold" fontSize="9px">
Races
</Text>
</Group>
<Group direction="column" align="end" gap={0}>
<Text variant="primary" font="mono" weight="bold" block size="md">
{RatingDisplay.format(rating)}
</Text>
<Text size="xs" variant="low" block uppercase letterSpacing="widest" weight="bold" fontSize="9px">
Rating
</Text>
</Group>
<Group direction="column" align="end" gap={0}>
<Text variant="success" font="mono" weight="bold" block size="md">
{wins}
</Text>
<Text size="xs" variant="low" block uppercase letterSpacing="widest" weight="bold" fontSize="9px">
Wins
</Text>
</Group>
</Group>
}
/>
);
}

View File

@@ -1,8 +1,10 @@
import { Image } from '@/ui/Image';
import { Stack } from '@/ui/Stack';
import { Avatar } from '@/ui/Avatar';
import { Group } from '@/ui/Group';
import { Text } from '@/ui/Text';
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
import { MedalDisplay } from '@/lib/display-objects/MedalDisplay';
import { Surface } from '@/ui/Surface';
import React from 'react';
interface PodiumDriver {
id: string;
@@ -20,121 +22,67 @@ interface RankingsPodiumProps {
export function RankingsPodium({ podium, onDriverClick }: RankingsPodiumProps) {
return (
<Stack mb={10}>
<Stack display="flex" alignItems="end" justifyContent="center" gap={4}>
{[1, 0, 2].map((index) => {
const driver = podium[index];
if (!driver) return null;
const position = index === 1 ? 1 : index === 0 ? 2 : 3;
const config = {
1: { height: '10rem', color: 'rgba(250, 204, 21, 0.2)', borderColor: 'rgba(250, 204, 21, 0.4)', crown: '#facc15' },
2: { height: '8rem', color: 'rgba(209, 213, 219, 0.2)', borderColor: 'rgba(209, 213, 219, 0.4)', crown: '#d1d5db' },
3: { height: '6rem', color: 'rgba(217, 119, 6, 0.2)', borderColor: 'rgba(217, 119, 6, 0.4)', crown: '#d97706' },
}[position] || { height: '6rem', color: 'rgba(217, 119, 6, 0.2)', borderColor: 'rgba(217, 119, 6, 0.4)', crown: '#d97706' };
<Group justify="center" align="end" gap={4}>
{[1, 0, 2].map((index) => {
const driver = podium[index];
if (!driver) return null;
const position = index === 1 ? 1 : index === 0 ? 2 : 3;
const isFirst = position === 1;
const config = {
1: { height: '10rem', variant: 'precision' },
2: { height: '8rem', variant: 'muted' },
3: { height: '6rem', variant: 'muted' },
}[position as 1 | 2 | 3];
return (
<Stack
key={driver.id}
as="button"
type="button"
onClick={() => onDriverClick?.(driver.id)}
display="flex"
flexDirection="col"
alignItems="center"
bg="transparent"
border={false}
cursor="pointer"
>
<Stack position="relative" mb={4}>
<Stack
position="relative"
w={position === 1 ? '24' : '20'}
h={position === 1 ? '24' : '20'}
rounded="full"
overflow="hidden"
border
borderColor={config.crown === '#facc15' ? 'border-warning-amber' : config.crown === '#d1d5db' ? 'border-gray-300' : 'border-orange-600'}
style={{ borderWidth: '4px', boxShadow: position === 1 ? '0 0 30px rgba(250, 204, 21, 0.3)' : 'none' }}
>
<Image
src={driver.avatarUrl}
alt={driver.name}
width={112}
height={112}
objectFit="cover"
/>
</Stack>
<Stack
position="absolute"
bottom="-2"
left="50%"
w="8"
h="8"
rounded="full"
display="flex"
alignItems="center"
justifyContent="center"
border
weight="bold"
size="sm"
borderColor={config.crown === '#facc15' ? 'border-warning-amber' : config.crown === '#d1d5db' ? 'border-gray-300' : 'border-orange-600'}
color={config.crown === '#facc15' ? 'text-warning-amber' : config.crown === '#d1d5db' ? 'text-gray-300' : 'text-orange-600'}
style={{
transform: 'translateX(-50%)',
borderWidth: '2px',
background: `linear-gradient(to bottom right, ${config.color}, transparent)`
}}
>
{position}
</Stack>
</Stack>
<Text weight="semibold" color="text-white" size={position === 1 ? 'lg' : 'base'} mb={1} block>
{driver.name}
</Text>
<Text font="mono" weight="bold" size={position === 1 ? 'xl' : 'lg'} block color={position === 1 ? 'text-warning-amber' : 'text-primary-blue'}>
{driver.rating.toString()}
</Text>
<Stack direction="row" align="center" gap={2} mt={1}>
<Stack direction="row" align="center" gap={1}>
<Text>🏆</Text>
<Text color="text-gray-400">{driver.wins}</Text>
</Stack>
<Text color="text-gray-500"></Text>
<Stack direction="row" align="center" gap={1}>
<Text>🏅</Text>
<Text color="text-gray-400">{driver.podiums}</Text>
</Stack>
</Stack>
<Stack
mt={4}
w={position === 1 ? '28' : '24'}
h={config.height}
rounded="lg"
display="flex"
alignItems="end"
justifyContent="center"
pb={4}
style={{
borderRadius: '0.5rem 0.5rem 0 0',
background: `linear-gradient(to top, ${config.color}, transparent)`,
borderTop: `1px solid ${config.borderColor}`,
borderLeft: `1px solid ${config.borderColor}`,
borderRight: `1px solid ${config.borderColor}`
}}
return (
<Group
key={driver.id}
direction="column"
align="center"
gap={4}
>
<Group direction="column" align="center" gap={2}>
<Group
justify="center"
align="center"
>
<Text weight="bold" size={position === 1 ? '4xl' : '3xl'} color={config.crown === '#facc15' ? 'text-warning-amber' : config.crown === '#d1d5db' ? 'text-gray-300' : 'text-orange-600'}>
{position}
</Text>
</Stack>
</Stack>
);
})}
</Stack>
</Stack>
<Avatar
src={driver.avatarUrl}
alt={driver.name}
size={isFirst ? 'lg' : 'md'}
/>
</Group>
<Text weight="bold" variant="high" size={isFirst ? 'md' : 'sm'}>{driver.name}</Text>
<Text font="mono" weight="bold" variant={isFirst ? 'warning' : 'primary'}>
{RatingDisplay.format(driver.rating)}
</Text>
</Group>
<Surface
variant={config.variant as any}
rounded="lg"
style={{
width: '6rem',
height: config.height,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px solid var(--ui-color-border-default)',
borderBottom: 'none',
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0
}}
>
<Text size="3xl" weight="bold" variant="low" opacity={0.2}>
{position}
</Text>
</Surface>
</Group>
);
})}
</Group>
);
}

View File

@@ -1,11 +1,9 @@
import { Icon } from '@/ui/Icon';
import { Image } from '@/ui/Image';
import { Stack } from '@/ui/Stack';
import { Group } from '@/ui/Group';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/ui/Table';
import { Text } from '@/ui/Text';
import { Medal } from 'lucide-react';
import { RankingRow } from './RankingRow';
import { EmptyState } from '@/ui/EmptyState';
import { Trophy } from 'lucide-react';
interface Driver {
id: string;
@@ -29,88 +27,34 @@ interface RankingsTableProps {
export function RankingsTable({ drivers, onDriverClick }: RankingsTableProps) {
if (drivers.length === 0) {
return (
<Stack py={16} align="center" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline" rounded="xl">
<Text color="text-gray-400" block mb={2}>No drivers found</Text>
<Text size="sm" color="text-gray-500">There are no drivers in the system yet</Text>
</Stack>
<EmptyState
title="No drivers found"
description="There are no drivers in the system yet"
icon={Trophy}
/>
);
}
return (
<Stack rounded="xl" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline" overflow="hidden" gap={0}>
<Table>
<TableHead>
<TableRow>
<TableHeader className="text-center w-16">Rank</TableHeader>
<TableHeader>Driver</TableHeader>
<TableHeader className="text-center">Races</TableHeader>
<TableHeader className="text-center">Rating</TableHeader>
<TableHeader className="text-center">Wins</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{drivers.map((driver) => (
<TableRow
key={driver.id}
clickable
onClick={() => onDriverClick?.(driver.id)}
>
<TableCell className="text-center">
<Stack
direction="row"
h="9"
w="9"
align="center"
justify="center"
rounded="full"
border
borderColor="border-charcoal-outline"
bg={driver.medalBg}
fontSize="sm"
fontWeight="bold"
>
{driver.rank <= 3 ? <Icon icon={Medal} size={4} color={driver.medalColor} /> : (
<Text color={driver.medalColor}>{driver.rank}</Text>
)}
</Stack>
</TableCell>
<TableCell>
<Stack direction="row" align="center" gap={3}>
<Stack position="relative" w="10" h="10" rounded="full" overflow="hidden" border borderColor="border-charcoal-outline" gap={0}>
<Image src={driver.avatarUrl} alt={driver.name} width={40} height={40} objectFit="cover" fullWidth fullHeight />
</Stack>
<Stack minWidth="0" gap={0}>
<Text weight="semibold" color="text-white" block truncate>
{driver.name}
</Text>
<Stack direction="row" align="center" gap={2} mt={1}>
<Text size="xs" color="text-gray-500">{driver.nationality}</Text>
<Text size="xs" color="text-gray-500">{driver.skillLevel}</Text>
</Stack>
</Stack>
</Stack>
</TableCell>
<TableCell className="text-center">
<Text color="text-gray-400">{driver.racesCompleted}</Text>
</TableCell>
<TableCell className="text-center">
<Text font="mono" weight="semibold" color="text-white">
{driver.rating.toString()}
</Text>
</TableCell>
<TableCell className="text-center">
<Text font="mono" weight="semibold" color="text-performance-green">
{driver.wins}
</Text>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Stack>
<Table>
<TableHead>
<TableRow>
<TableHeader className="text-center w-16">Rank</TableHeader>
<TableHeader>Driver</TableHeader>
<TableHeader className="text-center">Races</TableHeader>
<TableHeader className="text-center">Rating</TableHeader>
<TableHeader className="text-center">Wins</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{drivers.map((driver) => (
<RankingRow
key={driver.id}
{...driver}
onClick={() => onDriverClick?.(driver.id)}
/>
))}
</TableBody>
</Table>
);
}

View File

@@ -1,4 +1,4 @@
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Icon } from '@/ui/Icon';
import { Select } from '@/ui/Select';
import { Text } from '@/ui/Text';
@@ -23,19 +23,18 @@ export function SeasonSelector({ seasons, selectedSeasonId, onSeasonChange }: Se
}));
return (
<Box display="flex" alignItems="center" gap={3}>
<Box display="flex" alignItems="center" gap={2} color="text-gray-500">
<Icon icon={Calendar} size={4} />
<Text size="xs" weight="bold" uppercase letterSpacing="wider">Season</Text>
</Box>
<Box width="48">
<Group gap={3}>
<Group gap={2}>
<Icon icon={Calendar} size={4} intent="low" />
<Text size="xs" weight="bold" uppercase letterSpacing="wider" variant="low">Season</Text>
</Group>
<Group>
<Select
options={options}
value={selectedSeasonId}
onChange={(e) => onSeasonChange(e.target.value)}
fullWidth={true}
/>
</Box>
</Box>
</Group>
</Group>
);
}

View File

@@ -1,10 +1,11 @@
import { RankBadge } from '@/components/leaderboards/RankBadge';
import { getMediaUrl } from '@/lib/utilities/media';
import { Box } from '@/ui/Box';
import { Avatar } from '@/ui/Avatar';
import { Icon } from '@/ui/Icon';
import { Image } from '@/ui/Image';
import { LeaderboardList } from '@/ui/LeaderboardList';
import { LeaderboardPreviewShell } from '@/ui/LeaderboardPreviewShell';
import { LeaderboardRow } from '@/ui/LeaderboardRow';
import { Group } from '@/ui/Group';
import { Text } from '@/ui/Text';
import { Users } from 'lucide-react';
@@ -43,83 +44,57 @@ export function TeamLeaderboardPreview({ teams, onTeamClick, onNavigateToTeams }
const position = team.position;
return (
<Box
<LeaderboardRow
key={team.id}
as="button"
type="button"
onClick={() => onTeamClick(team.id)}
display="flex"
alignItems="center"
gap={4}
px={5}
py={3}
w="full"
textAlign="left"
transition
hoverBg="bg-white/[0.02]"
group
>
<Box w="8" display="flex" justifyContent="center">
<RankBadge rank={position} />
</Box>
<Box
display="flex"
h="9"
w="9"
alignItems="center"
justifyContent="center"
rounded="lg"
bg="bg-graphite-black/50"
border
borderColor="border-charcoal-outline"
overflow="hidden"
groupHoverBorderColor="primary-blue/50"
transition
>
<Image
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
alt={team.name}
width={36}
height={36}
fullWidth
fullHeight
objectFit="cover"
/>
</Box>
<Box flexGrow={1} minWidth="0">
<Text
weight="semibold"
color="text-white"
truncate
groupHoverTextColor="text-primary-blue"
transition
block
>
{team.name}
</Text>
<Box display="flex" alignItems="center" gap={2} flexWrap="wrap">
<Text size="xs" variant="low" uppercase font="mono">{team.performanceLevel}</Text>
<Box w="1" h="1" rounded="full" bg="bg-gray-700" />
<Box display="flex" alignItems="center" gap={1}>
<Icon icon={Users} size={3} color="text-gray-500" />
<Text size="xs" color="text-gray-500">{team.memberCount}</Text>
</Box>
</Box>
</Box>
<Box display="flex" alignItems="center" gap={6}>
<Box textAlign="right">
<Text color="text-primary-blue" font="mono" weight="bold" block size="sm">{team.rating?.toFixed(0) || '1000'}</Text>
<Text fontSize="10px" color="text-gray-500" block uppercase letterSpacing="wider" weight="bold">Rating</Text>
</Box>
<Box textAlign="right" minWidth="12">
<Text color="text-performance-green" font="mono" weight="bold" block size="sm">{team.totalWins}</Text>
<Text fontSize="10px" color="text-gray-500" block uppercase letterSpacing="wider" weight="bold">Wins</Text>
</Box>
</Box>
</Box>
rank={<RankBadge rank={position} />}
identity={
<Group gap={4}>
<Avatar
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
alt={team.name}
size="sm"
/>
<Group direction="column" align="start" gap={0}>
<Text
weight="bold"
variant="high"
truncate
block
>
{team.name}
</Text>
<Group gap={2} wrap>
<Text size="xs" variant="low" uppercase weight="bold" letterSpacing="wider" font="mono">{team.performanceLevel}</Text>
<Group gap={1}>
<Icon icon={Users} size={3} intent="low" />
<Text size="xs" variant="low" weight="bold">{team.memberCount}</Text>
</Group>
</Group>
</Group>
</Group>
}
stats={
<Group gap={8}>
<Group direction="column" align="end" gap={0}>
<Text variant="primary" font="mono" weight="bold" block size="md" align="right">
{team.rating?.toFixed(0) || '1000'}
</Text>
<Text size="xs" variant="low" block uppercase letterSpacing="widest" weight="bold" fontSize="9px" align="right">
Rating
</Text>
</Group>
<Group direction="column" align="end" gap={0}>
<Text variant="success" font="mono" weight="bold" block size="md" align="right">
{team.totalWins}
</Text>
<Text size="xs" variant="low" block uppercase letterSpacing="widest" weight="bold" fontSize="9px" align="right">
Wins
</Text>
</Group>
</Group>
}
/>
);
})}
</LeaderboardList>

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { TeamRankingRow } from './TeamRankingRow';
import { LeaderboardList } from '@/ui/LeaderboardList';
import { LeaderboardTableShell } from '@/ui/LeaderboardTableShell';
interface LeaderboardTeam {
@@ -19,30 +20,24 @@ interface TeamLeaderboardTableProps {
}
export function TeamLeaderboardTable({ teams, onTeamClick }: TeamLeaderboardTableProps) {
const columns = [
{ key: 'rank', label: 'Rank', width: '8rem' },
{ key: 'team', label: 'Team' },
{ key: 'rating', label: 'Rating', align: 'center' as const },
{ key: 'wins', label: 'Wins', align: 'center' as const },
{ key: 'races', label: 'Races', align: 'center' as const },
];
return (
<LeaderboardTableShell columns={columns}>
{teams.map((team) => (
<TeamRankingRow
key={team.id}
rank={team.position}
id={team.id}
name={team.name}
logoUrl={team.logoUrl}
rating={team.rating}
wins={team.totalWins}
races={team.totalRaces}
memberCount={team.memberCount}
onClick={() => onTeamClick?.(team.id)}
/>
))}
<LeaderboardTableShell>
<LeaderboardList>
{teams.map((team) => (
<TeamRankingRow
key={team.id}
rank={team.position}
id={team.id}
name={team.name}
logoUrl={team.logoUrl}
rating={team.rating}
wins={team.totalWins}
races={team.totalRaces}
memberCount={team.memberCount}
onClick={() => onTeamClick?.(team.id)}
/>
))}
</LeaderboardList>
</LeaderboardTableShell>
);
}

View File

@@ -1,12 +1,9 @@
import { getMediaUrl } from '@/lib/utilities/media';
import { Image } from '@/ui/Image';
import { TableCell, TableRow } from '@/ui/Table';
import { Avatar } from '@/ui/Avatar';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { RankMedal } from './RankMedal';
import { RankBadge } from './RankBadge';
import { LeaderboardRow } from '@/ui/LeaderboardRow';
import React from 'react';
interface TeamRankingRowProps {
@@ -33,66 +30,59 @@ export function TeamRankingRow({
onClick,
}: TeamRankingRowProps) {
return (
<TableRow
clickable={!!onClick}
<LeaderboardRow
onClick={onClick}
>
<TableCell>
<Box width="2rem" display="flex" justifyContent="center">
<RankMedal rank={rank} size="md" />
</Box>
</TableCell>
<TableCell>
<Group gap={3}>
<Surface
position="relative"
width="2.5rem"
height="2.5rem"
rounded="md"
overflow="hidden"
border
variant="muted"
>
<Image
src={logoUrl || getMediaUrl('team-logo', id)}
alt={name}
width={40}
height={40}
objectFit="cover"
/>
</Surface>
<Stack gap={0} flex={1} minWidth="0">
rank={<RankBadge rank={rank} />}
identity={
<Group gap={4}>
<Avatar
src={logoUrl || getMediaUrl('team-logo', id)}
alt={name}
size="md"
/>
<Group direction="column" align="start" gap={0}>
<Text
weight="semibold"
weight="bold"
variant="high"
block
truncate
>
{name}
</Text>
<Text size="xs" variant="low" block>
<Text size="xs" variant="low" uppercase weight="bold" letterSpacing="wider">
{memberCount} Members
</Text>
</Stack>
</Group>
</Group>
</TableCell>
<TableCell textAlign="center">
<Text font="mono" weight="bold" variant="primary">
{rating}
</Text>
</TableCell>
<TableCell textAlign="center">
<Text font="mono" weight="bold" variant="success">
{wins}
</Text>
</TableCell>
<TableCell textAlign="center">
<Text variant="low" font="mono">{races}</Text>
</TableCell>
</TableRow>
}
stats={
<Group gap={8}>
<Group direction="column" align="end" gap={0}>
<Text variant="low" font="mono" weight="bold" block size="md">
{races}
</Text>
<Text size="xs" variant="low" block uppercase letterSpacing="widest" weight="bold" fontSize="9px">
Races
</Text>
</Group>
<Group direction="column" align="end" gap={0}>
<Text variant="primary" font="mono" weight="bold" block size="md">
{rating}
</Text>
<Text size="xs" variant="low" block uppercase letterSpacing="widest" weight="bold" fontSize="9px">
Rating
</Text>
</Group>
<Group direction="column" align="end" gap={0}>
<Text variant="success" font="mono" weight="bold" block size="md">
{wins}
</Text>
<Text size="xs" variant="low" block uppercase letterSpacing="widest" weight="bold" fontSize="9px">
Wins
</Text>
</Group>
</Group>
}
/>
);
}