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';
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"
/>
}
/>
);
}

View File

@@ -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>
);
}

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 { 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>
);
}

View File

@@ -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>

View File

@@ -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>
);

View File

@@ -1,18 +1,16 @@
'use client';
import { DriversDirectoryHeader } from '@/components/drivers/DriversDirectoryHeader';
import { DriverSearchBar } from '@/components/drivers/DriverSearchBar';
import { DriverTable } from '@/components/drivers/DriverTable';
import { DriverTableRow } from '@/components/drivers/DriverTableRow';
import { DriversViewData } from '@/lib/types/view-data/DriversViewData';
import { DriverCard } from '@/components/drivers/DriverCard';
import { DriverStatsHeader } from '@/components/drivers/DriverStatsHeader';
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 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 {
viewData: DriversViewData | null;
viewData: DriversViewData;
searchQuery: string;
onSearchChange: (query: string) => void;
filteredDrivers: DriversViewData['drivers'];
@@ -20,7 +18,7 @@ interface DriversTemplateProps {
onViewLeaderboard: () => void;
}
export function DriversTemplate({
export function DriversTemplate({
viewData,
searchQuery,
onSearchChange,
@@ -29,47 +27,59 @@ export function DriversTemplate({
onViewLeaderboard
}: DriversTemplateProps) {
return (
<Container size="lg" py={8}>
<Group direction="column" gap={10} fullWidth>
<DriversDirectoryHeader
totalDriversLabel={viewData?.totalDriversLabel || '0'}
activeDriversLabel={viewData?.activeCountLabel || '0'}
totalWinsLabel={viewData?.totalWinsLabel || '0'}
totalRacesLabel={viewData?.totalRacesLabel || '0'}
onViewLeaderboard={onViewLeaderboard}
<main>
<Box marginBottom={8}>
<PageHeader
icon={Users}
title="Drivers"
description="Global driver roster and statistics."
action={
<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>
{filteredDrivers.map((driver, index) => (
<DriverTableRow
<Box marginBottom={6} className="w-full">
<Input
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}
rank={index + 1}
name={driver.name}
avatarUrl={driver.avatarUrl}
nationality={driver.nationality}
rating={driver.rating}
ratingLabel={driver.ratingLabel}
winsLabel={String(driver.wins)}
onClick={() => onDriverClick(driver.id)}
driver={driver}
onClick={onDriverClick}
/>
))}
</DriverTable>
{filteredDrivers.length === 0 && (
<EmptyState
icon={Search}
title="No drivers found"
description={`No drivers found matching "${searchQuery}"`}
action={{
label: 'Clear search',
onClick: () => onSearchChange(''),
variant: 'secondary'
}}
/>
)}
</Group>
</Container>
</Box>
) : (
<EmptyState
title="No drivers found"
description={`No drivers match "${searchQuery}"`}
icon={Search}
/>
)}
</main>
);
}

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>
);
};