website refactor
This commit is contained in:
67
apps/website/components/drivers/ActiveDriverCard.tsx
Normal file
67
apps/website/components/drivers/ActiveDriverCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
95
apps/website/components/drivers/CareerHighlights.tsx
Normal file
95
apps/website/components/drivers/CareerHighlights.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
36
apps/website/components/drivers/CareerStats.tsx
Normal file
36
apps/website/components/drivers/CareerStats.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
109
apps/website/components/drivers/DriverHeaderPanel.tsx
Normal file
109
apps/website/components/drivers/DriverHeaderPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
31
apps/website/components/drivers/DriverRatingPill.tsx
Normal file
31
apps/website/components/drivers/DriverRatingPill.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
34
apps/website/components/drivers/DriverStats.tsx
Normal file
34
apps/website/components/drivers/DriverStats.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
137
apps/website/components/drivers/DriverSummaryPill.tsx
Normal file
137
apps/website/components/drivers/DriverSummaryPill.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
27
apps/website/components/drivers/DriversSearch.tsx
Normal file
27
apps/website/components/drivers/DriversSearch.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
87
apps/website/components/drivers/LiveryCard.tsx
Normal file
87
apps/website/components/drivers/LiveryCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
86
apps/website/components/drivers/PerformanceMetrics.tsx
Normal file
86
apps/website/components/drivers/PerformanceMetrics.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
112
apps/website/components/drivers/PerformanceOverview.tsx
Normal file
112
apps/website/components/drivers/PerformanceOverview.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
78
apps/website/components/drivers/RacingProfile.tsx
Normal file
78
apps/website/components/drivers/RacingProfile.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
29
apps/website/components/drivers/RatingBadge.tsx
Normal file
29
apps/website/components/drivers/RatingBadge.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
118
apps/website/components/drivers/RatingBreakdown.tsx
Normal file
118
apps/website/components/drivers/RatingBreakdown.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
52
apps/website/components/drivers/RatingComponent.tsx
Normal file
52
apps/website/components/drivers/RatingComponent.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
48
apps/website/components/drivers/RatingHistoryItem.tsx
Normal file
48
apps/website/components/drivers/RatingHistoryItem.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
89
apps/website/components/drivers/SkillDistribution.tsx
Normal file
89
apps/website/components/drivers/SkillDistribution.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
41
apps/website/components/drivers/SkillLevelButton.tsx
Normal file
41
apps/website/components/drivers/SkillLevelButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
85
apps/website/components/drivers/SkillLevelHeader.tsx
Normal file
85
apps/website/components/drivers/SkillLevelHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user