website refactor

This commit is contained in:
2026-01-17 15:46:55 +01:00
parent 4d5ce9bfd6
commit 72a626ce71
346 changed files with 19308 additions and 8605 deletions

View File

@@ -0,0 +1,86 @@
'use client';
import React from 'react';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
interface DriverPerformanceOverviewProps {
stats: {
wins: number;
podiums: number;
totalRaces: number;
consistency: number;
dnfs: number;
bestFinish: number;
avgFinish: number;
};
}
export function DriverPerformanceOverview({ stats }: DriverPerformanceOverviewProps) {
const winRate = stats.totalRaces > 0 ? (stats.wins / stats.totalRaces) * 100 : 0;
const podiumRate = stats.totalRaces > 0 ? (stats.podiums / stats.totalRaces) * 100 : 0;
const metrics = [
{ label: 'Win Rate', value: `${winRate.toFixed(1)}%`, color: 'text-performance-green' },
{ label: 'Podium Rate', value: `${podiumRate.toFixed(1)}%`, color: 'text-warning-amber' },
{ label: 'Best Finish', value: `P${stats.bestFinish}`, color: 'text-white' },
{ label: 'Avg Finish', value: `P${stats.avgFinish.toFixed(1)}`, color: 'text-gray-400' },
{ label: 'Consistency', value: `${stats.consistency}%`, color: 'text-neon-aqua' },
{ label: 'DNFs', value: stats.dnfs, color: 'text-red-500' },
];
return (
<Box display="flex" flexDirection="col" gap={6} rounded="2xl" border borderColor="border-charcoal-outline" bg="bg-deep-charcoal/50" p={6}>
<Heading level={3}>Performance Overview</Heading>
<Box display="grid" gridCols={{ base: 2, md: 3, lg: 6 }} gap={6}>
{metrics.map((metric, index) => (
<Box key={index} display="flex" flexDirection="col" gap={1}>
<Text size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="wider">
{metric.label}
</Text>
<Text size="xl" weight="bold" font="mono" color={metric.color}>
{metric.value}
</Text>
</Box>
))}
</Box>
{/* Visual Progress Bars */}
<Box display="flex" flexDirection="col" gap={4} mt={2}>
<Box display="flex" flexDirection="col" gap={2}>
<Box display="flex" justifyContent="between" alignItems="center">
<Text size="xs" weight="bold" color="text-gray-400">Win Rate</Text>
<Text size="xs" weight="bold" font="mono" color="text-performance-green">{winRate.toFixed(1)}%</Text>
</Box>
<Box h="1.5" w="full" rounded="full" bg="bg-charcoal-outline" overflow="hidden">
<Box
h="full"
bg="bg-performance-green"
shadow="shadow-[0_0_8px_rgba(34,197,94,0.4)]"
transition
width={`${winRate}%`}
/>
</Box>
</Box>
<Box display="flex" flexDirection="col" gap={2}>
<Box display="flex" justifyContent="between" alignItems="center">
<Text size="xs" weight="bold" color="text-gray-400">Podium Rate</Text>
<Text size="xs" weight="bold" font="mono" color="text-warning-amber">{podiumRate.toFixed(1)}%</Text>
</Box>
<Box h="1.5" w="full" rounded="full" bg="bg-charcoal-outline" overflow="hidden">
<Box
h="full"
bg="bg-warning-amber"
shadow="shadow-[0_0_8px_rgba(255,190,77,0.4)]"
transition
width={`${podiumRate}%`}
/>
</Box>
</Box>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,106 @@
'use client';
import React from 'react';
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 { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Image } from '@/ui/Image';
import { SafetyRatingBadge } from './SafetyRatingBadge';
interface DriverProfileHeaderProps {
name: string;
avatarUrl?: string | null;
nationality: string;
rating: number;
safetyRating?: number;
globalRank?: number;
bio?: string | null;
friendRequestSent: boolean;
onAddFriend: () => void;
}
export function DriverProfileHeader({
name,
avatarUrl,
nationality,
rating,
safetyRating = 92,
globalRank,
bio,
friendRequestSent,
onAddFriend,
}: DriverProfileHeaderProps) {
const defaultAvatar = 'https://cdn.gridpilot.com/avatars/default.png';
return (
<Box position="relative" overflow="hidden" rounded="2xl" border borderColor="border-charcoal-outline" bg="bg-deep-charcoal" p={{ base: 6, lg: 8 }}>
{/* Background Accents */}
<Box position="absolute" right="-24" top="-24" w="96" h="96" rounded="full" bg="bg-primary-blue/5" blur="3xl" />
<Box position="relative" display="flex" flexDirection={{ base: 'col', lg: 'row' }} gap={8}>
{/* Avatar */}
<Box position="relative" h={{ base: '32', lg: '40' }} w={{ base: '32', lg: '40' }} flexShrink={0} overflow="hidden" rounded="2xl" border={true} borderWidth="2px" borderColor="border-charcoal-outline" bg="bg-deep-graphite" shadow="2xl">
<Image
src={avatarUrl || defaultAvatar}
alt={name}
fill
objectFit="cover"
/>
</Box>
{/* Info */}
<Box display="flex" flexGrow={1} flexDirection="col" gap={4}>
<Box display="flex" flexDirection={{ base: 'col', lg: 'row' }} alignItems={{ lg: 'center' }} justifyContent="between" gap={2}>
<Box>
<Stack direction="row" align="center" gap={3} mb={1}>
<Heading level={1}>{name}</Heading>
{globalRank && (
<Box display="flex" alignItems="center" gap={1} rounded="md" bg="bg-warning-amber/10" px={2} py={0.5} border borderColor="border-warning-amber/20">
<Trophy size={12} color="#FFBE4D" />
<Text size="xs" weight="bold" font="mono" color="text-warning-amber">
#{globalRank}
</Text>
</Box>
)}
</Stack>
<Stack direction="row" align="center" gap={4}>
<Stack direction="row" align="center" gap={1.5}>
<Globe size={14} color="#6B7280" />
<Text size="sm" color="text-gray-400">{nationality}</Text>
</Stack>
<Box w="1" h="1" rounded="full" bg="bg-gray-700" />
<Stack direction="row" align="center" gap={2}>
<RatingBadge rating={rating} size="sm" />
<SafetyRatingBadge rating={safetyRating} size="sm" />
</Stack>
</Stack>
</Box>
<Box mt={{ base: 4, lg: 0 }}>
<Button
variant={friendRequestSent ? 'secondary' : 'primary'}
onClick={onAddFriend}
disabled={friendRequestSent}
icon={friendRequestSent ? <Check size={18} /> : <UserPlus size={18} />}
>
{friendRequestSent ? 'Request Sent' : 'Add Friend'}
</Button>
</Box>
</Box>
{bio && (
<Box maxWidth="3xl">
<Text size="sm" color="text-gray-400" leading="relaxed">
{bio}
</Text>
</Box>
)}
</Box>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,57 @@
'use client';
import React from 'react';
import { LayoutDashboard, BarChart3, ShieldCheck } from 'lucide-react';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
export type ProfileTab = 'overview' | 'stats' | 'ratings';
interface DriverProfileTabsProps {
activeTab: ProfileTab;
onTabChange: (tab: ProfileTab) => void;
}
export function DriverProfileTabs({ activeTab, onTabChange }: DriverProfileTabsProps) {
const tabs = [
{ id: 'overview', label: 'Overview', icon: LayoutDashboard },
{ id: 'stats', label: 'Career Stats', icon: BarChart3 },
{ id: 'ratings', label: 'Ratings', icon: ShieldCheck },
] as const;
return (
<Box display="flex" alignItems="center" gap={1} borderBottom borderColor="border-charcoal-outline">
{tabs.map((tab) => {
const isActive = activeTab === tab.id;
const Icon = tab.icon;
return (
<Box
as="button"
key={tab.id}
onClick={() => onTabChange(tab.id)}
position="relative"
display="flex"
alignItems="center"
gap={2}
px={6}
py={4}
transition
hoverBg="bg-white/5"
color={isActive ? 'text-primary-blue' : 'text-gray-500'}
hoverTextColor={isActive ? 'text-primary-blue' : 'text-gray-300'}
>
<Icon size={18} />
<Text size="sm" weight={isActive ? 'bold' : 'medium'} color="inherit">
{tab.label}
</Text>
{isActive && (
<Box position="absolute" bottom="0" left="0" h="0.5" w="full" bg="bg-primary-blue" shadow="shadow-[0_0_8px_rgba(25,140,255,0.5)]" />
)}
</Box>
);
})}
</Box>
);
}

View File

@@ -0,0 +1,76 @@
'use client';
import React from 'react';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { MapPin, Car, Clock, Users2, MailCheck } from 'lucide-react';
interface DriverRacingProfileProps {
racingStyle?: string | null;
favoriteTrack?: string | null;
favoriteCar?: string | null;
availableHours?: string | null;
lookingForTeam?: boolean;
openToRequests?: boolean;
}
export function DriverRacingProfile({
racingStyle,
favoriteTrack,
favoriteCar,
availableHours,
lookingForTeam,
openToRequests,
}: DriverRacingProfileProps) {
const details = [
{ label: 'Racing Style', value: racingStyle || 'Not specified', icon: Users2 },
{ label: 'Favorite Track', value: favoriteTrack || 'Not specified', icon: MapPin },
{ label: 'Favorite Car', value: favoriteCar || 'Not specified', icon: Car },
{ label: 'Availability', value: availableHours || 'Not specified', icon: Clock },
];
return (
<Box display="flex" flexDirection="col" gap={6} rounded="2xl" border borderColor="border-charcoal-outline" bg="bg-deep-charcoal/50" p={6}>
<Box display="flex" alignItems="center" justifyContent="between">
<Heading level={3}>Racing Profile</Heading>
<Stack direction="row" gap={2}>
{lookingForTeam && (
<Box display="flex" alignItems="center" gap={1.5} rounded="full" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20" px={3} py={1}>
<Users2 size={12} color="#198CFF" />
<Text size="xs" weight="bold" color="text-primary-blue" uppercase letterSpacing="tight">Looking for Team</Text>
</Box>
)}
{openToRequests && (
<Box display="flex" alignItems="center" gap={1.5} rounded="full" bg="bg-performance-green/10" border borderColor="border-performance-green/20" px={3} py={1}>
<MailCheck size={12} color="#22C55E" />
<Text size="xs" weight="bold" color="text-performance-green" uppercase letterSpacing="tight">Open to Requests</Text>
</Box>
)}
</Stack>
</Box>
<Box display="grid" gridCols={{ base: 1, sm: 2 }} gap={4}>
{details.map((detail, index) => {
const Icon = detail.icon;
return (
<Box key={index} display="flex" alignItems="center" gap={4} rounded="xl" border borderColor="border-charcoal-outline/50" bg="bg-deep-graphite/50" p={4}>
<Box display="flex" h="10" w="10" alignItems="center" justifyContent="center" rounded="lg" bg="bg-charcoal-outline/50" color="text-gray-400">
<Icon size={20} />
</Box>
<Box display="flex" flexDirection="col">
<Text size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="wider">
{detail.label}
</Text>
<Text size="sm" weight="semibold" color="text-white">
{detail.value}
</Text>
</Box>
</Box>
);
})}
</Box>
</Box>
);
}

View File

@@ -0,0 +1,24 @@
'use client';
import React from 'react';
import { Search } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Input } from '@/ui/Input';
interface DriverSearchBarProps {
query: string;
onChange: (query: string) => void;
}
export function DriverSearchBar({ query, onChange }: DriverSearchBarProps) {
return (
<Box position="relative" group>
<Input
value={query}
onChange={(e) => onChange(e.target.value)}
placeholder="Search drivers by name or nationality..."
icon={<Search size={20} />}
/>
</Box>
);
}

View File

@@ -0,0 +1,45 @@
'use client';
import React from 'react';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
interface StatItem {
label: string;
value: string | number;
subValue?: string;
color?: string;
}
interface DriverStatsPanelProps {
stats: StatItem[];
}
export function DriverStatsPanel({ stats }: DriverStatsPanelProps) {
return (
<Box display="grid" gridCols={{ base: 2, sm: 3, lg: 6 }} gap="px" overflow="hidden" rounded="xl" border borderColor="border-charcoal-outline" bg="bg-charcoal-outline">
{stats.map((stat, index) => (
<Box key={index} display="flex" flexDirection="col" gap={1} bg="bg-deep-charcoal" p={5} transition hoverBg="bg-deep-charcoal/80">
<Text size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="wider">
{stat.label}
</Text>
<Box display="flex" alignItems="baseline" gap={1.5}>
<Text
size="2xl"
weight="bold"
font="mono"
color={stat.color || 'text-white'}
>
{stat.value}
</Text>
{stat.subValue && (
<Text size="xs" weight="bold" color="text-gray-600" font="mono">
{stat.subValue}
</Text>
)}
</Box>
</Box>
))}
</Box>
);
}

View File

@@ -0,0 +1,45 @@
'use client';
import React from 'react';
import { TrendingUp } from 'lucide-react';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
interface DriverTableProps {
children: React.ReactNode;
}
export function DriverTable({ children }: DriverTableProps) {
return (
<Stack gap={4}>
<Stack direction="row" align="center" gap={3}>
<Box display="flex" h="10" w="10" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20">
<TrendingUp size={20} color="#198CFF" />
</Box>
<Box>
<Heading level={2}>Driver Rankings</Heading>
<Text size="xs" color="text-gray-500">Top performers by skill rating</Text>
</Box>
</Stack>
<Box overflow="hidden" rounded="xl" border borderColor="border-charcoal-outline" bg="bg-deep-charcoal/50">
<Box as="table" w="full" textAlign="left">
<Box as="thead">
<Box as="tr" borderBottom borderColor="border-charcoal-outline" bg="bg-deep-charcoal/80">
<Box as="th" px={6} py={4} fontSize="xs" color="text-gray-500" textAlign="center" width="60px">#</Box>
<Box as="th" px={6} py={4} fontSize="xs" color="text-gray-500">Driver</Box>
<Box as="th" px={6} py={4} fontSize="xs" color="text-gray-500" width="150px">Nationality</Box>
<Box as="th" px={6} py={4} fontSize="xs" color="text-gray-500" textAlign="right" width="100px">Rating</Box>
<Box as="th" px={6} py={4} fontSize="xs" color="text-gray-500" textAlign="right" width="80px">Wins</Box>
</Box>
</Box>
<Box as="tbody">
{children}
</Box>
</Box>
</Box>
</Stack>
);
}

View File

@@ -0,0 +1,86 @@
'use client';
import React from 'react';
import { RatingBadge } from '@/ui/RatingBadge';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Image } from '@/ui/Image';
interface DriverTableRowProps {
rank: number;
name: string;
avatarUrl?: string | null;
nationality: string;
rating: number;
wins: number;
onClick: () => void;
}
export function DriverTableRow({
rank,
name,
avatarUrl,
nationality,
rating,
wins,
onClick,
}: DriverTableRowProps) {
const defaultAvatar = 'https://cdn.gridpilot.com/avatars/default.png';
return (
<Box
as="tr"
onClick={onClick}
cursor="pointer"
transition
hoverBg="bg-primary-blue/5"
group
borderBottom
borderColor="border-charcoal-outline/50"
>
<Box as="td" px={6} py={4} textAlign="center">
<Text
size="sm"
weight="bold"
font="mono"
color={rank <= 3 ? 'text-warning-amber' : 'text-gray-500'}
>
{rank}
</Text>
</Box>
<Box as="td" px={6} py={4}>
<Stack direction="row" align="center" gap={3}>
<Box position="relative" h="8" w="8" overflow="hidden" rounded="full" border borderColor="border-charcoal-outline" bg="bg-deep-charcoal">
<Image
src={avatarUrl || defaultAvatar}
alt={name}
fill
objectFit="cover"
/>
</Box>
<Text
size="sm"
weight="semibold"
color="text-white"
groupHoverTextColor="text-primary-blue"
transition
>
{name}
</Text>
</Stack>
</Box>
<Box as="td" px={6} py={4}>
<Text size="xs" color="text-gray-400">{nationality}</Text>
</Box>
<Box as="td" px={6} py={4} textAlign="right">
<RatingBadge rating={rating} size="sm" />
</Box>
<Box as="td" px={6} py={4} textAlign="right">
<Text size="sm" weight="semibold" font="mono" color="text-performance-green">
{wins}
</Text>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,101 @@
'use client';
import React from 'react';
import { Users, Trophy } from 'lucide-react';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Button } from '@/ui/Button';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
interface DriverStat {
label: string;
value: string | number;
color?: string;
animate?: boolean;
}
interface DriversDirectoryHeaderProps {
totalDrivers: number;
activeDrivers: number;
totalWins: number;
totalRaces: number;
onViewLeaderboard: () => void;
}
export function DriversDirectoryHeader({
totalDrivers,
activeDrivers,
totalWins,
totalRaces,
onViewLeaderboard,
}: DriversDirectoryHeaderProps) {
const stats: DriverStat[] = [
{ label: 'drivers', value: totalDrivers, color: 'text-primary-blue' },
{ label: 'active', value: activeDrivers, color: 'text-performance-green', animate: true },
{ label: 'total wins', value: totalWins.toLocaleString(), color: 'text-warning-amber' },
{ label: 'races', value: totalRaces.toLocaleString(), color: 'text-neon-aqua' },
];
return (
<Box
as="header"
position="relative"
overflow="hidden"
rounded="2xl"
border
borderColor="border-charcoal-outline/50"
bg="bg-gradient-to-br from-iron-gray/80 via-deep-graphite to-iron-gray/60"
p={{ base: 8, lg: 10 }}
>
{/* Background Accents */}
<Box position="absolute" right="-24" top="-24" w="96" h="96" rounded="full" bg="bg-primary-blue/5" blur="3xl" />
<Box position="absolute" bottom="-16" left="-16" w="64" h="64" rounded="full" bg="bg-neon-aqua/5" blur="3xl" />
<Box position="relative" display="flex" flexDirection={{ base: 'col', lg: 'row' }} alignItems={{ lg: 'center' }} justifyContent="between" gap={8}>
<Box maxWidth="2xl">
<Stack direction="row" align="center" gap={3} mb={4}>
<Box display="flex" h="12" w="12" alignItems="center" justifyContent="center" rounded="xl" border borderColor="border-charcoal-outline" bg="bg-deep-charcoal" shadow="lg">
<Users size={24} color="#198CFF" />
</Box>
<Heading level={1}>Drivers</Heading>
</Stack>
<Text size="lg" color="text-gray-400" block leading="relaxed">
Meet the racers who make every lap count. From rookies to champions, track their journey and see who&apos;s dominating the grid.
</Text>
<Box display="flex" flexWrap="wrap" gap={6} mt={6}>
{stats.map((stat, index) => (
<Stack key={index} direction="row" align="center" gap={2}>
<Box
w="2"
h="2"
rounded="full"
bg={stat.color?.replace('text-', 'bg-') || 'bg-primary-blue'}
animate={stat.animate ? 'pulse' : 'none'}
/>
<Text size="sm" color="text-gray-400">
<Text as="span" weight="semibold" color="text-white">{stat.value}</Text> {stat.label}
</Text>
</Stack>
))}
</Box>
</Box>
<Stack gap={2}>
<Button
variant="primary"
onClick={onViewLeaderboard}
icon={<Trophy size={20} />}
>
View Leaderboard
</Button>
<Text size="xs" color="text-gray-500" align="center" block>
See full driver rankings
</Text>
</Stack>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,73 @@
'use client';
import React from 'react';
import { Shield } from 'lucide-react';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
interface SafetyRatingBadgeProps {
rating: number;
size?: 'sm' | 'md' | 'lg';
}
export function SafetyRatingBadge({ rating, size = 'md' }: SafetyRatingBadgeProps) {
const getColor = (r: number) => {
if (r >= 90) return 'text-performance-green';
if (r >= 70) return 'text-warning-amber';
return 'text-red-500';
};
const getBgColor = (r: number) => {
if (r >= 90) return 'bg-performance-green/10';
if (r >= 70) return 'bg-warning-amber/10';
return 'bg-red-500/10';
};
const getBorderColor = (r: number) => {
if (r >= 90) return 'border-performance-green/20';
if (r >= 70) return 'border-warning-amber/20';
return 'border-red-500/20';
};
const sizeProps = {
sm: { px: 2, py: 0.5, gap: 1 },
md: { px: 3, py: 1, gap: 1.5 },
lg: { px: 4, py: 2, gap: 2 },
};
const iconSizes = {
sm: 12,
md: 14,
lg: 16,
};
const iconColors = {
'text-performance-green': '#22C55E',
'text-warning-amber': '#FFBE4D',
'text-red-500': '#EF4444',
};
const colorClass = getColor(rating);
return (
<Box
display="inline-flex"
alignItems="center"
rounded="full"
border
bg={getBgColor(rating)}
borderColor={getBorderColor(rating)}
{...sizeProps[size]}
>
<Shield size={iconSizes[size]} color={iconColors[colorClass as keyof typeof iconColors]} />
<Text
size={size === 'lg' ? 'sm' : 'xs'}
weight="bold"
font="mono"
color={colorClass}
>
SR {rating.toFixed(0)}
</Text>
</Box>
);
}