website refactor

This commit is contained in:
2026-01-20 21:35:50 +01:00
parent 06207bf835
commit 51288234f4
42 changed files with 892 additions and 449 deletions

View File

@@ -0,0 +1,19 @@
'use client';
import React, { ReactNode } from 'react';
import { Grid } from '@/ui/Grid';
interface DriverGridProps {
children: ReactNode;
}
/**
* DriverGrid - A semantic layout for displaying driver cards.
*/
export function DriverGrid({ children }: DriverGridProps) {
return (
<Grid cols={{ base: 1, md: 2, lg: 3, xl: 4 }} gap={4}>
{children}
</Grid>
);
}

View File

@@ -1,12 +1,6 @@
'use client';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Button } from '@/ui/Button';
import { Section } from '@/ui/Section';
import { ButtonGroup } from '@/ui/ButtonGroup';
import { Stack } from '@/ui/Stack';
import { Box } from '@/ui/Box';
import { LandingHero } from '@/ui/LandingHero';
/**
* Hero - Refined with Dieter Rams principles.
@@ -15,51 +9,12 @@ import { Box } from '@/ui/Box';
*/
export function Hero() {
return (
<Section variant="default" py={32}>
<Box maxWidth="54rem">
<Box marginBottom={24}>
<Text
variant="primary"
weight="bold"
uppercase
size="xs"
leading="none"
block
letterSpacing="0.2em"
marginBottom={10}
>
Sim Racing Infrastructure
</Text>
<Heading level={1} weight="bold" size="4xl">
Professional League Management.<br />
Engineered for Control.
</Heading>
</Box>
<Box marginBottom={24}>
<Text size="xl" variant="med" leading="relaxed" block maxWidth="42rem">
GridPilot eliminates the administrative overhead of running iRacing leagues.
No spreadsheets. No manual points. No protest chaos.
Just pure competition, structured for growth.
</Text>
</Box>
<ButtonGroup gap={10}>
<Button
variant="primary"
size="lg"
>
Create Your League
</Button>
<Button
variant="secondary"
size="lg"
>
View Demo
</Button>
</ButtonGroup>
</Box>
</Section>
<LandingHero
subtitle="Sim Racing Infrastructure"
title="Professional League Management. Engineered for Control."
description="GridPilot eliminates the administrative overhead of running iRacing leagues. No spreadsheets. No manual points. No protest chaos. Just pure competition, structured for growth."
primaryAction={{ label: 'Create Your League', href: '#' }}
secondaryAction={{ label: 'View Demo', href: '#' }}
/>
);
}

View File

@@ -12,11 +12,22 @@ import { Box } from '@/ui/Box';
import { StatusBadge } from '@/ui/StatusBadge';
import { Trophy, Globe, Settings2, Palette, ShieldCheck, BarChart3 } from 'lucide-react';
interface LeagueIdentityPreviewProps {
league?: {
id: string;
name: string;
description: string;
};
}
/**
* LeagueIdentityPreview - Radically redesigned for "Modern Precision" and "Dieter Rams" style.
* Focuses on the professional identity and deep customization options for admins.
*/
export function LeagueIdentityPreview() {
export function LeagueIdentityPreview({ league }: LeagueIdentityPreviewProps) {
const leagueName = league?.name || 'Apex Racing League';
const subdomain = league ? `${league.name.toLowerCase().replace(/\s+/g, '')}.gridpilot.racing` : 'apex.gridpilot.racing';
return (
<Section variant="default" py={32}>
<Box>
@@ -48,7 +59,7 @@ export function LeagueIdentityPreview() {
<Globe size={20} className="text-[var(--ui-color-text-low)]" />
<Stack gap={1}>
<Text weight="bold">Custom Subdomains</Text>
<Text size="xs" variant="low">yourleague.gridpilot.racing</Text>
<Text size="xs" variant="low">{subdomain}</Text>
</Stack>
</Group>
</Panel>
@@ -77,8 +88,8 @@ export function LeagueIdentityPreview() {
<Trophy size={20} className="text-[var(--ui-color-text-low)]" />
</Box>
<Stack gap={0}>
<Text weight="bold" size="sm">Apex Racing League</Text>
<Text size="xs" variant="low">apex.gridpilot.racing</Text>
<Text weight="bold" size="sm">{leagueName}</Text>
<Text size="xs" variant="low">{subdomain}</Text>
</Stack>
</Group>
</Panel>

View File

@@ -13,11 +13,30 @@ import { Grid } from '@/ui/Grid';
import { Box } from '@/ui/Box';
import { Gavel, Clock, User, MessageSquare } from 'lucide-react';
interface StewardingPreviewProps {
race?: {
id: string;
track: string;
car: string;
formattedDate: string;
};
team?: {
id: string;
name: string;
description: string;
};
}
/**
* StewardingPreview - Refined for "Modern Precision" and "Dieter Rams" style.
* Thorough down to the last detail.
*/
export function StewardingPreview() {
export function StewardingPreview({ race, team }: StewardingPreviewProps) {
const incidentId = race ? `${race.id.slice(0, 3).toUpperCase()}-WG` : '402-WG';
const trackName = race?.track || 'Watkins Glen - Cup';
const carName = race?.car || 'Porsche 911 GT3 R';
const teamName = team?.name || 'Alex Miller';
return (
<Section variant="muted" py={32}>
<Box>
@@ -36,9 +55,9 @@ export function StewardingPreview() {
<Group gap={2}>
<Text variant="low" size="xs" uppercase weight="bold" letterSpacing="0.1em">Incident Report</Text>
<Text variant="low" size="xs"></Text>
<Text variant="low" size="xs" uppercase weight="bold" letterSpacing="0.1em">ID: 402-WG</Text>
<Text variant="low" size="xs" uppercase weight="bold" letterSpacing="0.1em">ID: {incidentId}</Text>
</Group>
<Heading level={3} weight="bold">Turn 1 Contact: Miller vs Chen</Heading>
<Heading level={3} weight="bold">Turn 1 Contact: {teamName} vs David Chen</Heading>
</Stack>
<StatusBadge variant="warning">UNDER REVIEW</StatusBadge>
</Group>
@@ -50,8 +69,8 @@ export function StewardingPreview() {
<User size={14} className="text-[var(--ui-color-intent-primary)]" />
<Text size="xs" uppercase weight="bold" variant="low">Protestor</Text>
</Group>
<Text weight="bold">Alex Miller</Text>
<Text size="sm" variant="low">#42 - Porsche 911 GT3 R</Text>
<Text weight="bold">{teamName}</Text>
<Text size="sm" variant="low">#42 - {carName}</Text>
</Stack>
</Panel>
<Panel variant="bordered" padding="md">
@@ -71,7 +90,7 @@ export function StewardingPreview() {
<Text size="xs" uppercase weight="bold" variant="low">Session Info</Text>
</Group>
<Text weight="bold">Lap 1, 00:42.150</Text>
<Text size="sm" variant="low">Watkins Glen - Cup</Text>
<Text size="sm" variant="low">{trackName}</Text>
</Stack>
</Panel>
</Grid>

View File

@@ -38,7 +38,7 @@ export function TelemetryStrip() {
];
return (
<Section variant="default" py={16}>
<Section variant="default" padding="md">
<StatsStrip stats={stats} />
</Section>
);

View File

@@ -2,6 +2,8 @@
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Input } from '@/ui/Input';
import { Button } from '@/ui/Button';
import { Search, Command, ArrowRight, X } from 'lucide-react';
import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
@@ -46,71 +48,81 @@ export function CommandModal({ isOpen, onClose }: CommandModalProps) {
if (!isOpen) return null;
return createPortal(
<div className="fixed inset-0 z-[9999] flex items-start justify-center pt-[20vh] px-4">
<Box className="fixed inset-0 z-[9999] flex items-start justify-center pt-[20vh] px-4">
{/* Backdrop */}
<div
<Box
className="absolute inset-0 bg-black/60 backdrop-blur-sm transition-opacity"
onClick={onClose}
/>
{/* Modal Content */}
<div className="relative w-full max-w-lg bg-surface-charcoal border border-outline-steel rounded-xl shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-200">
<div className="flex items-center border-b border-outline-steel px-4 py-3 gap-3">
<Box className="relative w-full max-w-lg bg-surface-charcoal border border-outline-steel rounded-xl shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-200">
<Box display="flex" alignItems="center" className="border-b border-outline-steel px-4 py-3 gap-3">
<Search className="text-text-low" size={18} />
<input
<Input
autoFocus
type="text"
variant="ghost"
placeholder="Type a command or search..."
className="flex-1 bg-transparent border-none outline-none text-text-high placeholder:text-text-low/50 text-base h-6"
className="flex-1 text-base h-6"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<button onClick={onClose} className="text-text-low hover:text-text-high transition-colors">
<span className="sr-only">Close</span>
<kbd className="hidden sm:inline-block px-1.5 py-0.5 text-[10px] font-mono bg-white/5 rounded border border-white/5">ESC</kbd>
</button>
</div>
<Button variant="ghost" size="sm" onClick={onClose} className="text-text-low hover:text-text-high transition-colors">
<Text as="span" className="sr-only">Close</Text>
<Text as="kbd" className="hidden sm:inline-block px-1.5 py-0.5 text-[10px] font-mono bg-white/5 rounded border border-white/5">ESC</Text>
</Button>
</Box>
<div className="p-2">
<Box p={2}>
{results.length > 0 ? (
<div className="flex flex-col gap-1">
<div className="px-2 py-1.5 text-[10px] font-mono uppercase tracking-wider text-text-low/50 font-bold">
Suggestions
</div>
<Box display="flex" flexDirection="col" gap={1}>
<Box paddingX={2} paddingY={1.5}>
<Text size="xs" weight="bold" uppercase mono className="tracking-wider text-text-low/50">
Suggestions
</Text>
</Box>
{results.map((result, i) => (
<button
<Button
key={i}
variant="ghost"
fullWidth
className="flex items-center justify-between px-3 py-2.5 rounded-lg hover:bg-white/5 text-left group transition-colors"
onClick={onClose}
>
<span className="text-sm text-text-med group-hover:text-text-high font-medium">
<Text size="sm" weight="medium" className="text-text-med group-hover:text-text-high">
{result.label}
</span>
<div className="flex items-center gap-2">
<span className="text-[10px] font-mono text-text-low bg-white/5 px-1.5 py-0.5 rounded border border-white/5">
</Text>
<Box display="flex" alignItems="center" gap={2}>
<Text as="span" size="xs" mono className="text-text-low bg-white/5 px-1.5 py-0.5 rounded border border-white/5">
{result.shortcut}
</span>
</Text>
<ArrowRight size={14} className="text-text-low opacity-0 group-hover:opacity-100 transition-opacity -translate-x-2 group-hover:translate-x-0" />
</div>
</button>
</Box>
</Button>
))}
</div>
</Box>
) : (
<div className="px-4 py-8 text-center text-text-low text-sm">
No results found.
</div>
<Box paddingX={4} paddingY={8} className="text-center">
<Text size="sm" variant="low">
No results found.
</Text>
</Box>
)}
</div>
</Box>
<div className="px-4 py-2 bg-white/2 border-t border-white/5 flex items-center justify-between text-[10px] text-text-low">
<div className="flex gap-3">
<span><strong className="text-text-med"></strong> to navigate</span>
<span><strong className="text-text-med"></strong> to select</span>
</div>
<span>GridPilot Command</span>
</div>
</div>
</div>,
<Box paddingX={4} paddingY={2} display="flex" alignItems="center" justifyContent="space-between" className="bg-white/2 border-t border-white/5">
<Box display="flex" gap={3}>
<Text size="xs" variant="low">
<Text as="strong" variant="med"></Text> to navigate
</Text>
<Text size="xs" variant="low">
<Text as="strong" variant="med"></Text> to select
</Text>
</Box>
<Text size="xs" variant="low">GridPilot Command</Text>
</Box>
</Box>
</Box>,
document.body
);
}

View File

@@ -18,6 +18,8 @@ interface TeamLeaderboardPreviewProps {
totalWins: number;
logoUrl: string;
position: number;
rating?: number;
performanceLevel: string;
}[];
onTeamClick: (id: string) => void;
onNavigateToTeams: () => void;
@@ -28,12 +30,12 @@ export function TeamLeaderboardPreview({ teams, onTeamClick, onNavigateToTeams }
return (
<LeaderboardPreviewShell
title="Team Rankings"
title="Team Standings"
subtitle="Top Performing Teams"
onViewFull={onNavigateToTeams}
icon={Users}
iconColor="var(--neon-purple)"
iconBgGradient="linear-gradient(to bottom right, rgba(168, 85, 247, 0.2), rgba(168, 85, 247, 0.1))"
iconColor="var(--ui-color-intent-primary)"
iconBgGradient="linear-gradient(to bottom right, rgba(25, 140, 255, 0.2), rgba(25, 140, 255, 0.1))"
viewFullLabel="View All"
>
<LeaderboardList>
@@ -72,7 +74,7 @@ export function TeamLeaderboardPreview({ teams, onTeamClick, onNavigateToTeams }
border
borderColor="border-charcoal-outline"
overflow="hidden"
groupHoverBorderColor="purple-400/50"
groupHoverBorderColor="primary-blue/50"
transition
>
<Image
@@ -91,31 +93,26 @@ export function TeamLeaderboardPreview({ teams, onTeamClick, onNavigateToTeams }
weight="semibold"
color="text-white"
truncate
groupHoverTextColor="text-purple-400"
groupHoverTextColor="text-primary-blue"
transition
block
>
{team.name}
</Text>
<Box display="flex" alignItems="center" gap={2} flexWrap="wrap">
{team.category && (
<Box display="flex" alignItems="center" gap={1} color="text-purple-400">
<Box w="1.5" h="1.5" rounded="full" bg="bg-purple-400" />
<Text size="xs" weight="medium">{team.category}</Text>
</Box>
)}
<Text size="xs" variant="low" uppercase font="mono">{team.performanceLevel}</Text>
<Box w="1" h="1" rounded="full" bg="bg-gray-700" />
<Box display="flex" alignItems="center" gap={1}>
<Icon icon={Users} size={3} color="text-gray-500" />
<Text size="xs" color="text-gray-500">{team.memberCount} members</Text>
<Text size="xs" color="text-gray-500">{team.memberCount}</Text>
</Box>
</Box>
</Box>
<Box display="flex" alignItems="center" gap={6}>
<Box textAlign="right">
<Text color="text-purple-400" font="mono" weight="bold" block size="sm">{team.memberCount}</Text>
<Text fontSize="10px" color="text-gray-500" block uppercase letterSpacing="wider" weight="bold">Members</Text>
<Text color="text-primary-blue" font="mono" weight="bold" block size="sm">{team.rating?.toFixed(0) || '1000'}</Text>
<Text fontSize="10px" color="text-gray-500" block uppercase letterSpacing="wider" weight="bold">Rating</Text>
</Box>
<Box textAlign="right" minWidth="12">
<Text color="text-performance-green" font="mono" weight="bold" block size="sm">{team.totalWins}</Text>

View File

@@ -8,6 +8,7 @@ import { Stack } from '@/ui/Stack';
import { Select } from '@/ui/Select';
import { Text } from '@/ui/Text';
import { StatusDot } from '@/ui/StatusDot';
import { Box } from '@/ui/Box';
import { Filter, Search } from 'lucide-react';
export type TimeFilter = 'all' | 'upcoming' | 'live' | 'past';
@@ -77,9 +78,9 @@ export function RaceFilterModal({
onClick={() => setTimeFilter(filter)}
>
{filter === 'live' && (
<Stack mr={2}>
<Box mr={2}>
<StatusDot intent="success" size={1.5} pulse />
</Stack>
</Box>
)}
{filter.charAt(0).toUpperCase() + filter.slice(1)}
</Button>

View File

@@ -1,4 +1,4 @@
'use thought';
'use client';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';

View File

@@ -1,7 +1,10 @@
import React, { ReactNode } from 'react';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Container } from '@/ui/Container';
import { Group } from '@/ui/Group';
import { VerticalBar } from '@/ui/VerticalBar';
interface PageHeaderProps {
title: string;
@@ -15,34 +18,35 @@ interface PageHeaderProps {
*/
export function PageHeader({ title, subtitle, action }: PageHeaderProps) {
return (
<Box
marginBottom={12}
display="flex"
flexDirection={{ base: 'col', md: 'row' }}
alignItems={{ base: 'start', md: 'end' }}
justifyContent="between"
gap={6}
borderBottom
borderColor="var(--ui-color-border-muted)"
paddingBottom={8}
<Container
size="full"
padding="none"
py={12}
>
<Box display="flex" flexDirection="col" gap={2}>
<Box display="flex" alignItems="center" gap={3}>
<Box width="4px" height="32px" bg="var(--ui-color-intent-primary)" />
<Heading level={1} weight="bold" uppercase>{title}</Heading>
</Box>
{subtitle && (
<Text variant="low" size="lg" uppercase weight="bold" letterSpacing="widest">
{subtitle}
</Text>
<Group
justify="between"
align="end"
wrap
gap={6}
>
<Group direction="col" gap={2}>
<Group align="center" gap={3}>
<VerticalBar height="2rem" />
<Heading level={1} weight="bold" uppercase>{title}</Heading>
</Group>
{subtitle && (
<Text variant="low" size="lg" uppercase weight="bold" letterSpacing="widest">
{subtitle}
</Text>
)}
</Group>
{action && (
<Group align="center">
{action}
</Group>
)}
</Box>
{action && (
<Box display="flex" alignItems="center">
{action}
</Box>
)}
</Box>
</Group>
</Container>
);
}

View File

@@ -1,19 +1,10 @@
import React from 'react';
import { getMediaUrl } from '@/lib/utilities/media';
import { TeamLeaderboardPreview as SemanticTeamLeaderboardPreview } from '@/components/leaderboards/TeamLeaderboardPreview';
import type { LeaderboardTeamItem } from '@/lib/view-data/LeaderboardTeamItem';
interface TeamLeaderboardPreviewProps {
topTeams: Array<{
id: string;
name: string;
logoUrl?: string;
category?: string;
memberCount: number;
totalWins: number;
isRecruiting: boolean;
rating?: number;
performanceLevel: string;
}>;
topTeams: LeaderboardTeamItem[];
onTeamClick: (id: string) => void;
onViewFullLeaderboard: () => void;
}
@@ -27,15 +18,17 @@ export function TeamLeaderboardPreview({
return (
<SemanticTeamLeaderboardPreview
teams={topTeams.map((team, index) => ({
teams={topTeams.map((team) => ({
id: team.id,
name: team.name,
tag: '', // Not available in this view data
tag: team.tag,
memberCount: team.memberCount,
category: team.category,
totalWins: team.totalWins,
logoUrl: team.logoUrl || getMediaUrl('team-logo', team.id),
position: index + 1
position: team.position,
rating: team.rating,
performanceLevel: team.performanceLevel
}))}
onTeamClick={onTeamClick}
onNavigateToTeams={onViewFullLeaderboard}

View File

@@ -1,6 +1,7 @@
import React, { ReactNode } from 'react';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
interface TeamsHeaderProps {
title: string;
@@ -10,24 +11,32 @@ interface TeamsHeaderProps {
export function TeamsHeader({ title, subtitle, action }: TeamsHeaderProps) {
return (
<div className="mb-12 flex flex-col md:flex-row md:items-end justify-between gap-6 border-b border-[var(--ui-color-border-muted)] pb-8">
<div className="space-y-2">
<div className="flex items-center gap-3">
<div className="w-1 h-8 bg-[var(--ui-color-intent-primary)]" />
<Box
marginBottom={12}
display="flex"
flexDirection={{ base: 'col', md: 'row' }}
alignItems={{ md: 'end' }}
justifyContent="space-between"
gap={6}
className="border-b border-[var(--ui-color-border-muted)] pb-8"
>
<Box className="space-y-2">
<Box display="flex" alignItems="center" gap={3}>
<Box width={1} height={8} className="bg-[var(--ui-color-intent-primary)]" />
<Heading level={1} weight="bold" uppercase>{title}</Heading>
</div>
</Box>
{subtitle && (
<Text variant="low" size="lg" uppercase mono className="tracking-[0.2em]">
{subtitle}
</Text>
)}
</div>
</Box>
{action && (
<div className="flex items-center">
<Box display="flex" alignItems="center">
{action}
</div>
</Box>
)}
</div>
</Box>
);
}