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">
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="semibold"
color="text-white"
weight="bold"
variant="high"
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>
<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,10 +20,9 @@ export function LeaderboardFiltersBar({
children,
}: LeaderboardFiltersBarProps) {
return (
<Box marginBottom={6}>
<ControlBar
leftContent={
<Box maxWidth="32rem" fullWidth>
<Group fullWidth>
<Input
type="text"
value={searchQuery}
@@ -33,7 +31,7 @@ export function LeaderboardFiltersBar({
icon={<Icon icon={Search} size={4} intent="low" />}
fullWidth
/>
</Box>
</Group>
}
>
<Group gap={4}>
@@ -47,6 +45,5 @@ export function LeaderboardFiltersBar({
</Button>
</Group>
</ControlBar>
</Box>
);
}

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,16 +22,9 @@ 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}>
<LeaderboardTableShell>
<LeaderboardList>
{drivers.map((driver) => (
<RankingRow
key={driver.id}
@@ -39,6 +32,7 @@ export function LeaderboardTable({ drivers, onDriverClick }: LeaderboardTablePro
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;
if (rank <= 3) {
return <RankMedal rank={rank} size={size} />;
}
};
const medal = getMedalEmoji(rank);
return (
<Badge variant={getVariant(rank)} size={badgeSize}>
<Group gap={1}>
{medal && <Text size="xs">{medal}</Text>}
<Text size="xs" weight="bold">#{rank}</Text>
<Group justify="center" align="center">
<Text size={size === 'lg' ? 'md' : 'xs'} weight="bold" variant="low">
#{rank}
</Text>
</Group>
</Badge>
);
}

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>
)}
</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
</Group>
}
identity={
<Group gap={4}>
<Avatar
src={avatarUrl}
alt={name}
width={40}
height={40}
fullWidth
fullHeight
objectFit="cover"
size="md"
/>
</Stack>
<Stack minWidth="0">
<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">
<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>
</TableCell>
<TableCell textAlign="center">
<Text font="mono" weight="bold" color="text-performance-green">
<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>
</TableCell>
</TableRow>
<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}>
<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', 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' };
1: { height: '10rem', variant: 'precision' },
2: { height: '8rem', variant: 'muted' },
3: { height: '6rem', variant: 'muted' },
}[position as 1 | 2 | 3];
return (
<Stack
<Group
key={driver.id}
as="button"
type="button"
onClick={() => onDriverClick?.(driver.id)}
display="flex"
flexDirection="col"
alignItems="center"
bg="transparent"
border={false}
cursor="pointer"
direction="column"
align="center"
gap={4}
>
<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' }}
<Group direction="column" align="center" gap={2}>
<Group
justify="center"
align="center"
>
<Image
<Avatar
src={driver.avatarUrl}
alt={driver.name}
width={112}
height={112}
objectFit="cover"
size={isFirst ? 'lg' : 'md'}
/>
</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>
</Group>
<Text weight="semibold" color="text-white" size={position === 1 ? 'lg' : 'base'} mb={1} block>
{driver.name}
<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>
<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}
<Surface
variant={config.variant as any}
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}`
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 weight="bold" size={position === 1 ? '4xl' : '3xl'} color={config.crown === '#facc15' ? 'text-warning-amber' : config.crown === '#d1d5db' ? 'text-gray-300' : 'text-orange-600'}>
<Text size="3xl" weight="bold" variant="low" opacity={0.2}>
{position}
</Text>
</Stack>
</Stack>
</Surface>
</Group>
);
})}
</Stack>
</Stack>
</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,15 +27,15 @@ 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>
@@ -50,67 +48,13 @@ export function RankingsTable({ drivers, onDriverClick }: RankingsTableProps) {
</TableHead>
<TableBody>
{drivers.map((driver) => (
<TableRow
<RankingRow
key={driver.id}
clickable
{...driver}
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>
);
}

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
rank={<RankBadge rank={position} />}
identity={
<Group gap={4}>
<Avatar
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
alt={team.name}
width={36}
height={36}
fullWidth
fullHeight
objectFit="cover"
size="sm"
/>
</Box>
<Box flexGrow={1} minWidth="0">
<Group direction="column" align="start" gap={0}>
<Text
weight="semibold"
color="text-white"
weight="bold"
variant="high"
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>
<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,16 +20,9 @@ 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}>
<LeaderboardTableShell>
<LeaderboardList>
{teams.map((team) => (
<TeamRankingRow
key={team.id}
@@ -43,6 +37,7 @@ export function TeamLeaderboardTable({ teams, onTeamClick }: TeamLeaderboardTabl
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
rank={<RankBadge rank={rank} />}
identity={
<Group gap={4}>
<Avatar
src={logoUrl || getMediaUrl('team-logo', id)}
alt={name}
width={40}
height={40}
objectFit="cover"
size="md"
/>
</Surface>
<Stack gap={0} flex={1} minWidth="0">
<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>
</TableCell>
<TableCell textAlign="center">
<Text font="mono" weight="bold" variant="primary">
</Group>
}
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>
</TableCell>
<TableCell textAlign="center">
<Text font="mono" weight="bold" variant="success">
<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>
</TableCell>
<TableCell textAlign="center">
<Text variant="low" font="mono">{races}</Text>
</TableCell>
</TableRow>
<Text size="xs" variant="low" block uppercase letterSpacing="widest" weight="bold" fontSize="9px">
Wins
</Text>
</Group>
</Group>
}
/>
);
}

View File

@@ -7,13 +7,15 @@ export class LeaderboardsViewDataBuilder {
apiDto: { drivers: { drivers: DriverLeaderboardItemDTO[] }; teams: GetTeamsLeaderboardOutputDTO }
): LeaderboardsViewData {
return {
drivers: apiDto.drivers.drivers.slice(0, 10).map(driver => ({
drivers: apiDto.drivers.drivers.map(driver => ({
id: driver.id,
name: driver.name,
rating: driver.rating,
skillLevel: driver.skillLevel,
nationality: driver.nationality,
wins: driver.wins,
podiums: driver.podiums,
racesCompleted: driver.racesCompleted,
rank: driver.rank,
avatarUrl: driver.avatarUrl || '',
position: driver.rank,
@@ -25,6 +27,7 @@ export class LeaderboardsViewDataBuilder {
memberCount: team.memberCount,
category: undefined,
totalWins: team.totalWins || 0,
totalRaces: team.totalRaces || 0,
logoUrl: team.logoUrl || '',
position: index + 1,
isRecruiting: team.isRecruiting,

View File

@@ -252,6 +252,8 @@ export const routeMatchers = {
routes.public.drivers,
routes.public.teams,
routes.public.leaderboards,
routes.leaderboards.drivers,
routes.leaderboards.teams,
routes.public.races,
routes.public.sponsorSignup,
routes.auth.login,

View File

@@ -5,6 +5,8 @@ export interface LeaderboardDriverItem {
skillLevel: string;
nationality: string;
wins: number;
podiums: number;
racesCompleted: number;
rank: number;
avatarUrl: string;
position: number;

View File

@@ -5,7 +5,7 @@ export interface LeaderboardTeamItem {
memberCount: number;
category?: string;
totalWins: number;
totalRaces?: number;
totalRaces: number;
logoUrl: string;
position: number;
isRecruiting: boolean;

View File

@@ -46,4 +46,23 @@ module.exports = {
},
},
plugins: [],
safelist: [
{
pattern: /^(grid-cols|gap|p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr)-/,
variants: ['sm', 'md', 'lg', 'xl', '2xl'],
},
{
pattern: /^(w|h|max-w|min-w|max-h|min-h)-/,
variants: ['sm', 'md', 'lg', 'xl', '2xl'],
},
{
pattern: /^(flex|items|justify|self)-/,
variants: ['sm', 'md', 'lg', 'xl', '2xl'],
},
'grid',
'flex',
'block',
'inline-block',
'none',
],
}

View File

@@ -1,11 +1,14 @@
'use client';
import { DriverLeaderboardPreview } from '@/components/leaderboards/DriverLeaderboardPreview';
import { TeamLeaderboardPreview } from '@/components/leaderboards/TeamLeaderboardPreview';
import type { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData';
import { Section } from '@/ui/Section';
import { PageHero } from '@/ui/PageHero';
import { PageHeader } from '@/ui/PageHeader';
import { FeatureGrid } from '@/ui/FeatureGrid';
import { Container } from '@/ui/Container';
import { Stack } from '@/ui/Stack';
import { Group } from '@/ui/Group';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Trophy, Users, Activity } from 'lucide-react';
import React from 'react';
@@ -24,43 +27,86 @@ export function LeaderboardsTemplate({
onNavigateToDrivers,
onNavigateToTeams
}: LeaderboardsTemplateProps) {
const top10Drivers = viewData.drivers.slice(0, 10);
const top5Teams = viewData.teams.slice(0, 5);
// Newcomers: less than 10 races, sorted by rating
const topNewcomers = [...viewData.drivers]
.filter(d => d.racesCompleted < 10)
.sort((a, b) => b.rating - a.rating)
.slice(0, 5);
// All time: sorted by wins
const topAllTime = [...viewData.drivers]
.sort((a, b) => b.wins - a.wins)
.slice(0, 5);
return (
<Section variant="default" padding="lg">
<PageHero
title="Global Standings"
description="Performance metrics for drivers and teams. Rankings are calculated based on competitive results and consistency across all events."
<Section variant="default" padding="none" py={12}>
<Container size="full" padding="lg">
<Stack gap={16}>
<PageHeader
title="Leaderboards"
description="Global Performance Standings"
icon={Activity}
actions={[
{
label: 'Driver Rankings',
onClick: onNavigateToDrivers,
icon: Trophy,
variant: 'primary'
},
{
label: 'Team Standings',
onClick: onNavigateToTeams,
icon: Users,
variant: 'secondary'
action={
<Group gap={4}>
<Button
variant="secondary"
onClick={onNavigateToDrivers}
icon={<Icon icon={Trophy} size={4} />}
>
Drivers
</Button>
<Button
variant="secondary"
onClick={onNavigateToTeams}
icon={<Icon icon={Users} size={4} />}
>
Teams
</Button>
</Group>
}
]}
/>
<FeatureGrid columns={{ base: 1, lg: 2 }} gap={8}>
{/* Top 10 2026 up top */}
<DriverLeaderboardPreview
drivers={viewData.drivers}
title="Top 10 Drivers 2026"
subtitle="Current Season Standings"
drivers={top10Drivers}
onDriverClick={onDriverClick}
onNavigateToDrivers={onNavigateToDrivers}
/>
<FeatureGrid columns={{ base: 1, lg: 2 }} gap={12}>
<TeamLeaderboardPreview
teams={viewData.teams.map(t => ({
teams={top5Teams.map(t => ({
...t,
logoUrl: t.logoUrl || ''
}))}
onTeamClick={onTeamClick}
onNavigateToTeams={onNavigateToTeams}
/>
<Stack gap={12}>
<DriverLeaderboardPreview
title="Top Newcomers"
subtitle="Rising Stars (< 10 Races)"
drivers={topNewcomers}
onDriverClick={onDriverClick}
onNavigateToDrivers={onNavigateToDrivers}
/>
<DriverLeaderboardPreview
title="Top All Time"
subtitle="Most Wins"
drivers={topAllTime}
onDriverClick={onDriverClick}
onNavigateToDrivers={onNavigateToDrivers}
/>
</Stack>
</FeatureGrid>
</Stack>
</Container>
</Section>
);
}

View File

@@ -3,7 +3,7 @@ import { Grid } from './Grid';
interface FeatureGridProps {
children: ReactNode;
columns?: number | { base: number; md?: number; lg?: number };
columns?: number | { base: number; md?: number; lg?: number; xl?: number };
gap?: number;
}

View File

@@ -8,11 +8,9 @@ export interface LeaderboardListProps {
export const LeaderboardList = ({ children }: LeaderboardListProps) => {
return (
<Surface variant="muted" rounded="xl" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
<Box display="flex" flexDirection="col">
{children}
</Box>
</Surface>
);
};

View File

@@ -32,19 +32,19 @@ export const LeaderboardPreviewShell = ({
viewFullLabel
}: LeaderboardPreviewShellProps) => {
return (
<Surface variant="default" rounded="xl" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
<Box padding={6} borderBottom>
<Box display="flex" alignItems="center" justifyContent="between" marginBottom={4}>
<Box display="flex" alignItems="center" gap={4}>
<Box padding={2} rounded="lg" bg={iconBgGradient || "var(--ui-color-bg-surface-muted)"} style={iconColor ? { color: iconColor } : undefined}>
<Icon icon={icon} size={5} intent={iconColor ? undefined : intent} />
<Surface variant="precision" rounded="xl" style={{ overflow: 'hidden' }}>
<Box padding={6} borderBottom borderColor="var(--ui-color-border-muted)">
<Box display="flex" alignItems="center" justifyContent="between" marginBottom={6}>
<Box display="flex" alignItems="center" gap={5}>
<Box padding={3} rounded="xl" bg={iconBgGradient || "var(--ui-color-bg-surface-muted)"} style={iconColor ? { color: iconColor } : undefined}>
<Icon icon={icon} size={6} intent={iconColor ? undefined : intent} />
</Box>
<Box>
<Text size="lg" weight="bold" variant="high" block>
<Text size="xl" weight="bold" variant="high" block>
{title}
</Text>
{subtitle && (
<Text size="sm" variant="low">
<Text size="sm" variant="low" uppercase weight="bold" letterSpacing="widest">
{subtitle}
</Text>
)}
@@ -63,7 +63,7 @@ export const LeaderboardPreviewShell = ({
</Box>
{footer && (
<Box padding={4} borderTop bg="rgba(255,255,255,0.02)">
<Box padding={4} borderTop borderColor="var(--ui-color-border-muted)" bg="rgba(255,255,255,0.02)">
{footer}
</Box>
)}

View File

@@ -0,0 +1,76 @@
import { ReactNode } from 'react';
import { Box } from './Box';
import { Surface } from './Surface';
import { Group } from './Group';
export interface LeaderboardRowProps {
rank: ReactNode;
identity: ReactNode;
stats: ReactNode;
onClick?: () => void;
}
/**
* LeaderboardRow is a semantic UI component for displaying an entry in a leaderboard.
* It follows the "Modern Precision" theme with obsessive detail.
*/
export const LeaderboardRow = ({
rank,
identity,
stats,
onClick
}: LeaderboardRowProps) => {
return (
<Surface
as={onClick ? 'button' : 'div'}
variant="precision"
onClick={onClick}
padding="none"
cursor={onClick ? 'pointer' : 'default'}
width="full"
textAlign="left"
transition="all 0.2s cubic-bezier(0.4, 0, 0.2, 1)"
hoverBg="rgba(25, 140, 255, 0.04)"
display="block"
style={{
border: 'none',
borderBottom: '1px solid var(--ui-color-border-muted)',
background: 'transparent',
position: 'relative'
}}
className="group"
>
<Box
display="flex"
alignItems="center"
gap={6}
paddingX={6}
paddingY={4}
className="transition-transform duration-200 group-hover:translate-x-1"
>
<Box width="10" display="flex" justifyContent="center" flexShrink={0}>
{rank}
</Box>
<Box flexGrow={1} minWidth="0">
{identity}
</Box>
<Group gap={8}>
{stats}
</Group>
</Box>
{/* Hover indicator */}
<Box
position="absolute"
left={0}
top={0}
bottom={0}
width={1}
bg="var(--ui-color-intent-primary)"
className="opacity-0 transition-opacity duration-200 group-hover:opacity-100"
/>
</Surface>
);
};

View File

@@ -9,7 +9,7 @@ export interface LeaderboardTableShellProps {
export const LeaderboardTableShell = ({ children, columns }: LeaderboardTableShellProps) => {
return (
<Surface variant="default" rounded="xl" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
<Surface variant="precision" rounded="xl" style={{ overflow: 'hidden' }} marginBottom={8}>
<Box>
{children}
</Box>

View File

@@ -1,7 +1,6 @@
import { Box } from '@/ui/Box';
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import Link from 'next/link';
import { LucideIcon, ChevronRight } from 'lucide-react';
interface NavLinkProps {
@@ -14,7 +13,7 @@ interface NavLinkProps {
LinkComponent?: React.ComponentType<{ href: string; children: React.ReactNode; className?: string }>;
}
export function NavLink({ href, label, icon, isActive, variant = 'sidebar', collapsed = false, LinkComponent = Link as any }: NavLinkProps) {
export function NavLink({ href, label, icon, isActive, variant = 'sidebar', collapsed = false, LinkComponent }: NavLinkProps) {
const isTop = variant === 'top';
// Radical "Game Menu" Style
@@ -72,6 +71,7 @@ export function NavLink({ href, label, icon, isActive, variant = 'sidebar', coll
</Box>
);
if (LinkComponent) {
return (
<LinkComponent
href={href}
@@ -81,3 +81,13 @@ export function NavLink({ href, label, icon, isActive, variant = 'sidebar', coll
</LinkComponent>
);
}
return (
<a
href={href}
className={`w-full group block ${!isTop ? '' : ''}`}
>
{content}
</a>
);
}

View File

@@ -13,12 +13,14 @@ interface PageHeaderProps {
title: string;
description?: string;
action?: React.ReactNode;
icon?: LucideIcon;
}
export function PageHeader({
title,
description,
action,
icon,
}: PageHeaderProps) {
return (
<Box
@@ -33,7 +35,11 @@ export function PageHeader({
>
<Box>
<Box display="flex" alignItems="center" gap={3} marginBottom={2}>
{icon ? (
<Icon icon={icon} size={8} intent="primary" />
) : (
<Box width={1} height={8} backgroundColor="var(--ui-color-intent-primary)" />
)}
<Heading level={1} weight="bold" uppercase>{title}</Heading>
</Box>
{description && (

View File

@@ -0,0 +1,70 @@
import { Icon } from './Icon';
import { Text } from './Text';
import { Surface } from './Surface';
import { Crown, Medal } from 'lucide-react';
import React from 'react';
export interface RankMedalProps {
rank: number;
size?: 'sm' | 'md' | 'lg';
showIcon?: boolean;
variant?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry' | 'default';
bg?: string;
color?: string;
}
/**
* RankMedal is a semantic UI component for displaying a rank with a medal icon or number.
* It follows the "Modern Precision" theme.
*/
export const RankMedal = ({
rank,
size = 'md',
showIcon = true,
variant,
bg,
color
}: RankMedalProps) => {
const isTop3 = rank <= 3;
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,
};
return (
<Surface
variant="muted"
rounded="full"
style={{
height: sizePx[size],
width: sizePx[size],
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px solid var(--ui-color-border-default)',
backgroundColor: bg,
color: color
}}
>
{isTop3 && showIcon ? (
<Icon icon={rank === 1 ? Crown : Medal} size={iconSize[size] as any} intent={variant as any} />
) : (
<Text weight="bold" size={textSizeMap[size]} variant={variant as any}>{rank}</Text>
)}
</Surface>
);
};

View File

@@ -73,8 +73,11 @@ export const TableHeaderCell = ({ children, textAlign, w, className }: TableHead
const alignClass = textAlign === 'center' ? 'text-center' : (textAlign === 'right' ? 'text-right' : 'text-left');
return (
<th
className={`px-4 py-3 text-xs font-bold uppercase tracking-wider text-[var(--ui-color-text-low)] ${alignClass} ${className || ''}`}
style={w ? { width: w } : undefined}
className={`px-6 py-4 text-[10px] font-bold uppercase tracking-[0.15em] text-[var(--ui-color-text-low)] border-b border-[var(--ui-color-border-muted)] ${alignClass} ${className || ''}`}
style={{
width: w,
background: 'rgba(255, 255, 255, 0.01)'
}}
>
{children}
</th>
@@ -96,7 +99,7 @@ export const TableCell = ({ children, textAlign, className, py, colSpan, w, posi
const alignClass = textAlign === 'center' ? 'text-center' : (textAlign === 'right' ? 'text-right' : 'text-left');
return (
<td
className={`px-4 py-3 text-sm text-[var(--ui-color-text-high)] ${alignClass} ${className || ''}`}
className={`px-6 py-4 text-sm text-[var(--ui-color-text-high)] ${alignClass} ${className || ''}`}
colSpan={colSpan}
style={{
...(py !== undefined ? { paddingTop: `${py * 0.25}rem`, paddingBottom: `${py * 0.25}rem` } : {}),