website refactor

This commit is contained in:
2026-01-15 19:55:46 +01:00
parent 5ef149b782
commit ce7be39155
154 changed files with 436 additions and 356 deletions

View File

@@ -0,0 +1,116 @@
import React from 'react';
import { Crown, Flag } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Image } from '@/ui/Image';
import { mediaConfig } from '@/lib/config/mediaConfig';
interface LeaderboardItemProps {
position: number;
name: string;
avatarUrl?: string;
nationality: string;
rating: number;
wins: number;
skillLevelLabel?: string;
skillLevelColor?: string;
categoryLabel?: string;
categoryColor?: string;
onClick: () => void;
}
export function LeaderboardItem({
position,
name,
avatarUrl,
nationality,
rating,
wins,
skillLevelLabel,
skillLevelColor,
categoryLabel,
categoryColor,
onClick,
}: LeaderboardItemProps) {
const getMedalColor = (pos: number) => {
switch (pos) {
case 1: return 'text-yellow-400';
case 2: return 'text-gray-300';
case 3: return 'text-amber-600';
default: return 'text-gray-500';
}
};
const getMedalBg = (pos: number) => {
switch (pos) {
case 1: return 'bg-yellow-400/10 border-yellow-400/30';
case 2: return 'bg-gray-300/10 border-gray-300/30';
case 3: return 'bg-amber-600/10 border-amber-600/30';
default: return 'bg-iron-gray/50 border-charcoal-outline';
}
};
return (
<Box
as="button"
type="button"
onClick={onClick}
display="flex"
alignItems="center"
gap={4}
px={4}
py={3}
fullWidth
textAlign="left"
className="hover:bg-iron-gray/30 transition-colors group"
>
{/* Position */}
<Box
width="8"
height="8"
display="flex"
center
rounded="full"
border
className={`${getMedalBg(position)} ${getMedalColor(position)} text-xs font-bold`}
>
{position <= 3 ? <Crown className="w-3.5 h-3.5" /> : position}
</Box>
{/* Avatar */}
<Box position="relative" width="9" height="9" rounded="full" overflow="hidden" border={true} borderColor="border-charcoal-outline">
<Image src={avatarUrl || mediaConfig.avatars.defaultFallback} alt={name} fill objectFit="cover" />
</Box>
{/* Info */}
<Box flexGrow={1} minWidth="0">
<Text weight="medium" color="text-white" truncate block className="group-hover:text-primary-blue transition-colors">
{name}
</Text>
<Stack direction="row" align="center" gap={2}>
<Flag className="w-3 h-3 text-gray-500" />
<Text size="xs" color="text-gray-500">{nationality}</Text>
{categoryLabel && (
<Text size="xs" className={categoryColor}>{categoryLabel}</Text>
)}
{skillLevelLabel && (
<Text size="xs" className={skillLevelColor}>{skillLevelLabel}</Text>
)}
</Stack>
</Box>
{/* Stats */}
<Stack direction="row" align="center" gap={4}>
<Box textAlign="center">
<Text color="text-primary-blue" weight="semibold" font="mono" block>{rating.toLocaleString()}</Text>
<Text size="xs" color="text-gray-500" block style={{ fontSize: '10px' }}>Rating</Text>
</Box>
<Box textAlign="center">
<Text color="text-performance-green" weight="semibold" font="mono" block>{wins}</Text>
<Text size="xs" color="text-gray-500" block style={{ fontSize: '10px' }}>Wins</Text>
</Box>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,105 @@
import { routes } from '@/lib/routing/RouteConfig';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { LeaderboardItem } from '@/components/leaderboards/LeaderboardItem';
import { LeaderboardList } from '@/ui/LeaderboardList';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Award, ChevronRight } from 'lucide-react';
const SKILL_LEVELS = [
{ id: 'pro', label: 'Pro', color: 'text-yellow-400' },
{ id: 'advanced', label: 'Advanced', color: 'text-purple-400' },
{ id: 'intermediate', label: 'Intermediate', color: 'text-primary-blue' },
{ id: 'beginner', label: 'Beginner', color: 'text-green-400' },
];
const CATEGORIES = [
{ id: 'beginner', label: 'Beginner', color: 'text-green-400' },
{ id: 'intermediate', label: 'Intermediate', color: 'text-primary-blue' },
{ id: 'advanced', label: 'Advanced', color: 'text-purple-400' },
{ id: 'pro', label: 'Pro', color: 'text-yellow-400' },
{ id: 'endurance', label: 'Endurance', color: 'text-orange-400' },
{ id: 'sprint', label: 'Sprint', color: 'text-red-400' },
];
interface LeaderboardPreviewProps {
drivers: {
id: string;
name: string;
avatarUrl?: string;
nationality: string;
rating: number;
wins: number;
skillLevel?: string;
category?: string;
}[];
onDriverClick: (id: string) => void;
onNavigate: (href: string) => void;
}
export function LeaderboardPreview({ drivers, onDriverClick, onNavigate }: LeaderboardPreviewProps) {
const top5 = drivers.slice(0, 5);
return (
<Stack gap={4} mb={10}>
<Stack direction="row" align="center" justify="between">
<Stack direction="row" align="center" gap={3}>
<Box
display="flex"
center
w="10"
h="10"
rounded="xl"
bg="bg-gradient-to-br from-yellow-400/20 to-amber-600/10"
border
borderColor="border-yellow-400/30"
>
<Icon icon={Award} size={5} color="rgb(250, 204, 21)" />
</Box>
<Box>
<Heading level={2}>Top Drivers</Heading>
<Text size="xs" color="text-gray-500">Highest rated competitors</Text>
</Box>
</Stack>
<Button
variant="secondary"
onClick={() => onNavigate(routes.leaderboards.drivers)}
icon={<Icon icon={ChevronRight} size={4} />}
>
Full Rankings
</Button>
</Stack>
<LeaderboardList>
{top5.map((driver, index) => {
const levelConfig = SKILL_LEVELS.find((l) => l.id === driver.skillLevel);
const categoryConfig = CATEGORIES.find((c) => c.id === driver.category);
const position = index + 1;
return (
<LeaderboardItem
key={driver.id}
position={position}
name={driver.name}
avatarUrl={driver.avatarUrl}
nationality={driver.nationality}
rating={driver.rating}
wins={driver.wins}
skillLevelLabel={levelConfig?.label}
skillLevelColor={levelConfig?.color}
categoryLabel={categoryConfig?.label}
categoryColor={categoryConfig?.color}
onClick={() => onDriverClick(driver.id)}
/>
);
})}
</LeaderboardList>
</Stack>
);
}

View File

@@ -0,0 +1,14 @@
import React, { ReactNode } from 'react';
import { Stack } from '@/ui/Stack';
interface RankingListProps {
children: ReactNode;
}
export function RankingList({ children }: RankingListProps) {
return (
<Stack gap={3}>
{children}
</Stack>
);
}

View File

@@ -0,0 +1,70 @@
import React from 'react';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
interface RankingListItemProps {
name: string;
type: 'overall' | 'league';
rank: number;
totalDrivers: number;
percentile: number;
rating: number;
}
export function RankingListItem({
name,
type,
rank,
totalDrivers,
percentile,
rating,
}: RankingListItemProps) {
return (
<Box
display="flex"
alignItems="center"
justifyContent="between"
py={2}
px={3}
rounded="lg"
bg="bg-deep-graphite/60"
>
<Stack gap={0}>
<Text size="sm" weight="medium" color="text-white">
{name}
</Text>
<Text size="xs" color="text-gray-400">
{type === 'overall' ? 'Overall' : 'League'} ranking
</Text>
</Stack>
<Box display="flex" alignItems="center" gap={6} textAlign="right">
<Box>
<Text color="text-primary-blue" size="base" weight="semibold" block>
#{rank}
</Text>
<Text color="text-gray-500" size="xs" block>Position</Text>
</Box>
<Box>
<Text color="text-white" size="sm" weight="semibold" block>
{totalDrivers}
</Text>
<Text color="text-gray-500" size="xs" block>Drivers</Text>
</Box>
<Box>
<Text color="text-green-400" size="sm" weight="semibold" block>
{percentile.toFixed(1)}%
</Text>
<Text color="text-gray-500" size="xs" block>Percentile</Text>
</Box>
<Box>
<Text color="text-warning-amber" size="sm" weight="semibold" block>
{rating}
</Text>
<Text color="text-gray-500" size="xs" block>Rating</Text>
</Box>
</Box>
</Box>
);
}