website refactor

This commit is contained in:
2026-01-18 13:26:35 +01:00
parent 350c78504d
commit 0b301feb61
225 changed files with 1678 additions and 26666 deletions

View File

@@ -0,0 +1,67 @@
import { Box } from '@/ui/Box';
import { Image } from '@/ui/Image';
import { Text } from '@/ui/Text';
interface ActiveDriverCardProps {
name: string;
avatarUrl?: string;
categoryLabel?: string;
categoryColor?: string;
skillLevelLabel?: string;
skillLevelColor?: string;
onClick: () => void;
}
export function ActiveDriverCard({
name,
avatarUrl,
categoryLabel,
categoryColor,
skillLevelLabel,
skillLevelColor,
onClick,
}: ActiveDriverCardProps) {
return (
<Box
as="button"
type="button"
onClick={onClick}
p={3}
rounded="xl"
bg="bg-iron-gray/40"
border
borderColor="border-charcoal-outline"
transition
cursor="pointer"
hoverBorderColor="performance-green/40"
group
textAlign="center"
>
<Box position="relative" w="12" h="12" mx="auto" rounded="full" overflow="hidden" border borderColor="border-charcoal-outline" mb={2}>
<Image src={avatarUrl || '/default-avatar.png'} alt={name} objectFit="cover" fill />
<Box position="absolute" bottom="0" right="0" w="3" h="3" rounded="full" bg="bg-performance-green" border borderColor="border-iron-gray" style={{ borderWidth: '2px' }} />
</Box>
<Text
size="sm"
weight="medium"
color="text-white"
truncate
block
groupHoverTextColor="performance-green"
transition
>
{name}
</Text>
<Box display="flex" alignItems="center" justifyContent="center" gap={1}>
{categoryLabel && (
<Text size="xs" color={categoryColor}>{categoryLabel}</Text>
)}
{skillLevelLabel && (
<Text size="xs" color={skillLevelColor}>{skillLevelLabel}</Text>
)}
</Box>
</Box>
);
}

View File

@@ -0,0 +1,95 @@
import { AchievementCard } from '@/components/achievements/AchievementCard';
import { Box } from '@/ui/Box';
import { Card } from '@/ui/Card';
import { GoalCard } from '@/ui/GoalCard';
import { Heading } from '@/ui/Heading';
import { MilestoneItem } from '@/components/achievements/MilestoneItem';
import { Stack } from '@/ui/Stack';
interface Achievement {
id: string;
title: string;
description: string;
icon: string;
unlockedAt: string;
rarity: 'common' | 'rare' | 'epic' | 'legendary';
}
const mockAchievements: Achievement[] = [
{ id: '1', title: 'First Victory', description: 'Won your first race', icon: '🏆', unlockedAt: '2024-03-15', rarity: 'common' },
{ id: '2', title: '10 Podiums', description: 'Achieved 10 podium finishes', icon: '🥈', unlockedAt: '2024-05-22', rarity: 'rare' },
{ id: '3', title: 'Clean Racer', description: 'Completed 25 races with 0 incidents', icon: '✨', unlockedAt: '2024-08-10', rarity: 'epic' },
{ id: '4', title: 'Comeback King', description: 'Won a race after starting P10 or lower', icon: '⚡', unlockedAt: '2024-09-03', rarity: 'rare' },
{ id: '5', title: 'Perfect Weekend', description: 'Pole, fastest lap, and win in same race', icon: '💎', unlockedAt: '2024-10-17', rarity: 'legendary' },
{ id: '6', title: 'Century Club', description: 'Completed 100 races', icon: '💯', unlockedAt: '2024-11-01', rarity: 'epic' },
];
export function CareerHighlights() {
return (
<Stack gap={6}>
<Card>
<Heading level={3} mb={4}>Key Milestones</Heading>
<Stack gap={3}>
<MilestoneItem
label="First Race"
value="March 15, 2024"
icon="🏁"
/>
<MilestoneItem
label="First Win"
value="March 15, 2024 (Imola)"
icon="🏆"
/>
<MilestoneItem
label="Highest Rating"
value="1487 (Nov 2024)"
icon="📈"
/>
<MilestoneItem
label="Longest Win Streak"
value="4 races (Oct 2024)"
icon="🔥"
/>
<MilestoneItem
label="Most Wins (Track)"
value="Spa-Francorchamps (7)"
icon="🗺️"
/>
<MilestoneItem
label="Favorite Car"
value="Porsche 911 GT3 R (45 races)"
icon="🏎️"
/>
</Stack>
</Card>
<Card>
<Heading level={3} mb={4}>Achievements</Heading>
<Box display="grid" gridCols={{ base: 1, sm: 2 }} gap={3}>
{mockAchievements.map((achievement) => (
<AchievementCard
key={achievement.id}
title={achievement.title}
description={achievement.description}
icon={achievement.icon}
unlockedAt={achievement.unlockedAt}
rarity={achievement.rarity}
/>
))}
</Box>
</Card>
<GoalCard
title="Next Goals"
icon="🎯"
goalLabel="Win 25 races"
currentValue={23}
maxValue={25}
/>
</Stack>
);
}

View File

@@ -0,0 +1,36 @@
import { Box } from '@/ui/Box';
import { Card } from '@/ui/Card';
import { Grid } from '@/ui/Grid';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { StatGridItem } from '@/ui/StatGridItem';
import { TrendingUp } from 'lucide-react';
interface CareerStatsProps {
stats: {
totalRaces: number;
wins: number;
podiums: number;
consistency: number | null;
};
}
export function CareerStats({ stats }: CareerStatsProps) {
return (
<Card>
<Box mb={4}>
<Heading level={2} icon={<Icon icon={TrendingUp} size={5} color="#10b981" />}>
Career Statistics
</Heading>
</Box>
<Grid cols={2} gap={4}>
<StatGridItem label="Races" value={stats.totalRaces} />
<StatGridItem label="Wins" value={stats.wins} color="text-performance-green" />
<StatGridItem label="Podiums" value={stats.podiums} color="text-warning-amber" />
<StatGridItem label="Consistency" value={`${stats.consistency}%`} color="text-primary-blue" />
</Grid>
</Card>
);
}

View File

@@ -1,10 +1,10 @@
import React from 'react';
import { Card } from '@/ui/Card';
import { RankBadge } from '@/ui/RankBadge';
import { RankBadge } from '@/components/leaderboards/RankBadge';
import { DriverIdentity } from '@/components/drivers/DriverIdentity';
import { DriverViewModel } from '@/lib/view-models/DriverViewModel';
import { Stack } from '@/ui/Stack';
import { DriverStats } from '@/ui/DriverStats';
import { DriverStats } from '@/components/drivers/DriverStats';
import { routes } from '@/lib/routing/RouteConfig';
export interface DriverCardProps {

View File

@@ -0,0 +1,109 @@
import React from 'react';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Image } from '@/ui/Image';
import { RatingBadge } from '@/components/drivers/RatingBadge';
interface DriverHeaderPanelProps {
name: string;
avatarUrl?: string;
nationality: string;
rating: number;
globalRank?: number | null;
bio?: string | null;
actions?: React.ReactNode;
}
export function DriverHeaderPanel({
name,
avatarUrl,
nationality,
rating,
globalRank,
bio,
actions
}: DriverHeaderPanelProps) {
const defaultAvatar = 'https://cdn.gridpilot.com/avatars/default.png';
return (
<Box
bg="bg-panel-gray"
rounded="xl"
border
borderColor="border-charcoal-outline"
overflow="hidden"
position="relative"
>
{/* Background Accent */}
<Box
position="absolute"
top={0}
left={0}
right={0}
height="24"
bg="bg-gradient-to-r from-primary-blue/20 to-transparent"
opacity={0.5}
/>
<Box p={6} position="relative">
<Stack direction={{ base: 'col', md: 'row' }} gap={6} align="start" className="md:items-center">
{/* Avatar */}
<Box
width="32"
height="32"
rounded="2xl"
overflow="hidden"
border
borderColor="border-charcoal-outline"
bg="bg-graphite-black"
flexShrink={0}
>
<Image
src={avatarUrl || defaultAvatar}
alt={name}
fill
objectFit="cover"
/>
</Box>
{/* Info */}
<Box flexGrow={1}>
<Stack gap={2}>
<Stack direction="row" align="center" gap={3} wrap>
<Text as="h1" size="3xl" weight="bold" color="text-white">
{name}
</Text>
<RatingBadge rating={rating} size="lg" />
</Stack>
<Stack direction="row" align="center" gap={4} wrap>
<Text size="sm" color="text-gray-400">
{nationality}
</Text>
{globalRank !== undefined && globalRank !== null && (
<Text size="sm" color="text-gray-400">
Global Rank: <Text color="text-warning-amber" weight="semibold">#{globalRank}</Text>
</Text>
)}
</Stack>
{bio && (
<Text size="sm" color="text-gray-400" className="max-w-2xl mt-2" lineClamp={2}>
{bio}
</Text>
)}
</Stack>
</Box>
{/* Actions */}
{actions && (
<Box flexShrink={0}>
{actions}
</Box>
)}
</Stack>
</Box>
</Box>
);
}

View File

@@ -9,9 +9,9 @@ import { Stack } from '@/ui/Stack';
import { StatCard } from '@/ui/StatCard';
import { ProfileHeader } from '@/components/drivers/ProfileHeader';
import { ProfileStats } from './ProfileStats';
import { CareerHighlights } from '@/ui/CareerHighlights';
import { CareerHighlights } from '@/components/drivers/CareerHighlights';
import { DriverRankings } from '@/components/drivers/DriverRankings';
import { PerformanceMetrics } from '@/ui/PerformanceMetrics';
import { PerformanceMetrics } from '@/components/drivers/PerformanceMetrics';
import { useDriverProfile } from "@/hooks/driver/useDriverProfile";
interface DriverProfileProps {

View File

@@ -5,7 +5,7 @@ import { Globe, Trophy, UserPlus, Check } from 'lucide-react';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Button } from '@/ui/Button';
import { RatingBadge } from '@/ui/RatingBadge';
import { RatingBadge } from '@/components/drivers/RatingBadge';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Image } from '@/ui/Image';

View File

@@ -0,0 +1,31 @@
import { Box } from '@/ui/Box';
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { Star, Trophy } from 'lucide-react';
interface DriverRatingPillProps {
rating: number | null;
rank: number | null;
}
export function DriverRatingPill({ rating, rank }: DriverRatingPillProps) {
return (
<Box display="flex" alignItems="center" gap={2} mt={0.5} style={{ fontSize: '11px' }}>
<Box display="inline-flex" alignItems="center" gap={1}>
<Icon icon={Star} size={3} color="var(--warning-amber)" />
<Text color="text-amber-300" className="tabular-nums">
{rating !== null ? rating : '—'}
</Text>
</Box>
{rank !== null && (
<Box display="inline-flex" alignItems="center" gap={1}>
<Icon icon={Trophy} size={3} color="var(--primary-blue)" />
<Text color="text-primary-blue" className="tabular-nums">#{rank}</Text>
</Box>
)}
</Box>
);
}

View File

@@ -0,0 +1,34 @@
import React from 'react';
import { Stack } from '@/ui/Stack';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
interface DriverStatsProps {
rating: number;
wins: number;
podiums: number;
winRate: string;
}
export function DriverStats({ rating, wins, podiums, winRate }: DriverStatsProps) {
return (
<Stack direction="row" align="center" gap={8} textAlign="center">
<Box>
<Text size="2xl" weight="bold" color="text-primary-blue" block>{rating}</Text>
<Text size="xs" color="text-gray-400" block>Rating</Text>
</Box>
<Box>
<Text size="2xl" weight="bold" color="text-green-400" block>{wins}</Text>
<Text size="xs" color="text-gray-400" block>Wins</Text>
</Box>
<Box>
<Text size="2xl" weight="bold" color="text-warning-amber" block>{podiums}</Text>
<Text size="xs" color="text-gray-400" block>Podiums</Text>
</Box>
<Box>
<Text size="sm" color="text-gray-400" block>{winRate}%</Text>
<Text size="xs" color="text-gray-500" block>Win Rate</Text>
</Box>
</Stack>
);
}

View File

@@ -0,0 +1,137 @@
import React from 'react';
import { Box } from '@/ui/Box';
import { Image } from '@/ui/Image';
import { Link } from '@/ui/Link';
import { PlaceholderImage } from '@/ui/PlaceholderImage';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
interface DriverSummaryPillProps {
name: string;
avatarSrc?: string | null;
rating?: number | null;
rank?: number | null;
onClick?: () => void;
href?: string;
ratingComponent?: React.ReactNode;
}
export function DriverSummaryPill({
name,
avatarSrc,
onClick,
href,
ratingComponent,
}: DriverSummaryPillProps) {
const content = (
<>
<Box
w="8"
h="8"
rounded="full"
overflow="hidden"
bg="bg-charcoal-outline"
display="flex"
alignItems="center"
justifyContent="center"
border
borderColor="border-charcoal-outline/80"
>
{avatarSrc ? (
<Image
src={avatarSrc}
alt={name}
width={32}
height={32}
objectFit="cover"
fill
/>
) : (
<PlaceholderImage size={32} />
)}
</Box>
<Stack direction="col" align="start" justify="center">
<Text
size="xs"
weight="semibold"
color="text-white"
truncate
block
style={{ maxWidth: '140px' }}
>
{name}
</Text>
{ratingComponent}
</Stack>
</>
);
if (href) {
return (
<Link
href={href}
block
display="flex"
alignItems="center"
gap={3}
rounded="full"
bg="bg-iron-gray/70"
px={3}
py={1.5}
border
borderColor="border-charcoal-outline/80"
shadow="0 0 18px rgba(0,0,0,0.45)"
transition
hoverBorderColor="primary-blue/60"
className="hover:bg-iron-gray"
>
{content}
</Link>
);
}
if (onClick) {
return (
<Box
as="button"
type="button"
onClick={onClick}
cursor="pointer"
display="flex"
alignItems="center"
gap={3}
rounded="full"
bg="bg-iron-gray/70"
px={3}
py={1.5}
border
borderColor="border-charcoal-outline/80"
shadow="0 0 18px rgba(0,0,0,0.45)"
transition
hoverBorderColor="primary-blue/60"
className="hover:bg-iron-gray"
>
{content}
</Box>
);
}
return (
<Box
display="flex"
alignItems="center"
gap={3}
rounded="full"
bg="bg-iron-gray/70"
px={3}
py={1.5}
border
borderColor="border-charcoal-outline/80"
>
{content}
</Box>
);
}

View File

@@ -1,8 +1,8 @@
import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
import { DriverRatingPill } from '@/ui/DriverRatingPill';
import { DriverSummaryPill as UiDriverSummaryPill } from '@/ui/DriverSummaryPill';
import { DriverRatingPill } from '@/components/drivers/DriverRatingPill';
import { DriverSummaryPill as UiDriverSummaryPill } from '@/components/drivers/DriverSummaryPill';
export interface DriverSummaryPillProps {
driver: DriverViewModel;

View File

@@ -1,7 +1,7 @@
'use client';
import React from 'react';
import { RatingBadge } from '@/ui/RatingBadge';
import { RatingBadge } from '@/components/drivers/RatingBadge';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';

View File

@@ -0,0 +1,27 @@
import { Box } from '@/ui/Box';
import { Icon } from '@/ui/Icon';
import { Input } from '@/ui/Input';
import { Search } from 'lucide-react';
interface DriversSearchProps {
query: string;
onChange: (query: string) => void;
}
export function DriversSearch({ query, onChange }: DriversSearchProps) {
return (
<Box mb={8}>
<Box maxWidth="28rem">
<Input
type="text"
placeholder="Search drivers by name or nationality..."
value={query}
onChange={(e) => onChange(e.target.value)}
icon={<Icon icon={Search} size={5} color="#6b7280" />}
/>
</Box>
</Box>
);
}

View File

@@ -6,7 +6,7 @@ import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Image } from '@/ui/Image';
import { MedalBadge } from '@/ui/MedalBadge';
import { MedalBadge } from '@/components/leaderboards/MedalBadge';
import { MiniStat } from '@/ui/MiniStat';
import { Text } from '@/ui/Text';
import { Flag, Shield, Star, TrendingUp } from 'lucide-react';

View File

@@ -0,0 +1,87 @@
import { Card } from '@/ui/Card';
import { Button } from '@/ui/Button';
import { Car, Download, Trash2, Edit } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Badge } from '@/ui/Badge';
import { Icon } from '@/ui/Icon';
interface DriverLiveryItem {
id: string;
carId: string;
carName: string;
thumbnailUrl: string;
uploadedAt: Date;
isValidated: boolean;
}
interface LiveryCardProps {
livery: DriverLiveryItem;
onEdit?: (id: string) => void;
onDownload?: (id: string) => void;
onDelete?: (id: string) => void;
}
export function LiveryCard({ livery, onEdit, onDownload, onDelete }: LiveryCardProps) {
return (
<Card className="overflow-hidden hover:border-primary-blue/50 transition-colors">
{/* Livery Preview */}
<Box height={48} backgroundColor="deep-graphite" rounded="lg" mb={4} display="flex" center border borderColor="charcoal-outline">
<Icon icon={Car} size={16} color="text-gray-600" />
</Box>
{/* Livery Info */}
<Stack gap={3}>
<Stack direction="row" align="center" justify="between">
<Heading level={3}>{livery.carName}</Heading>
{livery.isValidated ? (
<Badge variant="success">
Validated
</Badge>
) : (
<Badge variant="warning">
Pending
</Badge>
)}
</Stack>
<Text size="xs" color="text-gray-500">
Uploaded {new Date(livery.uploadedAt).toLocaleDateString()}
</Text>
{/* Actions */}
<Stack direction="row" gap={2} pt={2}>
<Button
variant="secondary"
size="sm"
fullWidth
onClick={() => onEdit?.(livery.id)}
icon={<Icon icon={Edit} size={4} />}
>
Edit
</Button>
<Button
variant="secondary"
size="sm"
onClick={() => onDownload?.(livery.id)}
icon={<Icon icon={Download} size={4} />}
aria-label="Download"
>
{null}
</Button>
<Button
variant="danger"
size="sm"
onClick={() => onDelete?.(livery.id)}
icon={<Icon icon={Trash2} size={4} />}
aria-label="Delete"
>
{null}
</Button>
</Stack>
</Stack>
</Card>
);
}

View File

@@ -0,0 +1,86 @@
import { Box } from '@/ui/Box';
import { Card } from '@/ui/Card';
import { Text } from '@/ui/Text';
interface PerformanceMetricsProps {
stats: {
winRate: number;
podiumRate: number;
dnfRate: number;
avgFinish: number;
consistency: number;
bestFinish: number;
worstFinish: number;
};
}
export function PerformanceMetrics({ stats }: PerformanceMetricsProps) {
const getPerformanceVariant = (value: number, type: 'rate' | 'finish' | 'consistency'): 'blue' | 'green' | 'orange' | 'purple' => {
if (type === 'rate') {
if (value >= 30) return 'green';
if (value >= 15) return 'orange';
return 'blue';
}
if (type === 'consistency') {
if (value >= 80) return 'green';
if (value >= 60) return 'orange';
return 'blue';
}
return 'blue';
};
const metrics = [
{
label: 'Win Rate',
value: `${stats.winRate.toFixed(1)}%`,
variant: getPerformanceVariant(stats.winRate, 'rate'),
icon: '🏆'
},
{
label: 'Podium Rate',
value: `${stats.podiumRate.toFixed(1)}%`,
variant: getPerformanceVariant(stats.podiumRate, 'rate'),
icon: '🥇'
},
{
label: 'DNF Rate',
value: `${stats.dnfRate.toFixed(1)}%`,
variant: stats.dnfRate < 10 ? 'green' : 'orange',
icon: '❌'
},
{
label: 'Avg Finish',
value: stats.avgFinish.toFixed(1),
variant: 'blue' as const,
icon: '📊'
},
{
label: 'Consistency',
value: `${stats.consistency.toFixed(0)}%`,
variant: getPerformanceVariant(stats.consistency, 'consistency'),
icon: '🎯'
},
{
label: 'Best / Worst',
value: `${stats.bestFinish} / ${stats.worstFinish}`,
variant: 'blue' as const,
icon: '📈'
}
];
return (
<Box display="grid" responsiveGridCols={{ base: 2, md: 3 }} gap={4}>
{metrics.map((metric, index) => (
<Card key={index}>
<Box p={4} textAlign="center">
<Text size="2xl" block mb={2}>{metric.icon}</Text>
<Text size="sm" color="text-gray-400" block mb={1}>{metric.label}</Text>
<Text size="xl" weight="bold" color="text-white" block>{metric.value}</Text>
</Box>
</Card>
))}
</Box>
);
}

View File

@@ -0,0 +1,112 @@
import { Box } from '@/ui/Box';
import { Card } from '@/ui/Card';
import { CircularProgress } from '@/ui/CircularProgress';
import { Grid } from '@/ui/Grid';
import { GridItem } from '@/ui/GridItem';
import { Heading } from '@/ui/Heading';
import { HorizontalBarChart } from '@/ui/HorizontalBarChart';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Activity, BarChart3, Target, TrendingUp } from 'lucide-react';
interface PerformanceOverviewProps {
stats: {
wins: number;
podiums: number;
totalRaces: number;
consistency: number | null;
dnfs: number;
bestFinish: number;
avgFinish: number | null;
};
}
export function PerformanceOverview({ stats }: PerformanceOverviewProps) {
return (
<Card>
<Box mb={6}>
<Heading level={2} icon={<Icon icon={Activity} size={5} color="#00f2ff" />}>
Performance Overview
</Heading>
</Box>
<Grid cols={12} gap={8}>
<GridItem colSpan={12} lgSpan={6}>
<Stack align="center" gap={4}>
<Stack direction="row" gap={6}>
<CircularProgress
value={stats.wins}
max={stats.totalRaces}
label="Win Rate"
color="#10b981"
/>
<CircularProgress
value={stats.podiums}
max={stats.totalRaces}
label="Podium Rate"
color="#f59e0b"
/>
</Stack>
<Stack direction="row" gap={6}>
<CircularProgress
value={stats.consistency ?? 0}
max={100}
label="Consistency"
color="#3b82f6"
/>
<CircularProgress
value={stats.totalRaces - stats.dnfs}
max={stats.totalRaces}
label="Finish Rate"
color="#00f2ff"
/>
</Stack>
</Stack>
</GridItem>
<GridItem colSpan={12} lgSpan={6}>
<Box mb={4}>
<Heading level={3} icon={<Icon icon={BarChart3} size={4} color="#9ca3af" />}>
Results Breakdown
</Heading>
</Box>
<HorizontalBarChart
data={[
{ label: 'Wins', value: stats.wins, color: 'bg-performance-green' },
{ label: 'Podiums (2nd-3rd)', value: stats.podiums - stats.wins, color: 'bg-warning-amber' },
{ label: 'DNFs', value: stats.dnfs, color: 'bg-red-500' },
]}
maxValue={stats.totalRaces}
/>
<Box mt={6}>
<Grid cols={2} gap={4}>
<Box p={4} style={{ backgroundColor: '#0f1115', borderRadius: '0.75rem', border: '1px solid #262626' }}>
<Stack gap={2}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={TrendingUp} size={4} color="#10b981" />
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase' }}>Best Finish</Text>
</Stack>
<Text size="2xl" weight="bold" color="text-performance-green">P{stats.bestFinish}</Text>
</Stack>
</Box>
<Box p={4} style={{ backgroundColor: '#0f1115', borderRadius: '0.75rem', border: '1px solid #262626' }}>
<Stack gap={2}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Target} size={4} color="#3b82f6" />
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase' }}>Avg Finish</Text>
</Stack>
<Text size="2xl" weight="bold" color="text-primary-blue">
P{(stats.avgFinish ?? 0).toFixed(1)}
</Text>
</Stack>
</Box>
</Grid>
</Box>
</GridItem>
</Grid>
</Card>
);
}

View File

@@ -5,7 +5,7 @@ import { Badge } from '@/ui/Badge';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { CountryFlag } from '@/ui/CountryFlag';
import { DriverRatingPill } from '@/ui/DriverRatingPill';
import { DriverRatingPill } from '@/components/drivers/DriverRatingPill';
import { Heading } from '@/ui/Heading';
import { Image } from '@/ui/Image';
import { PlaceholderImage } from '@/ui/PlaceholderImage';

View File

@@ -8,7 +8,7 @@ import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/Stack';
import { StatCard } from '@/ui/StatCard';
import { RankBadge } from '@/ui/RankBadge';
import { RankBadge } from '@/components/leaderboards/RankBadge';
interface ProfileStatsProps {
driverId?: string;

View File

@@ -0,0 +1,78 @@
import { Box } from '@/ui/Box';
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
import { Flag, UserPlus, Users } from 'lucide-react';
interface RacingProfileProps {
racingStyle: string;
favoriteTrack: string;
favoriteCar: string;
availableHours: string;
lookingForTeam: boolean;
openToRequests: boolean;
}
export function RacingProfile({
racingStyle,
favoriteTrack,
favoriteCar,
availableHours,
lookingForTeam,
openToRequests,
}: RacingProfileProps) {
return (
<Card>
<Box mb={4}>
<Heading level={2} icon={<Icon icon={Flag} size={5} color="#00f2ff" />}>
Racing Profile
</Heading>
</Box>
<Stack gap={4}>
<Box>
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }} block>Racing Style</Text>
<Text color="text-white" weight="medium">{racingStyle}</Text>
</Box>
<Box>
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }} block>Favorite Track</Text>
<Text color="text-white" weight="medium">{favoriteTrack}</Text>
</Box>
<Box>
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }} block>Favorite Car</Text>
<Text color="text-white" weight="medium">{favoriteCar}</Text>
</Box>
<Box>
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }} block>Available</Text>
<Text color="text-white" weight="medium">{availableHours}</Text>
</Box>
{/* Status badges */}
<Box mt={4} pt={4} style={{ borderTop: '1px solid rgba(38, 38, 38, 0.5)' }}>
<Stack gap={2}>
{lookingForTeam && (
<Surface variant="muted" rounded="lg" padding={2} style={{ backgroundColor: 'rgba(16, 185, 129, 0.1)', border: '1px solid rgba(16, 185, 129, 0.3)' }}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Users} size={4} color="#10b981" />
<Text size="sm" color="text-performance-green" weight="medium">Looking for Team</Text>
</Stack>
</Surface>
)}
{openToRequests && (
<Surface variant="muted" rounded="lg" padding={2} style={{ backgroundColor: 'rgba(59, 130, 246, 0.1)', border: '1px solid rgba(59, 130, 246, 0.3)' }}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={UserPlus} size={4} color="#3b82f6" />
<Text size="sm" color="text-primary-blue" weight="medium">Open to Friend Requests</Text>
</Stack>
</Surface>
)}
</Stack>
</Box>
</Stack>
</Card>
);
}

View File

@@ -0,0 +1,29 @@
import React from 'react';
interface RatingBadgeProps {
rating: number;
size?: 'sm' | 'md' | 'lg';
className?: string;
}
export function RatingBadge({ rating, size = 'md', className = '' }: RatingBadgeProps) {
const getColor = (val: number) => {
if (val >= 2500) return 'text-yellow-400 bg-yellow-400/10 border-yellow-400/20';
if (val >= 2000) return 'text-purple-400 bg-purple-400/10 border-purple-400/20';
if (val >= 1500) return 'text-primary-blue bg-primary-blue/10 border-primary-blue/20';
if (val >= 1000) return 'text-performance-green bg-performance-green/10 border-performance-green/20';
return 'text-gray-400 bg-gray-400/10 border-gray-400/20';
};
const sizeMap = {
sm: 'px-1.5 py-0.5 text-[10px]',
md: 'px-2 py-1 text-xs',
lg: 'px-3 py-1.5 text-sm',
};
return (
<div className={`inline-flex items-center justify-center font-mono font-bold rounded border ${sizeMap[size]} ${getColor(rating)} ${className}`}>
{rating.toLocaleString()}
</div>
);
}

View File

@@ -0,0 +1,118 @@
import { Box } from '@/ui/Box';
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { RatingComponent } from '@/components/drivers/RatingComponent';
import { RatingHistoryItem } from '@/components/drivers/RatingHistoryItem';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
interface RatingBreakdownProps {
skillRating?: number;
safetyRating?: number;
sportsmanshipRating?: number;
}
export function RatingBreakdown({
skillRating = 1450,
safetyRating = 92,
sportsmanshipRating = 4.8
}: RatingBreakdownProps) {
return (
<Stack gap={6}>
<Card>
<Heading level={3} mb={6}>Rating Components</Heading>
<Stack gap={6}>
<RatingComponent
label="Skill Rating"
value={skillRating}
maxValue={2000}
color="text-primary-blue"
description="Based on race results, competition strength, and consistency"
breakdown={[
{ label: 'Race Results', percentage: 60 },
{ label: 'Competition Quality', percentage: 25 },
{ label: 'Consistency', percentage: 15 }
]}
/>
<RatingComponent
label="Safety Rating"
value={safetyRating}
maxValue={100}
color="text-performance-green"
suffix="%"
description="Reflects incident-free racing and clean overtakes"
breakdown={[
{ label: 'Incident Rate', percentage: 70 },
{ label: 'Clean Overtakes', percentage: 20 },
{ label: 'Position Awareness', percentage: 10 }
]}
/>
<RatingComponent
label="Sportsmanship"
value={sportsmanshipRating}
maxValue={5}
color="text-warning-amber"
suffix="/5"
description="Community feedback on racing behavior and fair play"
breakdown={[
{ label: 'Peer Reviews', percentage: 50 },
{ label: 'Fair Racing', percentage: 30 },
{ label: 'Team Play', percentage: 20 }
]}
/>
</Stack>
</Card>
<Card>
<Heading level={3} mb={4}>Rating History</Heading>
<Stack gap={3}>
<RatingHistoryItem
date="November 2024"
skillChange={+15}
safetyChange={+2}
sportsmanshipChange={0}
/>
<RatingHistoryItem
date="October 2024"
skillChange={+28}
safetyChange={-1}
sportsmanshipChange={+0.1}
/>
<RatingHistoryItem
date="September 2024"
skillChange={-12}
safetyChange={+3}
sportsmanshipChange={0}
/>
</Stack>
</Card>
<Card borderColor="border-primary-blue/30" bg="bg-charcoal-outline/20">
<Box display="flex" alignItems="center" gap={3} mb={3}>
<Text size="2xl">📈</Text>
<Heading level={3}>Rating Insights</Heading>
</Box>
<Stack as="ul" gap={2}>
<Box as="li" display="flex" alignItems="start" gap={2}>
<Text color="text-performance-green" mt={0.5}></Text>
<Text size="sm" color="text-gray-400">Strong safety rating - keep up the clean racing!</Text>
</Box>
<Box as="li" display="flex" alignItems="start" gap={2}>
<Text color="text-warning-amber" mt={0.5}></Text>
<Text size="sm" color="text-gray-400">Skill rating improving - competitive against higher-rated drivers</Text>
</Box>
<Box as="li" display="flex" alignItems="start" gap={2}>
<Text color="text-primary-blue" mt={0.5}>i</Text>
<Text size="sm" color="text-gray-400">Complete more races to stabilize your ratings</Text>
</Box>
</Stack>
</Card>
</Stack>
);
}

View File

@@ -0,0 +1,52 @@
import { Box } from '@/ui/Box';
import { ProgressBar } from '@/ui/ProgressBar';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
interface RatingComponentProps {
label: string;
value: number;
maxValue: number;
color: string;
suffix?: string;
description: string;
breakdown: { label: string; percentage: number }[];
}
export function RatingComponent({
label,
value,
maxValue,
color,
suffix = '',
description,
breakdown,
}: RatingComponentProps) {
const percentage = (value / maxValue) * 100;
return (
<Box>
<Box display="flex" alignItems="center" justifyContent="between" mb={2}>
<Text weight="medium" color="text-white">{label}</Text>
<Text size="2xl" weight="bold" color={color}>
{value}{suffix}
</Text>
</Box>
<ProgressBar value={percentage} max={100} color={color} mb={3} />
<Text size="xs" color="text-gray-400" block mb={3}>{description}</Text>
<Stack gap={1}>
{breakdown.map((item, index) => (
<Box key={index} display="flex" alignItems="center" justifyContent="between">
<Text size="xs" color="text-gray-500">{item.label}</Text>
<Text size="xs" color="text-gray-400">{item.percentage}%</Text>
</Box>
))}
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,48 @@
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
interface RatingHistoryItemProps {
date: string;
skillChange: number;
safetyChange: number;
sportsmanshipChange: number;
}
export function RatingHistoryItem({
date,
skillChange,
safetyChange,
sportsmanshipChange,
}: RatingHistoryItemProps) {
const formatChange = (value: number) => {
if (value === 0) return '—';
return value > 0 ? `+${value}` : `${value}`;
};
const getChangeColor = (value: number) => {
if (value === 0) return 'text-gray-500';
return value > 0 ? 'text-performance-green' : 'text-red-400';
};
return (
<Box display="flex" alignItems="center" justifyContent="between" p={3} rounded="md" bg="bg-deep-graphite" border borderColor="border-charcoal-outline">
<Text color="text-white" size="sm">{date}</Text>
<Box display="flex" alignItems="center" gap={4}>
<Box textAlign="center">
<Text size="xs" color="text-gray-500" block mb={1} style={{ fontSize: '10px' }}>Skill</Text>
<Text size="xs" weight="bold" color={getChangeColor(skillChange)}>{formatChange(skillChange)}</Text>
</Box>
<Box textAlign="center">
<Text size="xs" color="text-gray-500" block mb={1} style={{ fontSize: '10px' }}>Safety</Text>
<Text size="xs" weight="bold" color={getChangeColor(safetyChange)}>{formatChange(safetyChange)}</Text>
</Box>
<Box textAlign="center">
<Text size="xs" color="text-gray-500" block mb={1} style={{ fontSize: '10px' }}>Sports</Text>
<Text size="xs" weight="bold" color={getChangeColor(sportsmanshipChange)}>{formatChange(sportsmanshipChange)}</Text>
</Box>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,89 @@
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { BarChart3 } from 'lucide-react';
const SKILL_LEVELS = [
{ id: 'pro', label: 'Pro', icon: BarChart3, color: 'text-yellow-400', bgColor: 'bg-yellow-400/10', borderColor: 'border-yellow-400/30' },
{ id: 'advanced', label: 'Advanced', icon: BarChart3, color: 'text-purple-400', bgColor: 'bg-purple-400/10', borderColor: 'border-purple-400/30' },
{ id: 'intermediate', label: 'Intermediate', icon: BarChart3, color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', borderColor: 'border-primary-blue/30' },
{ id: 'beginner', label: 'Beginner', icon: BarChart3, color: 'text-green-400', bgColor: 'bg-green-400/10', borderColor: 'border-green-400/30' },
];
interface SkillDistributionProps {
drivers: {
skillLevel?: string;
}[];
}
export function SkillDistribution({ drivers }: SkillDistributionProps) {
const distribution = SKILL_LEVELS.map((level) => ({
...level,
count: drivers.filter((d) => d.skillLevel === level.id).length,
percentage: drivers.length > 0
? Math.round((drivers.filter((d) => d.skillLevel === level.id).length / drivers.length) * 100)
: 0,
}));
return (
<Box mb={10}>
<Box display="flex" alignItems="center" gap={3} mb={4}>
<Box
display="flex"
h="10"
w="10"
alignItems="center"
justifyContent="center"
rounded="xl"
bg="bg-neon-aqua/10"
border
borderColor="border-neon-aqua/20"
>
<Icon icon={BarChart3} size={5} color="var(--neon-aqua)" />
</Box>
<Box>
<Heading level={2}>Skill Distribution</Heading>
<Text size="xs" color="text-gray-500">Driver population by skill level</Text>
</Box>
</Box>
<Box display="grid" responsiveGridCols={{ base: 2, lg: 4 }} gap={4}>
{distribution.map((level) => {
return (
<Box
key={level.id}
p={4}
rounded="xl"
border
className={`${level.bgColor} ${level.borderColor}`}
>
<Box display="flex" alignItems="center" justifyContent="between" mb={3}>
<Icon icon={level.icon} size={5} className={level.color} />
<Text size="2xl" weight="bold" className={level.color}>{level.count}</Text>
</Box>
<Text color="text-white" weight="medium" block mb={1}>{level.label}</Text>
<Box fullWidth h="2" rounded="full" bg="bg-deep-graphite/50" overflow="hidden">
<Box
h="full"
rounded="full"
transition
className={
level.id === 'pro' ? 'bg-yellow-400' :
level.id === 'advanced' ? 'bg-purple-400' :
level.id === 'intermediate' ? 'bg-primary-blue' :
'bg-green-400'
}
style={{ width: `${level.percentage}%` }}
/>
</Box>
<Text size="xs" color="text-gray-500" block mt={1}>{level.percentage}% of drivers</Text>
</Box>
);
})}
</Box>
</Box>
);
}

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { LucideIcon } from 'lucide-react';
import { Button } from '@/ui/Button';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
interface SkillLevelButtonProps {
label: string;
icon: LucideIcon;
color: string;
bgColor: string;
borderColor: string;
count: number;
onClick: () => void;
}
export function SkillLevelButton({
label,
icon,
color,
bgColor,
borderColor,
count,
onClick,
}: SkillLevelButtonProps) {
return (
<Button
variant="ghost"
onClick={onClick}
fullWidth
className={`${bgColor} border ${borderColor} flex items-center justify-between p-3 rounded-lg h-auto`}
>
<Stack direction="row" align="center" gap={2}>
<Icon icon={icon} size={4} className={color} />
<Text weight="medium" color="text-white">{label}</Text>
</Stack>
<Text size="sm" color="text-gray-400">{count} teams</Text>
</Button>
);
}

View File

@@ -0,0 +1,85 @@
import React from 'react';
import { LucideIcon, ChevronRight, UserPlus } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Badge } from '@/ui/Badge';
interface SkillLevelHeaderProps {
label: string;
icon: LucideIcon;
bgColor: string;
borderColor: string;
color: string;
description: string;
teamCount: number;
recruitingCount: number;
isExpanded: boolean;
onToggle: () => void;
showToggle: boolean;
}
export function SkillLevelHeader({
label,
icon,
bgColor,
borderColor,
color,
description,
teamCount,
recruitingCount,
isExpanded,
onToggle,
showToggle,
}: SkillLevelHeaderProps) {
return (
<Box display="flex" alignItems="center" justifyContent="between" mb={4}>
<Stack direction="row" align="center" gap={3}>
<Box
display="flex"
center
width="11"
height="11"
rounded="xl"
className={`${bgColor} border ${borderColor}`}
>
<Icon icon={icon} size={5} className={color} />
</Box>
<Box>
<Stack direction="row" align="center" gap={2}>
<Heading level={2}>{label}</Heading>
<Badge variant="default">
{teamCount} {teamCount === 1 ? 'team' : 'teams'}
</Badge>
{recruitingCount > 0 && (
<Badge variant="success" icon={UserPlus}>
{recruitingCount} recruiting
</Badge>
)}
</Stack>
<Text size="sm" color="text-gray-500">{description}</Text>
</Box>
</Stack>
{showToggle && (
<Box
as="button"
type="button"
onClick={onToggle}
display="flex"
alignItems="center"
gap={1}
px={3}
py={1.5}
rounded="lg"
className="text-sm text-gray-400 hover:text-white hover:bg-iron-gray/50 transition-all"
>
<Text size="sm">{isExpanded ? 'Show less' : `View all ${teamCount}`}</Text>
<Icon icon={ChevronRight} size={4} className={`transition-transform ${isExpanded ? 'rotate-90' : ''}`} />
</Box>
)}
</Box>
);
}