website refactor
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
106
apps/website/components/drivers/DriverProfileHeader.tsx
Normal file
106
apps/website/components/drivers/DriverProfileHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
57
apps/website/components/drivers/DriverProfileTabs.tsx
Normal file
57
apps/website/components/drivers/DriverProfileTabs.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
76
apps/website/components/drivers/DriverRacingProfile.tsx
Normal file
76
apps/website/components/drivers/DriverRacingProfile.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
24
apps/website/components/drivers/DriverSearchBar.tsx
Normal file
24
apps/website/components/drivers/DriverSearchBar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
45
apps/website/components/drivers/DriverStatsPanel.tsx
Normal file
45
apps/website/components/drivers/DriverStatsPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
45
apps/website/components/drivers/DriverTable.tsx
Normal file
45
apps/website/components/drivers/DriverTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
86
apps/website/components/drivers/DriverTableRow.tsx
Normal file
86
apps/website/components/drivers/DriverTableRow.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
101
apps/website/components/drivers/DriversDirectoryHeader.tsx
Normal file
101
apps/website/components/drivers/DriversDirectoryHeader.tsx
Normal 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'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>
|
||||
);
|
||||
}
|
||||
73
apps/website/components/drivers/SafetyRatingBadge.tsx
Normal file
73
apps/website/components/drivers/SafetyRatingBadge.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user