website refactor
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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" />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -5,6 +5,8 @@ export interface LeaderboardDriverItem {
|
||||
skillLevel: string;
|
||||
nationality: string;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
racesCompleted: number;
|
||||
rank: number;
|
||||
avatarUrl: string;
|
||||
position: number;
|
||||
|
||||
@@ -5,7 +5,7 @@ export interface LeaderboardTeamItem {
|
||||
memberCount: number;
|
||||
category?: string;
|
||||
totalWins: number;
|
||||
totalRaces?: number;
|
||||
totalRaces: number;
|
||||
logoUrl: string;
|
||||
position: number;
|
||||
isRecruiting: boolean;
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
76
apps/website/ui/LeaderboardRow.tsx
Normal file
76
apps/website/ui/LeaderboardRow.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
@@ -80,4 +80,14 @@ export function NavLink({ href, label, icon, isActive, variant = 'sidebar', coll
|
||||
{content}
|
||||
</LinkComponent>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
className={`w-full group block ${!isTop ? '' : ''}`}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
70
apps/website/ui/RankMedal.tsx
Normal file
70
apps/website/ui/RankMedal.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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` } : {}),
|
||||
|
||||
Reference in New Issue
Block a user