website refactor

This commit is contained in:
2026-01-20 12:16:58 +01:00
parent 3556db494f
commit a0d8d47e49
8 changed files with 276 additions and 187 deletions

View File

@@ -1,70 +1,66 @@
import { DriverIdentity } from '@/ui/DriverIdentity'; 'use client';
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';
export interface DriverCardProps { import { DriverIdentity } from '@/ui/DriverIdentity';
id: string; import { ProfileCard } from '@/ui/ProfileCard';
name: string; import { StatGrid } from '@/ui/StatGrid';
rating: number; import { Badge } from '@/ui/Badge';
skillLevel: 'beginner' | 'intermediate' | 'advanced' | 'pro'; import { Flag, Trophy, Medal } from 'lucide-react';
nationality: string;
racesCompleted: number; interface DriverCardProps {
wins: number; driver: {
podiums: number; id: string;
rank: number; name: string;
onClick?: () => void; 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) { export function DriverCard({ driver, onClick }: DriverCardProps) {
const { const stats = [
id, { label: 'Races', value: driver.racesCompleted, intent: 'low', icon: Flag },
name, { label: 'Wins', value: driver.wins, intent: 'primary', icon: Trophy },
rating, { label: 'Podiums', value: driver.podiums, intent: 'warning', icon: Medal },
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';
return ( return (
<Card <ProfileCard
onClick={onClick} onClick={() => onClick(driver.id)}
transition variant="muted"
> identity={
<Stack direction="row" align="center" justify="between"> <DriverIdentity
<Stack direction="row" align="center" gap={4} flexGrow={1}> driver={{
<RankBadge rank={rank} size="lg" /> id: driver.id,
name: driver.name,
<DriverIdentity avatarUrl: driver.avatarUrl || null,
driver={driverViewModel} }}
href={routes.driver.detail(id)} contextLabel={`Rank #${driver.rank}`}
meta={`${nationality}${racesCompleted} races`} meta={driver.nationality}
size="md"
/>
</Stack>
<DriverStats
rating={rating}
wins={wins}
podiums={podiums}
winRate={winRate}
/> />
</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"
/>
}
/>
); );
} }

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { Group } from '@/ui/Group'; import { Box } from '@/ui/Box';
import { Input } from '@/ui/Input'; import { Input } from '@/ui/Input';
import { Search } from 'lucide-react'; import { Search } from 'lucide-react';
@@ -11,13 +11,14 @@ interface DriverSearchBarProps {
export function DriverSearchBar({ query, onChange }: DriverSearchBarProps) { export function DriverSearchBar({ query, onChange }: DriverSearchBarProps) {
return ( return (
<Group fullWidth> <Box as="div" width="full" maxWidth="400px">
<Input <Input
value={query} value={query}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
placeholder="Search drivers by name or nationality..." placeholder="Search competitors..."
icon={<Search size={20} />} icon={<Search size={16} />}
size="sm"
/> />
</Group> </Box>
); );
} }

View 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' },
]}
/>
);
}

View File

@@ -2,11 +2,12 @@
import { Heading } from '@/ui/Heading'; import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text'; 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 { Table, TableHead, TableBody, TableRow, TableHeaderCell } from '@/ui/Table';
import { TrendingUp } from 'lucide-react'; import { TrendingUp } from 'lucide-react';
import { Card } from '@/ui/Card';
import { Icon } from '@/ui/Icon'; import { Icon } from '@/ui/Icon';
import { Surface } from '@/ui/Surface';
import { Stack } from '@/ui/Stack';
import React from 'react'; import React from 'react';
interface DriverTableProps { interface DriverTableProps {
@@ -15,31 +16,31 @@ interface DriverTableProps {
export function DriverTable({ children }: DriverTableProps) { export function DriverTable({ children }: DriverTableProps) {
return ( return (
<Group direction="column" gap={4} fullWidth> <Stack direction="col" gap="md">
<Group direction="row" align="center" gap={3}> <Box display="flex" alignItems="center" gap={3}>
<Card variant="dark"> <Surface variant="precision" rounded="md" padding="sm">
<Icon icon={TrendingUp} size={5} intent="primary" /> <Icon icon={TrendingUp} size={5} intent="primary" />
</Card> </Surface>
<Group direction="column"> <Stack direction="col" gap="none">
<Heading level={2}>Driver Rankings</Heading> <Heading level={2} weight="bold">Driver Rankings</Heading>
<Text size="xs" variant="low">Top performers by skill rating</Text> <Text size="xs" variant="low">Performance metrics based on sanctioned race results</Text>
</Group> </Stack>
</Group> </Box>
<Table> <Table>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableHeaderCell textAlign="center" w="60px">#</TableHeaderCell> <TableHeaderCell textAlign="center" w="60px">Rank</TableHeaderCell>
<TableHeaderCell>Driver</TableHeaderCell> <TableHeaderCell>Competitor</TableHeaderCell>
<TableHeaderCell w="150px">Nationality</TableHeaderCell> <TableHeaderCell w="180px">Nationality</TableHeaderCell>
<TableHeaderCell textAlign="right" w="100px">Rating</TableHeaderCell> <TableHeaderCell textAlign="right" w="120px">Skill Rating</TableHeaderCell>
<TableHeaderCell textAlign="right" w="80px">Wins</TableHeaderCell> <TableHeaderCell textAlign="right" w="100px">Victories</TableHeaderCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{children} {children}
</TableBody> </TableBody>
</Table> </Table>
</Group> </Stack>
); );
} }

View File

@@ -2,9 +2,10 @@
import { RatingBadge } from '@/components/drivers/RatingBadge'; import { RatingBadge } from '@/components/drivers/RatingBadge';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { Group } from '@/ui/Group'; import { Box } from '@/ui/Box';
import { TableRow, TableCell } from '@/ui/Table'; import { TableRow, TableCell } from '@/ui/Table';
import { Avatar } from '@/ui/Avatar'; import { Avatar } from '@/ui/Avatar';
import { CountryFlag } from '@/ui/CountryFlag';
interface DriverTableRowProps { interface DriverTableRowProps {
rank: number; rank: number;
@@ -28,19 +29,19 @@ export function DriverTableRow({
onClick, onClick,
}: DriverTableRowProps) { }: DriverTableRowProps) {
return ( return (
<TableRow onClick={onClick} clickable> <TableRow onClick={onClick}>
<TableCell textAlign="center"> <TableCell textAlign="center">
<Text <Text
size="sm" size="sm"
weight="bold" weight="bold"
font="mono" mono
variant={rank <= 3 ? 'warning' : 'low'} variant={rank <= 3 ? 'warning' : 'low'}
> >
{rank} {rank.toString().padStart(2, '0')}
</Text> </Text>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Group direction="row" align="center" gap={3}> <Box display="flex" alignItems="center" gap={3}>
<Avatar <Avatar
src={avatarUrl || undefined} src={avatarUrl || undefined}
alt={name} alt={name}
@@ -53,16 +54,22 @@ export function DriverTableRow({
> >
{name} {name}
</Text> </Text>
</Group> </Box>
</TableCell> </TableCell>
<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>
<TableCell textAlign="right"> <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>
<TableCell textAlign="right"> <TableCell textAlign="right">
<Text size="sm" weight="semibold" font="mono" variant="success"> <Text size="sm" weight="bold" mono variant="success">
{winsLabel} {winsLabel}
</Text> </Text>
</TableCell> </TableCell>

View File

@@ -2,19 +2,15 @@
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading'; import { Heading } from '@/ui/Heading';
import { Group } from '@/ui/Group';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { Section } from '@/ui/Section'; import { Section } from '@/ui/Section';
import { Container } from '@/ui/Container'; import { Container } from '@/ui/Container';
import { Card } from '@/ui/Card';
import { Icon } from '@/ui/Icon'; import { Icon } from '@/ui/Icon';
import { Trophy, Users } from 'lucide-react'; import { MetricCard } from '@/ui/MetricCard';
import { Trophy, Users, Zap, Flag } from 'lucide-react';
interface DriverStat { import { Grid } from '@/ui/Grid';
label: string; import { Box } from '@/ui/Box';
value: string | number; import { Stack } from '@/ui/Stack';
intent?: 'primary' | 'success' | 'warning' | 'telemetry';
}
interface DriversDirectoryHeaderProps { interface DriversDirectoryHeaderProps {
totalDriversLabel: string; totalDriversLabel: string;
@@ -31,53 +27,68 @@ export function DriversDirectoryHeader({
totalRacesLabel, totalRacesLabel,
onViewLeaderboard, onViewLeaderboard,
}: DriversDirectoryHeaderProps) { }: 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 ( return (
<Section variant="dark" padding="md"> <Section variant="dark" padding="lg">
<Container> <Container>
<Group direction="row" align="center" justify="between" gap={8} fullWidth> <Stack direction="col" gap="lg">
<Group direction="column" gap={6}> <Stack direction="row" align="center" justify="between">
<Group direction="row" align="center" gap={3}> <Stack direction="col" gap="xs">
<Card variant="dark"> <Stack direction="row" align="center" gap="md">
<Icon icon={Users} size={6} intent="primary" /> <Icon icon={Users} size={6} intent="primary" />
</Card> <Heading level={1} weight="bold">
<Heading level={1}>Drivers</Heading> Driver Directory
</Group> </Heading>
</Stack>
<Text size="lg" variant="low"> <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. The official registry of GridPilot competitors. Tracking performance,
</Text> 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 <Button
variant="primary" variant="primary"
onClick={onViewLeaderboard} onClick={onViewLeaderboard}
icon={<Icon icon={Trophy} size={5} />} icon={<Icon icon={Trophy} size={4} />}
> >
View Leaderboard Global Rankings
</Button> </Button>
<Text size="xs" variant="low" align="center"> </Stack>
See full driver rankings
</Text> <Grid cols={4} gap="md">
</Group> <Box>
</Group> <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> </Container>
</Section> </Section>
); );

View File

@@ -1,18 +1,16 @@
'use client'; 'use client';
import { DriversDirectoryHeader } from '@/components/drivers/DriversDirectoryHeader'; import { DriversViewData } from '@/lib/types/view-data/DriversViewData';
import { DriverSearchBar } from '@/components/drivers/DriverSearchBar'; import { DriverCard } from '@/components/drivers/DriverCard';
import { DriverTable } from '@/components/drivers/DriverTable'; import { DriverStatsHeader } from '@/components/drivers/DriverStatsHeader';
import { DriverTableRow } from '@/components/drivers/DriverTableRow'; import { PageHeader } from '@/ui/PageHeader';
import { Input } from '@/ui/Input';
import { Box } from '@/ui/Box';
import { Search, Users } from 'lucide-react';
import { EmptyState } from '@/ui/EmptyState'; import { EmptyState } from '@/ui/EmptyState';
import type { DriversViewData } from '@/lib/types/view-data/DriversViewData';
import { Container } from '@/ui/Container';
import { Group } from '@/ui/Group';
import { Search } from 'lucide-react';
import React from 'react';
interface DriversTemplateProps { interface DriversTemplateProps {
viewData: DriversViewData | null; viewData: DriversViewData;
searchQuery: string; searchQuery: string;
onSearchChange: (query: string) => void; onSearchChange: (query: string) => void;
filteredDrivers: DriversViewData['drivers']; filteredDrivers: DriversViewData['drivers'];
@@ -20,7 +18,7 @@ interface DriversTemplateProps {
onViewLeaderboard: () => void; onViewLeaderboard: () => void;
} }
export function DriversTemplate({ export function DriversTemplate({
viewData, viewData,
searchQuery, searchQuery,
onSearchChange, onSearchChange,
@@ -29,47 +27,59 @@ export function DriversTemplate({
onViewLeaderboard onViewLeaderboard
}: DriversTemplateProps) { }: DriversTemplateProps) {
return ( return (
<Container size="lg" py={8}> <main>
<Group direction="column" gap={10} fullWidth> <Box marginBottom={8}>
<DriversDirectoryHeader <PageHeader
totalDriversLabel={viewData?.totalDriversLabel || '0'} icon={Users}
activeDriversLabel={viewData?.activeCountLabel || '0'} title="Drivers"
totalWinsLabel={viewData?.totalWinsLabel || '0'} description="Global driver roster and statistics."
totalRacesLabel={viewData?.totalRacesLabel || '0'} action={
onViewLeaderboard={onViewLeaderboard} <Box
as="button"
onClick={onViewLeaderboard}
className="px-4 py-2 rounded-md bg-[var(--ui-color-bg-surface)] border border-[var(--ui-color-border-default)] text-sm font-medium hover:bg-[var(--ui-color-bg-surface-muted)] transition-colors"
>
Leaderboard
</Box>
}
/> />
</Box>
<DriverSearchBar query={searchQuery} onChange={onSearchChange} /> <Box marginBottom={8}>
<DriverStatsHeader
totalDrivers={viewData.totalDriversLabel}
activeDrivers={viewData.activeCountLabel}
totalRaces={viewData.totalRacesLabel}
/>
</Box>
<DriverTable> <Box marginBottom={6} className="w-full">
{filteredDrivers.map((driver, index) => ( <Input
<DriverTableRow placeholder="Search drivers by name or nationality..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
icon={Search}
variant="search"
/>
</Box>
{filteredDrivers.length > 0 ? (
<Box display="grid" gap={4} className="grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{filteredDrivers.map(driver => (
<DriverCard
key={driver.id} key={driver.id}
rank={index + 1} driver={driver}
name={driver.name} onClick={onDriverClick}
avatarUrl={driver.avatarUrl}
nationality={driver.nationality}
rating={driver.rating}
ratingLabel={driver.ratingLabel}
winsLabel={String(driver.wins)}
onClick={() => onDriverClick(driver.id)}
/> />
))} ))}
</DriverTable> </Box>
) : (
{filteredDrivers.length === 0 && ( <EmptyState
<EmptyState title="No drivers found"
icon={Search} description={`No drivers match "${searchQuery}"`}
title="No drivers found" icon={Search}
description={`No drivers found matching "${searchQuery}"`} />
action={{ )}
label: 'Clear search', </main>
onClick: () => onSearchChange(''),
variant: 'secondary'
}}
/>
)}
</Group>
</Container>
); );
} }

View File

@@ -0,0 +1,38 @@
import { ReactNode } from 'react';
import { Card } from './Card';
import { Box } from './Box';
export interface ProfileCardProps {
identity: ReactNode;
stats?: ReactNode;
actions?: ReactNode;
variant?: 'default' | 'muted' | 'outline' | 'glass';
onClick?: () => void;
}
export const ProfileCard = ({ identity, stats, actions, variant = 'default', onClick }: ProfileCardProps) => {
return (
<Card
variant={variant}
padding="md"
onClick={onClick}
className="h-full flex flex-col gap-6 transition-all duration-200 hover:border-[var(--ui-color-border-bright)]"
>
<Box display="flex" justifyContent="between" alignItems="start" gap={4}>
<Box flex={1} minWidth="0">
{identity}
</Box>
{actions && (
<Box flexShrink={0}>
{actions}
</Box>
)}
</Box>
{stats && (
<Box marginTop="auto" paddingTop={4} borderTop="1px solid var(--ui-color-border-muted)">
{stats}
</Box>
)}
</Card>
);
};