website refactor
This commit is contained in:
@@ -1,70 +1,66 @@
|
||||
import { DriverIdentity } from '@/ui/DriverIdentity';
|
||||
import { DriverStats } from '@/components/drivers/DriverStats';
|
||||
import { RankBadge } from '@/components/leaderboards/RankBadge';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { DriverViewModel } from '@/lib/view-models/DriverViewModel';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
'use client';
|
||||
|
||||
export interface DriverCardProps {
|
||||
id: string;
|
||||
name: string;
|
||||
rating: number;
|
||||
skillLevel: 'beginner' | 'intermediate' | 'advanced' | 'pro';
|
||||
nationality: string;
|
||||
racesCompleted: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
rank: number;
|
||||
onClick?: () => void;
|
||||
import { DriverIdentity } from '@/ui/DriverIdentity';
|
||||
import { ProfileCard } from '@/ui/ProfileCard';
|
||||
import { StatGrid } from '@/ui/StatGrid';
|
||||
import { Badge } from '@/ui/Badge';
|
||||
import { Flag, Trophy, Medal } from 'lucide-react';
|
||||
|
||||
interface DriverCardProps {
|
||||
driver: {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl?: string;
|
||||
rating: number;
|
||||
ratingLabel: string;
|
||||
nationality: string;
|
||||
racesCompleted: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
rank: number;
|
||||
};
|
||||
onClick: (id: string) => void;
|
||||
}
|
||||
|
||||
export function DriverCard(props: DriverCardProps) {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
rating,
|
||||
nationality,
|
||||
racesCompleted,
|
||||
wins,
|
||||
podiums,
|
||||
rank,
|
||||
onClick,
|
||||
} = props;
|
||||
|
||||
// Create a proper DriverViewModel instance
|
||||
const driverViewModel = new DriverViewModel({
|
||||
id,
|
||||
name,
|
||||
avatarUrl: null,
|
||||
});
|
||||
|
||||
const winRate = racesCompleted > 0 ? ((wins / racesCompleted) * 100).toFixed(0) : '0';
|
||||
export function DriverCard({ driver, onClick }: DriverCardProps) {
|
||||
const stats = [
|
||||
{ label: 'Races', value: driver.racesCompleted, intent: 'low', icon: Flag },
|
||||
{ label: 'Wins', value: driver.wins, intent: 'primary', icon: Trophy },
|
||||
{ label: 'Podiums', value: driver.podiums, intent: 'warning', icon: Medal },
|
||||
];
|
||||
|
||||
return (
|
||||
<Card
|
||||
onClick={onClick}
|
||||
transition
|
||||
>
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Stack direction="row" align="center" gap={4} flexGrow={1}>
|
||||
<RankBadge rank={rank} size="lg" />
|
||||
|
||||
<DriverIdentity
|
||||
driver={driverViewModel}
|
||||
href={routes.driver.detail(id)}
|
||||
meta={`${nationality} • ${racesCompleted} races`}
|
||||
size="md"
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<DriverStats
|
||||
rating={rating}
|
||||
wins={wins}
|
||||
podiums={podiums}
|
||||
winRate={winRate}
|
||||
<ProfileCard
|
||||
onClick={() => onClick(driver.id)}
|
||||
variant="muted"
|
||||
identity={
|
||||
<DriverIdentity
|
||||
driver={{
|
||||
id: driver.id,
|
||||
name: driver.name,
|
||||
avatarUrl: driver.avatarUrl || null,
|
||||
}}
|
||||
contextLabel={`Rank #${driver.rank}`}
|
||||
meta={driver.nationality}
|
||||
/>
|
||||
</Stack>
|
||||
</Card>
|
||||
}
|
||||
actions={
|
||||
<Badge variant="outline" size="sm">
|
||||
{driver.ratingLabel}
|
||||
</Badge>
|
||||
}
|
||||
stats={
|
||||
<StatGrid
|
||||
stats={stats.map(s => ({
|
||||
label: s.label,
|
||||
value: s.value,
|
||||
intent: s.intent as any,
|
||||
icon: s.icon
|
||||
}))}
|
||||
columns={3}
|
||||
variant="box"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Group } from '@/ui/Group';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Input } from '@/ui/Input';
|
||||
import { Search } from 'lucide-react';
|
||||
|
||||
@@ -11,13 +11,14 @@ interface DriverSearchBarProps {
|
||||
|
||||
export function DriverSearchBar({ query, onChange }: DriverSearchBarProps) {
|
||||
return (
|
||||
<Group fullWidth>
|
||||
<Box as="div" width="full" maxWidth="400px">
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder="Search drivers by name or nationality..."
|
||||
icon={<Search size={20} />}
|
||||
placeholder="Search competitors..."
|
||||
icon={<Search size={16} />}
|
||||
size="sm"
|
||||
/>
|
||||
</Group>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
25
apps/website/components/drivers/DriverStatsHeader.tsx
Normal file
25
apps/website/components/drivers/DriverStatsHeader.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { StatGrid } from '@/ui/StatGrid';
|
||||
import { Users, Trophy, Activity } from 'lucide-react';
|
||||
|
||||
interface DriverStatsHeaderProps {
|
||||
totalDrivers: string;
|
||||
activeDrivers: string;
|
||||
totalRaces: string;
|
||||
}
|
||||
|
||||
export function DriverStatsHeader({ totalDrivers, activeDrivers, totalRaces }: DriverStatsHeaderProps) {
|
||||
return (
|
||||
<StatGrid
|
||||
columns={{ base: 1, md: 3 }}
|
||||
variant="card"
|
||||
cardVariant="muted"
|
||||
stats={[
|
||||
{ label: 'Total Drivers', value: totalDrivers, icon: Users, intent: 'primary' },
|
||||
{ label: 'Active Drivers', value: activeDrivers, icon: Activity, intent: 'success' },
|
||||
{ label: 'Total Races', value: totalRaces, icon: Trophy, intent: 'warning' },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Group } from '@/ui/Group';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Table, TableHead, TableBody, TableRow, TableHeaderCell } from '@/ui/Table';
|
||||
import { TrendingUp } from 'lucide-react';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import React from 'react';
|
||||
|
||||
interface DriverTableProps {
|
||||
@@ -15,31 +16,31 @@ interface DriverTableProps {
|
||||
|
||||
export function DriverTable({ children }: DriverTableProps) {
|
||||
return (
|
||||
<Group direction="column" gap={4} fullWidth>
|
||||
<Group direction="row" align="center" gap={3}>
|
||||
<Card variant="dark">
|
||||
<Stack direction="col" gap="md">
|
||||
<Box display="flex" alignItems="center" gap={3}>
|
||||
<Surface variant="precision" rounded="md" padding="sm">
|
||||
<Icon icon={TrendingUp} size={5} intent="primary" />
|
||||
</Card>
|
||||
<Group direction="column">
|
||||
<Heading level={2}>Driver Rankings</Heading>
|
||||
<Text size="xs" variant="low">Top performers by skill rating</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
</Surface>
|
||||
<Stack direction="col" gap="none">
|
||||
<Heading level={2} weight="bold">Driver Rankings</Heading>
|
||||
<Text size="xs" variant="low">Performance metrics based on sanctioned race results</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell textAlign="center" w="60px">#</TableHeaderCell>
|
||||
<TableHeaderCell>Driver</TableHeaderCell>
|
||||
<TableHeaderCell w="150px">Nationality</TableHeaderCell>
|
||||
<TableHeaderCell textAlign="right" w="100px">Rating</TableHeaderCell>
|
||||
<TableHeaderCell textAlign="right" w="80px">Wins</TableHeaderCell>
|
||||
<TableHeaderCell textAlign="center" w="60px">Rank</TableHeaderCell>
|
||||
<TableHeaderCell>Competitor</TableHeaderCell>
|
||||
<TableHeaderCell w="180px">Nationality</TableHeaderCell>
|
||||
<TableHeaderCell textAlign="right" w="120px">Skill Rating</TableHeaderCell>
|
||||
<TableHeaderCell textAlign="right" w="100px">Victories</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{children}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
import { RatingBadge } from '@/components/drivers/RatingBadge';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Group } from '@/ui/Group';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { TableRow, TableCell } from '@/ui/Table';
|
||||
import { Avatar } from '@/ui/Avatar';
|
||||
import { CountryFlag } from '@/ui/CountryFlag';
|
||||
|
||||
interface DriverTableRowProps {
|
||||
rank: number;
|
||||
@@ -28,19 +29,19 @@ export function DriverTableRow({
|
||||
onClick,
|
||||
}: DriverTableRowProps) {
|
||||
return (
|
||||
<TableRow onClick={onClick} clickable>
|
||||
<TableRow onClick={onClick}>
|
||||
<TableCell textAlign="center">
|
||||
<Text
|
||||
size="sm"
|
||||
weight="bold"
|
||||
font="mono"
|
||||
mono
|
||||
variant={rank <= 3 ? 'warning' : 'low'}
|
||||
>
|
||||
{rank}
|
||||
{rank.toString().padStart(2, '0')}
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Group direction="row" align="center" gap={3}>
|
||||
<Box display="flex" alignItems="center" gap={3}>
|
||||
<Avatar
|
||||
src={avatarUrl || undefined}
|
||||
alt={name}
|
||||
@@ -53,16 +54,22 @@ export function DriverTableRow({
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
</Group>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text size="xs" variant="low">{nationality}</Text>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<CountryFlag countryCode={nationality} size="sm" />
|
||||
<Text size="xs" variant="low" uppercase>{nationality}</Text>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell textAlign="right">
|
||||
<RatingBadge rating={rating} ratingLabel={ratingLabel} size="sm" />
|
||||
<Box display="flex" alignItems="center" justifyContent="end" gap={2}>
|
||||
<Text size="xs" variant="low" mono>{rating}</Text>
|
||||
<RatingBadge rating={rating} ratingLabel={ratingLabel} size="sm" />
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell textAlign="right">
|
||||
<Text size="sm" weight="semibold" font="mono" variant="success">
|
||||
<Text size="sm" weight="bold" mono variant="success">
|
||||
{winsLabel}
|
||||
</Text>
|
||||
</TableCell>
|
||||
|
||||
@@ -2,19 +2,15 @@
|
||||
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Group } from '@/ui/Group';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Section } from '@/ui/Section';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Trophy, Users } from 'lucide-react';
|
||||
|
||||
interface DriverStat {
|
||||
label: string;
|
||||
value: string | number;
|
||||
intent?: 'primary' | 'success' | 'warning' | 'telemetry';
|
||||
}
|
||||
import { MetricCard } from '@/ui/MetricCard';
|
||||
import { Trophy, Users, Zap, Flag } from 'lucide-react';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
|
||||
interface DriversDirectoryHeaderProps {
|
||||
totalDriversLabel: string;
|
||||
@@ -31,53 +27,68 @@ export function DriversDirectoryHeader({
|
||||
totalRacesLabel,
|
||||
onViewLeaderboard,
|
||||
}: DriversDirectoryHeaderProps) {
|
||||
const stats: DriverStat[] = [
|
||||
{ label: 'drivers', value: totalDriversLabel, intent: 'primary' },
|
||||
{ label: 'active', value: activeDriversLabel, intent: 'success' },
|
||||
{ label: 'total wins', value: totalWinsLabel, intent: 'warning' },
|
||||
{ label: 'races', value: totalRacesLabel, intent: 'telemetry' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Section variant="dark" padding="md">
|
||||
<Section variant="dark" padding="lg">
|
||||
<Container>
|
||||
<Group direction="row" align="center" justify="between" gap={8} fullWidth>
|
||||
<Group direction="column" gap={6}>
|
||||
<Group direction="row" align="center" gap={3}>
|
||||
<Card variant="dark">
|
||||
<Stack direction="col" gap="lg">
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Stack direction="col" gap="xs">
|
||||
<Stack direction="row" align="center" gap="md">
|
||||
<Icon icon={Users} size={6} intent="primary" />
|
||||
</Card>
|
||||
<Heading level={1}>Drivers</Heading>
|
||||
</Group>
|
||||
|
||||
<Text size="lg" variant="low">
|
||||
Meet the racers who make every lap count. From rookies to champions, track their journey and see who's dominating the grid.
|
||||
</Text>
|
||||
<Heading level={1} weight="bold">
|
||||
Driver Directory
|
||||
</Heading>
|
||||
</Stack>
|
||||
<Text size="lg" variant="low">
|
||||
The official registry of GridPilot competitors. Tracking performance,
|
||||
reliability, and championship progress across all sanctioned events.
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Group direction="row" gap={6} wrap>
|
||||
{stats.map((stat, index) => (
|
||||
<Group key={index} direction="row" align="center" gap={2}>
|
||||
<Text size="sm" variant="low">
|
||||
<Text as="span" weight="semibold" variant="high">{stat.value}</Text> {stat.label}
|
||||
</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Group direction="column" gap={2} align="center">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onViewLeaderboard}
|
||||
icon={<Icon icon={Trophy} size={5} />}
|
||||
icon={<Icon icon={Trophy} size={4} />}
|
||||
>
|
||||
View Leaderboard
|
||||
Global Rankings
|
||||
</Button>
|
||||
<Text size="xs" variant="low" align="center">
|
||||
See full driver rankings
|
||||
</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
<Grid cols={4} gap="md">
|
||||
<Box>
|
||||
<MetricCard
|
||||
label="Total Registered"
|
||||
value={totalDriversLabel}
|
||||
icon={Users}
|
||||
intent="primary"
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<MetricCard
|
||||
label="Active Competitors"
|
||||
value={activeDriversLabel}
|
||||
icon={Zap}
|
||||
intent="success"
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<MetricCard
|
||||
label="Career Victories"
|
||||
value={totalWinsLabel}
|
||||
icon={Trophy}
|
||||
intent="warning"
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<MetricCard
|
||||
label="Total Race Starts"
|
||||
value={totalRacesLabel}
|
||||
icon={Flag}
|
||||
intent="telemetry"
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user