website refactor
This commit is contained in:
@@ -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"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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 { 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
The official registry of GridPilot competitors. Tracking performance,
|
||||||
|
reliability, and championship progress across all sanctioned events.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
38
apps/website/ui/ProfileCard.tsx
Normal file
38
apps/website/ui/ProfileCard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user