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';
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; id: string;
name: string; name: string;
avatarUrl?: string;
rating: number; rating: number;
skillLevel: 'beginner' | 'intermediate' | 'advanced' | 'pro'; ratingLabel: string;
nationality: string; nationality: string;
racesCompleted: number; racesCompleted: number;
wins: number; wins: number;
podiums: number; podiums: number;
rank: number; rank: number;
onClick?: () => void; };
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">
<Stack direction="row" align="center" gap={4} flexGrow={1}>
<RankBadge rank={rank} size="lg" />
<DriverIdentity <DriverIdentity
driver={driverViewModel} driver={{
href={routes.driver.detail(id)} id: driver.id,
meta={`${nationality}${racesCompleted} races`} name: driver.name,
size="md" avatarUrl: driver.avatarUrl || null,
}}
contextLabel={`Rank #${driver.rank}`}
meta={driver.nationality}
/> />
</Stack> }
actions={
<DriverStats <Badge variant="outline" size="sm">
rating={rating} {driver.ratingLabel}
wins={wins} </Badge>
podiums={podiums} }
winRate={winRate} stats={
<StatGrid
stats={stats.map(s => ({
label: s.label,
value: s.value,
intent: s.intent as any,
icon: s.icon
}))}
columns={3}
variant="box"
/>
}
/> />
</Stack>
</Card>
); );
} }

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">
<Box display="flex" alignItems="center" justifyContent="end" gap={2}>
<Text size="xs" variant="low" mono>{rating}</Text>
<RatingBadge rating={rating} ratingLabel={ratingLabel} size="sm" /> <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,
reliability, and championship progress across all sanctioned events.
</Text> </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'];
@@ -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
icon={Search}
title="No drivers found" title="No drivers found"
description={`No drivers found matching "${searchQuery}"`} description={`No drivers match "${searchQuery}"`}
action={{ icon={Search}
label: 'Clear search',
onClick: () => onSearchChange(''),
variant: 'secondary'
}}
/> />
)} )}
</Group> </main>
</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>
);
};