website refactor

This commit is contained in:
2026-01-19 18:01:30 +01:00
parent 6154d54435
commit 61b5cf3b64
120 changed files with 2226 additions and 2021 deletions

View File

@@ -1,6 +1,6 @@
import { notFound, redirect } from 'next/navigation';
import { DashboardPageQuery } from '@/lib/page-queries/DashboardPageQuery';
import { DashboardTemplate } from '@/templates/DashboardTemplate';
import { DashboardPageClient } from '@/client-wrapper/DashboardPageClient';
import { logger } from '@/lib/infrastructure/logging/logger';
export default async function DashboardPage() {
@@ -23,5 +23,5 @@ export default async function DashboardPage() {
// Success
const viewData = result.unwrap();
return <DashboardTemplate viewData={viewData} />;
}
return <DashboardPageClient viewData={viewData} />;
}

View File

@@ -1,7 +1,3 @@
'use client';
import React from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { CreateLeagueWizard } from '@/client-wrapper/CreateLeagueWizard';
import { Section } from '@/ui/Section';
import { Container } from '@/ui/Container';
@@ -10,11 +6,13 @@ import { SearchParamBuilder } from '@/lib/routing/search-params/SearchParamBuild
type StepName = 'basics' | 'visibility' | 'structure' | 'schedule' | 'scoring' | 'stewarding' | 'review';
export default function CreateLeaguePage() {
const router = useRouter();
const searchParams = useSearchParams();
interface CreateLeaguePageProps {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
const wizardParams = SearchParamParser.parseWizard(searchParams as unknown as URLSearchParams).unwrap();
export default async function CreateLeaguePage({ searchParams }: CreateLeaguePageProps) {
const resolvedParams = await searchParams;
const wizardParams = SearchParamParser.parseWizard(resolvedParams as any).unwrap();
const rawStep = wizardParams.step;
let currentStepName: StepName = 'basics';
@@ -28,23 +26,11 @@ export default function CreateLeaguePage() {
currentStepName = rawStep;
}
const handleStepChange = (stepName: StepName) => {
const builder = new SearchParamBuilder();
// Copy existing params if needed, but here we just want to set the step
if (searchParams) {
searchParams.forEach((value, key) => {
if (key !== 'step') builder.set(key, value);
});
}
builder.step(stepName);
router.push(`/leagues/create${builder.build()}`);
};
return (
<Section>
<Container size="md">
<CreateLeagueWizard stepName={currentStepName} onStepChange={handleStepChange} />
<CreateLeagueWizard stepName={currentStepName} />
</Container>
</Section>
);
}
}

View File

@@ -1,5 +1,4 @@
import { PageWrapper } from '@/components/shared/state/PageWrapper';
import { HomeTemplate, type HomeViewData } from '@/templates/HomeTemplate';
import { HomePageClient } from '@/client-wrapper/HomePageClient';
import { PageDataFetcher } from '@/lib/page/PageDataFetcher';
import { HomePageQuery } from '@/lib/page-queries/HomePageQuery';
import { notFound, redirect } from 'next/navigation';
@@ -19,7 +18,5 @@ export default async function Page() {
notFound();
}
const Template = ({ viewData }: { viewData: HomeViewData }) => <HomeTemplate viewData={viewData} />;
return <PageWrapper data={data} Template={Template} />;
return <HomePageClient viewData={data} />;
}

View File

@@ -3,7 +3,7 @@
import { useState } from 'react';
import { SponsorSettingsTemplate } from '@/templates/SponsorSettingsTemplate';
import { logoutAction } from '@/app/actions/logoutAction';
import { ConfirmDialog } from '@/ui/ConfirmDialog';
import { ConfirmDialog } from '@/components/shared/ConfirmDialog';
import { useRouter } from 'next/navigation';
import { routes } from '@/lib/routing/RouteConfig';
import { logger } from '@/lib/infrastructure/logging/logger';

View File

@@ -1,37 +1,5 @@
'use client';
import { CreateTeamPageClient } from '@/client-wrapper/CreateTeamPageClient';
import { CreateTeamForm } from '@/components/teams/CreateTeamForm';
import { routes } from '@/lib/routing/RouteConfig';
import { Container } from '@/ui/Container';
import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/Stack';
import { Section } from '@/ui/Section';
import { useRouter } from 'next/navigation';
export default function CreateTeamPage() {
const router = useRouter();
const handleNavigate = (teamId: string) => {
router.push(routes.team.detail(teamId));
};
const handleCancel = () => {
router.back();
};
return (
<Section>
<Container size="sm">
<Stack gap={8}>
<Stack gap={2}>
<Heading level={1}>Create a Team</Heading>
</Stack>
<CreateTeamForm
onNavigate={handleNavigate}
onCancel={handleCancel}
/>
</Stack>
</Container>
</Section>
);
export default async function CreateTeamPage() {
return <CreateTeamPageClient />;
}

View File

@@ -12,6 +12,7 @@ import type { Weekday } from '@/lib/types/Weekday';
import type { WizardErrors } from '@/lib/types/WizardErrors';
import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel';
import { CreateLeagueWizardTemplate, Step } from '@/templates/CreateLeagueWizardTemplate';
import { SearchParamBuilder } from '@/lib/routing/search-params/SearchParamBuilder';
import {
Award,
Calendar,
@@ -91,7 +92,7 @@ type LeagueWizardFormModel = LeagueConfigFormModel & {
interface CreateLeagueWizardProps {
stepName: StepName;
onStepChange: (stepName: StepName) => void;
onStepChange?: (stepName: StepName) => void;
}
function stepNameToStep(stepName: StepName): Step {
@@ -200,6 +201,16 @@ export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizar
const router = useRouter();
const { session } = useAuth();
const handleStepChange = useCallback((newStepName: StepName) => {
if (onStepChange) {
onStepChange(newStepName);
} else {
const builder = new SearchParamBuilder();
builder.step(newStepName);
router.push(`/leagues/create${builder.build()}`);
}
}, [onStepChange, router]);
const step = stepNameToStep(stepName);
const [loading, setLoading] = useState(false);
const [presetsLoading, setPresetsLoading] = useState(true);
@@ -330,19 +341,19 @@ export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizar
const nextStep = (step < 7 ? ((step + 1) as Step) : step);
saveHighestStep(nextStep);
setHighestCompletedStep((prev) => Math.max(prev, nextStep));
onStepChange(stepToStepName(nextStep));
handleStepChange(stepToStepName(nextStep));
};
const goToPreviousStep = () => {
const prevStep = (step > 1 ? ((step - 1) as Step) : step);
onStepChange(stepToStepName(prevStep));
handleStepChange(stepToStepName(prevStep));
};
const goToStep = useCallback((targetStep: Step) => {
if (targetStep <= highestCompletedStep) {
onStepChange(stepToStepName(targetStep));
handleStepChange(stepToStepName(targetStep));
}
}, [highestCompletedStep, onStepChange]);
}, [highestCompletedStep, handleStepChange]);
const handleSubmit = async (event: FormEvent) => {
event.preventDefault();
@@ -413,7 +424,7 @@ export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizar
}));
if (LeagueWizardCommandModel.hasWizardErrors(allErrors)) {
onStepChange('basics');
handleStepChange('basics');
return;
}

View File

@@ -0,0 +1,42 @@
'use client';
import { CreateTeamForm } from '@/components/teams/CreateTeamForm';
import { routes } from '@/lib/routing/RouteConfig';
import { Container } from '@/ui/Container';
import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/Stack';
import { Section } from '@/ui/Section';
import { useRouter } from 'next/navigation';
/**
* CreateTeamPageClient
*
* Client wrapper for the Create Team page.
*/
export function CreateTeamPageClient() {
const router = useRouter();
const handleNavigate = (teamId: string) => {
router.push(routes.team.detail(teamId));
};
const handleCancel = () => {
router.back();
};
return (
<Section>
<Container size="sm">
<Stack gap={8}>
<Stack gap={2}>
<Heading level={1}>Create a Team</Heading>
</Stack>
<CreateTeamForm
onNavigate={handleNavigate}
onCancel={handleCancel}
/>
</Stack>
</Container>
</Section>
);
}

View File

@@ -0,0 +1,26 @@
'use client';
import { DashboardTemplate } from '@/templates/DashboardTemplate';
import type { DashboardViewData } from '@/lib/view-data/DashboardViewData';
import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts';
import { useRouter } from 'next/navigation';
import { routes } from '@/lib/routing/RouteConfig';
/**
* DashboardPageClient
*
* Client wrapper for the Dashboard page.
* Manages client-side interactions for the dashboard.
*/
export function DashboardPageClient({ viewData }: ClientWrapperProps<DashboardViewData>) {
const router = useRouter();
const handleNavigateToRaces = () => router.push(routes.public.races);
return (
<DashboardTemplate
viewData={viewData}
onNavigateToRaces={handleNavigateToRaces}
/>
);
}

View File

@@ -0,0 +1,20 @@
'use client';
import { HomeTemplate, type HomeViewData } from '@/templates/HomeTemplate';
import { PageWrapper } from '@/components/shared/state/PageWrapper';
interface HomePageClientProps {
viewData: HomeViewData;
}
/**
* HomePageClient - Client wrapper for the Home page.
* Manages state and handlers for the home page.
*/
export function HomePageClient({ viewData }: HomePageClientProps) {
const Template = ({ viewData }: { viewData: HomeViewData }) => (
<HomeTemplate viewData={viewData} />
);
return <PageWrapper data={viewData} Template={Template} />;
}

View File

@@ -8,7 +8,7 @@ import { NotificationProvider } from '@/components/notifications/NotificationPro
import { NotificationIntegration } from '@/components/errors/NotificationIntegration';
import { EnhancedErrorBoundary } from '@/components/errors/EnhancedErrorBoundary';
import { DevToolbar } from '@/components/dev/DevToolbar';
import { ThemeProvider } from '@/ui/theme/ThemeProvider';
import { ThemeProvider } from '@/components/shared/ThemeProvider';
import React from 'react';
interface AppWrapperProps {

View File

@@ -1,6 +1,6 @@
'use client';
import { ProgressLine } from '@/ui/ProgressLine';
import { ProgressLine } from '@/components/shared/ProgressLine';
import { SectionHeader } from '@/ui/SectionHeader';
import React from 'react';

View File

@@ -1,5 +1,12 @@
import React from 'react';
import { Footer } from '@/ui/Footer';
import { Container } from '@/ui/Container';
import { Grid } from '@/ui/Grid';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Link } from '@/ui/Link';
import { Surface } from '@/ui/Surface';
import { BrandMark } from '@/ui/BrandMark';
import { Box } from '@/ui/Box';
interface AppFooterProps {
children?: React.ReactNode;
@@ -7,9 +14,76 @@ interface AppFooterProps {
/**
* AppFooter is the bottom section of the application.
* It follows the "Telemetry Workspace" structure: matte surface, thin separators, minimal.
* Aligned with ControlBar (Header) design.
*/
export function AppFooter({ children }: AppFooterProps) {
return (
<Footer />
<Surface
as="footer"
variant="muted"
paddingY={8}
marginTop="auto"
style={{ borderTop: '1px solid var(--ui-color-border-default)' }}
>
<Container size="xl">
{children ? (
children
) : (
<Grid cols={{ base: 1, md: 4 }} gap={12}>
<Stack gap={4}>
<Stack direction="row" align="center" gap={4}>
<BrandMark />
<Box display={{ base: 'none', sm: 'flex' }} alignItems="center" gap={2} borderLeft borderColor="[#23272B]" pl={4}>
<Box w="4px" h="4px" rounded="full" bg="primary-accent" animate="pulse" />
<Text size="xs" variant="low" weight="bold" font="mono" letterSpacing="0.1em">
INFRASTRUCTURE
</Text>
</Box>
</Stack>
<Text size="sm" variant="low" maxWidth="xs">
The ultimate companion for sim racers. Track your performance, manage your team, and compete in leagues.
</Text>
</Stack>
<Stack gap={4}>
<Text size="xs" weight="bold" variant="high" uppercase letterSpacing="wider">Platform</Text>
<Stack gap={2}>
<Link href="/leagues" variant="secondary">Leagues</Link>
<Link href="/teams" variant="secondary">Teams</Link>
<Link href="/leaderboards" variant="secondary">Leaderboards</Link>
</Stack>
</Stack>
<Stack gap={4}>
<Text size="xs" weight="bold" variant="high" uppercase letterSpacing="wider">Support</Text>
<Stack gap={2}>
<Link href="/docs" variant="secondary">Documentation</Link>
<Link href="/status" variant="secondary">System Status</Link>
<Link href="/contact" variant="secondary">Contact Us</Link>
</Stack>
</Stack>
<Stack gap={4}>
<Text size="xs" weight="bold" variant="high" uppercase letterSpacing="wider">Legal</Text>
<Stack gap={2}>
<Link href="/privacy" variant="secondary">Privacy Policy</Link>
<Link href="/terms" variant="secondary">Terms of Service</Link>
</Stack>
</Stack>
</Grid>
)}
<Box borderTop marginTop={8} paddingTop={6} display="flex" justifyContent="between" alignItems="center">
<Text size="xs" variant="low">
© {new Date().getFullYear()} GridPilot. All rights reserved.
</Text>
<Stack direction="row" gap={4}>
<Text size="xs" variant="low" font="mono">v1.0.0-stable</Text>
<Box w="4px" h="4px" rounded="full" bg="success-green" />
</Stack>
</Box>
</Container>
</Surface>
);
}

View File

@@ -1,7 +1,6 @@
'use client';
import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
import { SectionHeader } from '@/ui/SectionHeader';
import React from 'react';
@@ -24,9 +23,7 @@ export function AuthCard({ children, title, description }: AuthCardProps) {
description={description}
variant="minimal"
/>
<Box>
{children}
</Box>
{children}
</Card>
);
}

View File

@@ -1,6 +1,6 @@
'use client';
import { Stack } from '@/ui/Stack';
import { Group } from '@/ui/Group';
import React from 'react';
interface AuthFooterLinksProps {
@@ -14,10 +14,8 @@ interface AuthFooterLinksProps {
*/
export function AuthFooterLinks({ children }: AuthFooterLinksProps) {
return (
<Stack as="footer" mt={8} pt={6} borderTop borderStyle="solid" borderColor="outline-steel">
<Stack gap={3} align="center">
{children}
</Stack>
</Stack>
<Group direction="column" gap={3} align="center" fullWidth>
{children}
</Group>
);
}

View File

@@ -1,6 +1,7 @@
'use client';
import { Stack } from '@/ui/Stack';
import { Form } from '@/ui/Form';
import { Group } from '@/ui/Group';
import React from 'react';
interface AuthFormProps {
@@ -15,8 +16,10 @@ interface AuthFormProps {
*/
export function AuthForm({ children, onSubmit }: AuthFormProps) {
return (
<Stack as="form" {...({ onSubmit, noValidate: true } as any)} gap={6}>
{children}
</Stack>
<Form onSubmit={onSubmit}>
<Group direction="column" gap={6}>
{children}
</Group>
</Form>
);
}

View File

@@ -8,7 +8,7 @@ import {
Trophy,
Car,
} from 'lucide-react';
import { WorkflowMockup, WorkflowStep } from '@/ui/WorkflowMockup';
import { WorkflowMockup, WorkflowStep } from '@/components/mockups/WorkflowMockup';
const WORKFLOW_STEPS: WorkflowStep[] = [
{

View File

@@ -10,7 +10,7 @@ import { Activity, AlertTriangle, ChevronDown, ChevronUp, MessageSquare, Wrench,
import { useEffect, useState } from 'react';
// Import our new components
import { Accordion } from '@/ui/Accordion';
import { Accordion } from '@/components/shared/Accordion';
import { Badge } from '@/ui/Badge';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';

View File

@@ -30,17 +30,17 @@ export function DriverEntryRow({
return (
<Stack
onClick={onClick}
display="flex"
alignItems="center"
direction="row"
align="center"
gap={3}
p={3}
rounded="xl"
cursor="pointer"
transition
bg={isCurrentUser ? 'bg-primary-blue/10' : 'transparent'}
bg={isCurrentUser ? 'rgba(25, 140, 255, 0.1)' : 'transparent'}
border
borderColor={isCurrentUser ? 'border-primary-blue/30' : 'transparent'}
hoverBorderColor={isCurrentUser ? 'primary-blue/40' : 'charcoal-outline/20'}
borderColor={isCurrentUser ? 'rgba(25, 140, 255, 0.3)' : 'transparent'}
hoverBorderColor={isCurrentUser ? 'rgba(25, 140, 255, 0.4)' : 'var(--ui-color-border-low)'}
>
<Stack
display="flex"
@@ -49,9 +49,10 @@ export function DriverEntryRow({
w="8"
h="8"
rounded="lg"
bg="bg-iron-gray"
color="text-gray-500"
style={{ fontWeight: 'bold', fontSize: '0.875rem' }}
bg="var(--ui-color-bg-surface-muted)"
color="var(--ui-color-text-low)"
weight="bold"
fontSize="0.875rem"
>
{index + 1}
</Stack>
@@ -63,7 +64,7 @@ export function DriverEntryRow({
rounded="full"
overflow="hidden"
border={isCurrentUser}
borderColor={isCurrentUser ? 'border-primary-blue' : ''}
borderColor={isCurrentUser ? 'var(--ui-color-intent-primary)' : ''}
>
<Image
src={avatarUrl}
@@ -81,11 +82,11 @@ export function DriverEntryRow({
w="5"
h="5"
rounded="full"
bg="bg-deep-graphite"
bg="var(--ui-color-bg-base)"
display="flex"
alignItems="center"
justifyContent="center"
style={{ fontSize: '0.625rem' }}
fontSize="0.625rem"
>
{CountryFlagDisplay.fromCountryCode(country).toString()}
</Stack>
@@ -96,14 +97,14 @@ export function DriverEntryRow({
<Text
weight="semibold"
size="sm"
color={isCurrentUser ? 'text-primary-blue' : 'text-white'}
variant={isCurrentUser ? 'primary' : 'high'}
truncate
>
{name}
</Text>
{isCurrentUser && <Badge variant="primary">You</Badge>}
</Stack>
<Text size="xs" color="text-gray-500">{country}</Text>
<Text size="xs" variant="low">{country}</Text>
</Stack>
{rating != null && (

View File

@@ -29,10 +29,10 @@ export function DriverHeaderPanel({
return (
<Stack
bg="bg-panel-gray"
bg="var(--ui-color-bg-surface)"
rounded="xl"
border
borderColor="border-charcoal-outline"
borderColor="var(--ui-color-border-low)"
overflow="hidden"
position="relative"
>
@@ -43,12 +43,12 @@ export function DriverHeaderPanel({
left={0}
right={0}
height="24"
bg="bg-gradient-to-r from-primary-blue/20 to-transparent"
bg="linear-gradient(to right, rgba(25, 140, 255, 0.2), transparent)"
opacity={0.5}
/>
<Stack p={6} position="relative">
<Stack direction={{ base: 'col', md: 'row' }} gap={6} align="start" className="md:items-center">
<Stack direction={{ base: 'col', md: 'row' }} gap={6} align={{ base: 'start', md: 'center' }}>
{/* Avatar */}
<Stack
width="32"
@@ -56,8 +56,8 @@ export function DriverHeaderPanel({
rounded="2xl"
overflow="hidden"
border
borderColor="border-charcoal-outline"
bg="bg-graphite-black"
borderColor="var(--ui-color-border-low)"
bg="var(--ui-color-bg-base)"
flexShrink={0}
>
<Image
@@ -72,25 +72,25 @@ export function DriverHeaderPanel({
<Stack flexGrow={1}>
<Stack gap={2}>
<Stack direction="row" align="center" gap={3} wrap>
<Text as="h1" size="3xl" weight="bold" color="text-white">
<Text as="h1" size="3xl" weight="bold" variant="high">
{name}
</Text>
<RatingBadge rating={rating} ratingLabel={ratingLabel} size="lg" />
</Stack>
<Stack direction="row" align="center" gap={4} wrap>
<Text size="sm" color="text-gray-400">
<Text size="sm" variant="low">
{nationality}
</Text>
{globalRankLabel && (
<Text size="sm" color="text-gray-400">
Global Rank: <Text color="text-warning-amber" weight="semibold">{globalRankLabel}</Text>
<Text size="sm" variant="low">
Global Rank: <Text variant="warning" weight="semibold">{globalRankLabel}</Text>
</Text>
)}
</Stack>
{bio && (
<Text size="sm" color="text-gray-400" className="max-w-2xl mt-2" lineClamp={2}>
<Text size="sm" variant="low" maxWidth="2xl" mt={2} lineClamp={2}>
{bio}
</Text>
)}

View File

@@ -1,6 +1,6 @@
'use client';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Input } from '@/ui/Input';
import { Search } from 'lucide-react';
@@ -11,13 +11,13 @@ interface DriverSearchBarProps {
export function DriverSearchBar({ query, onChange }: DriverSearchBarProps) {
return (
<Box position="relative" group>
<Group fullWidth>
<Input
value={query}
onChange={(e) => onChange(e.target.value)}
placeholder="Search drivers by name or nationality..."
icon={<Search size={20} />}
/>
</Box>
</Group>
);
}

View File

@@ -2,7 +2,7 @@
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Group } from '@/ui/Group';
import { Table, TableHead, TableBody, TableRow, TableHeaderCell } from '@/ui/Table';
import { TrendingUp } from 'lucide-react';
import { Card } from '@/ui/Card';
@@ -15,16 +15,16 @@ interface DriverTableProps {
export function DriverTable({ children }: DriverTableProps) {
return (
<Stack gap={4}>
<Stack direction="row" align="center" gap={3}>
<Group direction="column" gap={4} fullWidth>
<Group direction="row" align="center" gap={3}>
<Card variant="dark">
<Icon icon={TrendingUp} size={5} intent="primary" />
</Card>
<Stack>
<Group direction="column">
<Heading level={2}>Driver Rankings</Heading>
<Text size="xs" variant="low">Top performers by skill rating</Text>
</Stack>
</Stack>
</Group>
</Group>
<Table>
<TableHead>
@@ -40,6 +40,6 @@ export function DriverTable({ children }: DriverTableProps) {
{children}
</TableBody>
</Table>
</Stack>
</Group>
);
}

View File

@@ -1,9 +1,8 @@
'use client';
import { RatingBadge } from '@/components/drivers/RatingBadge';
import { Image } from '@/ui/Image';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Group } from '@/ui/Group';
import { TableRow, TableCell } from '@/ui/Table';
import { Avatar } from '@/ui/Avatar';
@@ -41,7 +40,7 @@ export function DriverTableRow({
</Text>
</TableCell>
<TableCell>
<Stack direction="row" align="center" gap={3}>
<Group direction="row" align="center" gap={3}>
<Avatar
src={avatarUrl || undefined}
alt={name}
@@ -54,7 +53,7 @@ export function DriverTableRow({
>
{name}
</Text>
</Stack>
</Group>
</TableCell>
<TableCell>
<Text size="xs" variant="low">{nationality}</Text>

View File

@@ -2,7 +2,7 @@
import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/Stack';
import { Group } from '@/ui/Group';
import { Text } from '@/ui/Text';
import { Section } from '@/ui/Section';
import { Container } from '@/ui/Container';
@@ -41,31 +41,31 @@ export function DriversDirectoryHeader({
return (
<Section variant="dark" padding="md">
<Container>
<Stack direction="row" align="center" justify="between" gap={8}>
<Stack gap={6}>
<Stack direction="row" align="center" gap={3}>
<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">
<Icon icon={Users} size={6} intent="primary" />
</Card>
<Heading level={1}>Drivers</Heading>
</Stack>
</Group>
<Text size="lg" variant="low">
Meet the racers who make every lap count. From rookies to champions, track their journey and see who&apos;s dominating the grid.
Meet the racers who make every lap count. From rookies to champions, track their journey and see who's dominating the grid.
</Text>
<Stack direction="row" gap={6} wrap>
<Group direction="row" gap={6} wrap>
{stats.map((stat, index) => (
<Stack key={index} direction="row" align="center" gap={2}>
<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>
</Stack>
</Group>
))}
</Stack>
</Stack>
</Group>
</Group>
<Stack gap={2} align="center">
<Group direction="column" gap={2} align="center">
<Button
variant="primary"
onClick={onViewLeaderboard}
@@ -76,8 +76,8 @@ export function DriversDirectoryHeader({
<Text size="xs" variant="low" align="center">
See full driver rankings
</Text>
</Stack>
</Stack>
</Group>
</Group>
</Container>
</Section>
);

View File

@@ -25,10 +25,10 @@ interface LiveryCardProps {
export function LiveryCard({ livery, onEdit, onDownload, onDelete }: LiveryCardProps) {
return (
<Card className="overflow-hidden hover:border-primary-blue/50 transition-colors">
<Card overflow="hidden" hoverBorderColor="var(--ui-color-intent-primary)" transition="all 0.3s ease">
{/* Livery Preview */}
<Stack height={48} backgroundColor="deep-graphite" rounded="lg" mb={4} display="flex" center border borderColor="charcoal-outline">
<Icon icon={Car} size={16} color="text-gray-600" />
<Stack height={48} bg="var(--ui-color-bg-base)" rounded="lg" mb={4} display="flex" center border borderColor="var(--ui-color-border-low)">
<Icon icon={Car} size={16} intent="low" />
</Stack>
{/* Livery Info */}

View File

@@ -27,7 +27,7 @@ export function PerformanceOverview({ stats }: PerformanceOverviewProps) {
return (
<Card>
<Stack mb={6}>
<Heading level={2} icon={<Icon icon={Activity} size={5} color="#00f2ff" />}>
<Heading level={2} icon={<Icon icon={Activity} size={5} intent="telemetry" />}>
Performance Overview
</Heading>
</Stack>
@@ -39,13 +39,13 @@ export function PerformanceOverview({ stats }: PerformanceOverviewProps) {
value={stats.wins}
max={stats.totalRaces}
label="Win Rate"
color="#10b981"
color="var(--ui-color-intent-success)"
/>
<CircularProgress
value={stats.podiums}
max={stats.totalRaces}
label="Podium Rate"
color="#f59e0b"
color="var(--ui-color-intent-warning)"
/>
</Stack>
<Stack direction="row" gap={6}>
@@ -53,13 +53,13 @@ export function PerformanceOverview({ stats }: PerformanceOverviewProps) {
value={stats.consistency ?? 0}
max={100}
label="Consistency"
color="#3b82f6"
color="var(--ui-color-intent-primary)"
/>
<CircularProgress
value={stats.totalRaces - stats.dnfs}
max={stats.totalRaces}
label="Finish Rate"
color="#00f2ff"
color="var(--ui-color-intent-telemetry)"
/>
</Stack>
</Stack>
@@ -67,37 +67,37 @@ export function PerformanceOverview({ stats }: PerformanceOverviewProps) {
<GridItem colSpan={12} lgSpan={6}>
<Stack mb={4}>
<Heading level={3} icon={<Icon icon={BarChart3} size={4} color="#9ca3af" />}>
<Heading level={3} icon={<Icon icon={BarChart3} size={4} intent="low" />}>
Results Breakdown
</Heading>
</Stack>
<HorizontalBarChart
data={[
{ label: 'Wins', value: stats.wins, color: 'bg-performance-green' },
{ label: 'Podiums (2nd-3rd)', value: stats.podiums - stats.wins, color: 'bg-warning-amber' },
{ label: 'DNFs', value: stats.dnfs, color: 'bg-red-500' },
{ label: 'Wins', value: stats.wins, color: 'var(--ui-color-intent-success)' },
{ label: 'Podiums (2nd-3rd)', value: stats.podiums - stats.wins, color: 'var(--ui-color-intent-warning)' },
{ label: 'DNFs', value: stats.dnfs, color: 'var(--ui-color-intent-critical)' },
]}
maxValue={stats.totalRaces}
/>
<Stack mt={6}>
<Grid cols={2} gap={4}>
<Stack p={4} style={{ backgroundColor: '#0f1115', borderRadius: '0.75rem', border: '1px solid #262626' }}>
<Stack p={4} bg="var(--ui-color-bg-base)" rounded="xl" border borderColor="var(--ui-color-border-low)">
<Stack gap={2}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={TrendingUp} size={4} color="#10b981" />
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase' }}>Best Finish</Text>
<Icon icon={TrendingUp} size={4} intent="success" />
<Text size="xs" variant="low" uppercase>Best Finish</Text>
</Stack>
<Text size="2xl" weight="bold" color="text-performance-green">P{stats.bestFinish}</Text>
<Text size="2xl" weight="bold" variant="success">P{stats.bestFinish}</Text>
</Stack>
</Stack>
<Stack p={4} style={{ backgroundColor: '#0f1115', borderRadius: '0.75rem', border: '1px solid #262626' }}>
<Stack p={4} bg="var(--ui-color-bg-base)" rounded="xl" border borderColor="var(--ui-color-border-low)">
<Stack gap={2}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Target} size={4} color="#3b82f6" />
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase' }}>Avg Finish</Text>
<Icon icon={Target} size={4} intent="primary" />
<Text size="xs" variant="low" uppercase>Avg Finish</Text>
</Stack>
<Text size="2xl" weight="bold" color="text-primary-blue">
<Text size="2xl" weight="bold" variant="primary">
P{(stats.avgFinish ?? 0).toFixed(1)}
</Text>
</Stack>

View File

@@ -9,6 +9,8 @@ import { Link } from '@/ui/Link';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Icon } from '@/ui/Icon';
import { Calendar, Clock, ExternalLink, Globe, Star, Trophy, UserPlus } from 'lucide-react';
interface ProfileHeroProps {
@@ -54,25 +56,40 @@ export function ProfileHero({
friendRequestSent,
}: ProfileHeroProps) {
return (
<Surface variant="muted" rounded="2xl" border padding={6} style={{ background: 'linear-gradient(to bottom right, rgba(38, 38, 38, 0.8), rgba(38, 38, 38, 0.6), #0f1115)', borderColor: '#262626' }}>
<Surface
variant="muted"
rounded="2xl"
border
padding={6}
bg="linear-gradient(to bottom right, rgba(38, 38, 38, 0.8), rgba(38, 38, 38, 0.6), var(--ui-color-bg-base))"
borderColor="var(--ui-color-border-low)"
>
<Stack direction="row" align="start" gap={6} wrap>
{/* Avatar */}
<Stack style={{ position: 'relative' }}>
<Stack style={{ width: '7rem', height: '7rem', borderRadius: '1rem', background: 'linear-gradient(to bottom right, #3b82f6, #9333ea)', padding: '0.25rem', boxShadow: '0 20px 25px -5px rgba(59, 130, 246, 0.2)' }}>
<Stack style={{ width: '100%', height: '100%', borderRadius: '0.75rem', overflow: 'hidden', backgroundColor: '#262626' }}>
<Box position="relative">
<Stack
w="28"
h="28"
rounded="xl"
bg="linear-gradient(to bottom right, var(--ui-color-intent-primary), rgba(147, 51, 234, 1))"
padding={1}
shadow="xl"
>
<Stack fullWidth fullHeight rounded="lg" overflow="hidden" bg="var(--ui-color-bg-surface-muted)">
<Image
src={driver.avatarUrl || mediaConfig.avatars.defaultFallback}
alt={driver.name}
width={144}
height={144}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
objectFit="cover"
fill
/>
</Stack>
</Stack>
</Stack>
</Box>
{/* Driver Info */}
<Stack style={{ flex: 1, minWidth: 0 }}>
<Stack flex={1} minWidth="0">
<Stack direction="row" align="center" gap={3} wrap mb={2}>
<Heading level={1}>{driver.name}</Heading>
<Text size="4xl" aria-label={`Country: ${driver.country}`}>
@@ -84,18 +101,18 @@ export function ProfileHero({
<Stack direction="row" align="center" gap={4} wrap mb={4}>
{stats && (
<>
<Surface variant="muted" rounded="lg" padding={1} style={{ backgroundColor: 'rgba(59, 130, 246, 0.1)', border: '1px solid rgba(59, 130, 246, 0.3)', paddingLeft: '0.75rem', paddingRight: '0.75rem' }}>
<Surface variant="muted" rounded="lg" padding={1} bg="rgba(25, 140, 255, 0.1)" border borderColor="rgba(25, 140, 255, 0.3)" px={3}>
<Stack direction="row" align="center" gap={2}>
<Star style={{ width: '1rem', height: '1rem', color: '#3b82f6' }} />
<Text font="mono" weight="bold" color="text-primary-blue">{stats.ratingLabel}</Text>
<Text size="xs" color="text-gray-400">Rating</Text>
<Icon icon={Star} size={4} intent="primary" />
<Text font="mono" weight="bold" variant="primary">{stats.ratingLabel}</Text>
<Text size="xs" variant="low">Rating</Text>
</Stack>
</Surface>
<Surface variant="muted" rounded="lg" padding={1} style={{ backgroundColor: 'rgba(250, 204, 21, 0.1)', border: '1px solid rgba(250, 204, 21, 0.3)', paddingLeft: '0.75rem', paddingRight: '0.75rem' }}>
<Surface variant="muted" rounded="lg" padding={1} bg="rgba(250, 204, 21, 0.1)" border borderColor="rgba(250, 204, 21, 0.3)" px={3}>
<Stack direction="row" align="center" gap={2}>
<Trophy style={{ width: '1rem', height: '1rem', color: '#facc15' }} />
<Text font="mono" weight="bold" style={{ color: '#facc15' }}>{globalRankLabel}</Text>
<Text size="xs" color="text-gray-400">Global</Text>
<Icon icon={Trophy} size={4} intent="warning" />
<Text font="mono" weight="bold" color="rgba(250, 204, 21, 1)">{globalRankLabel}</Text>
<Text size="xs" variant="low">Global</Text>
</Stack>
</Surface>
</>
@@ -103,19 +120,19 @@ export function ProfileHero({
</Stack>
{/* Meta info */}
<Stack direction="row" align="center" gap={4} wrap style={{ fontSize: '0.875rem', color: '#9ca3af' }}>
<Stack direction="row" align="center" gap={4} wrap color="var(--ui-color-text-low)">
<Stack direction="row" align="center" gap={1.5}>
<Globe style={{ width: '1rem', height: '1rem' }} />
<Icon icon={Globe} size={4} />
<Text size="sm">iRacing: {driver.iracingId}</Text>
</Stack>
<Stack direction="row" align="center" gap={1.5}>
<Calendar style={{ width: '1rem', height: '1rem' }} />
<Icon icon={Calendar} size={4} />
<Text size="sm">
Joined {driver.joinedAtLabel}
</Text>
</Stack>
<Stack direction="row" align="center" gap={1.5}>
<Clock style={{ width: '1rem', height: '1rem' }} />
<Icon icon={Clock} size={4} />
<Text size="sm">{timezone}</Text>
</Stack>
</Stack>
@@ -127,7 +144,7 @@ export function ProfileHero({
variant="primary"
onClick={onAddFriend}
disabled={friendRequestSent}
icon={<UserPlus style={{ width: '1rem', height: '1rem' }} />}
icon={<Icon icon={UserPlus} size={4} />}
>
{friendRequestSent ? 'Request Sent' : 'Add Friend'}
</Button>
@@ -136,23 +153,22 @@ export function ProfileHero({
{/* Social Handles */}
{socialHandles.length > 0 && (
<Stack mt={6} pt={6} style={{ borderTop: '1px solid rgba(38, 38, 38, 0.5)' }}>
<Stack mt={6} pt={6} borderTop borderColor="rgba(255, 255, 255, 0.05)">
<Stack direction="row" align="center" gap={2} wrap>
<Text size="sm" color="text-gray-500" style={{ marginRight: '0.5rem' }}>Connect:</Text>
<Text size="sm" variant="low" mr={2}>Connect:</Text>
{socialHandles.map((social) => {
const Icon = getSocialIcon(social.platform);
const SocialIcon = getSocialIcon(social.platform);
return (
<Stack key={social.platform}>
<Link
href={social.url}
target="_blank"
rel="noopener noreferrer"
variant="ghost"
>
<Surface variant="muted" rounded="lg" padding={1} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', paddingLeft: '0.75rem', paddingRight: '0.75rem', backgroundColor: 'rgba(38, 38, 38, 0.5)', border: '1px solid #262626', color: '#9ca3af' }}>
<Icon style={{ width: '1rem', height: '1rem' }} />
<Surface variant="muted" rounded="lg" padding={1} display="flex" alignItems="center" gap={2} px={3} bg="rgba(255, 255, 255, 0.05)" border borderColor="var(--ui-color-border-low)" color="var(--ui-color-text-low)">
<Icon icon={SocialIcon} size={4} />
<Text size="sm">{social.handle}</Text>
<ExternalLink style={{ width: '0.75rem', height: '0.75rem', opacity: 0.5 }} />
<Icon icon={ExternalLink} size={3} opacity={0.5} />
</Surface>
</Link>
</Stack>

View File

@@ -7,10 +7,10 @@ import { Text } from '@/ui/Text';
import { BarChart3 } from 'lucide-react';
const SKILL_LEVELS = [
{ id: 'pro', label: 'Pro', icon: BarChart3, color: 'text-yellow-400', bgColor: 'bg-yellow-400/10', borderColor: 'border-yellow-400/30' },
{ id: 'advanced', label: 'Advanced', icon: BarChart3, color: 'text-purple-400', bgColor: 'bg-purple-400/10', borderColor: 'border-purple-400/30' },
{ id: 'intermediate', label: 'Intermediate', icon: BarChart3, color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', borderColor: 'border-primary-blue/30' },
{ id: 'beginner', label: 'Beginner', icon: BarChart3, color: 'text-green-400', bgColor: 'bg-green-400/10', borderColor: 'border-green-400/30' },
{ id: 'pro', label: 'Pro', icon: BarChart3, color: 'var(--ui-color-intent-warning)', bgColor: 'rgba(255, 190, 77, 0.1)', borderColor: 'rgba(255, 190, 77, 0.3)' },
{ id: 'advanced', label: 'Advanced', icon: BarChart3, color: 'rgba(147, 51, 234, 1)', bgColor: 'rgba(147, 51, 234, 0.1)', borderColor: 'rgba(147, 51, 234, 0.3)' },
{ id: 'intermediate', label: 'Intermediate', icon: BarChart3, color: 'var(--ui-color-intent-primary)', bgColor: 'rgba(25, 140, 255, 0.1)', borderColor: 'rgba(25, 140, 255, 0.3)' },
{ id: 'beginner', label: 'Beginner', icon: BarChart3, color: 'var(--ui-color-intent-success)', bgColor: 'rgba(16, 185, 129, 0.1)', borderColor: 'rgba(16, 185, 129, 0.3)' },
];
interface SkillDistributionProps {
@@ -38,15 +38,15 @@ export function SkillDistribution({ drivers }: SkillDistributionProps) {
alignItems="center"
justifyContent="center"
rounded="xl"
bg="bg-neon-aqua/10"
bg="rgba(78, 212, 224, 0.1)"
border
borderColor="border-neon-aqua/20"
borderColor="rgba(78, 212, 224, 0.2)"
>
<Icon icon={BarChart3} size={5} color="var(--neon-aqua)" />
<Icon icon={BarChart3} size={5} intent="telemetry" />
</Box>
<Box>
<Heading level={2}>Skill Distribution</Heading>
<Text size="xs" color="text-gray-500">Driver population by skill level</Text>
<Text size="xs" variant="low">Driver population by skill level</Text>
</Box>
</Box>
@@ -58,28 +58,24 @@ export function SkillDistribution({ drivers }: SkillDistributionProps) {
p={4}
rounded="xl"
border
className={`${level.bgColor} ${level.borderColor}`}
bg={level.bgColor}
borderColor={level.borderColor}
>
<Box display="flex" alignItems="center" justifyContent="between" mb={3}>
<Icon icon={level.icon} size={5} className={level.color} />
<Text size="2xl" weight="bold" className={level.color}>{level.count}</Text>
<Icon icon={level.icon} size={5} color={level.color} />
<Text size="2xl" weight="bold" color={level.color}>{level.count}</Text>
</Box>
<Text color="text-white" weight="medium" block mb={1}>{level.label}</Text>
<Box fullWidth h="2" rounded="full" bg="bg-deep-graphite/50" overflow="hidden">
<Text variant="high" weight="medium" block mb={1}>{level.label}</Text>
<Box fullWidth h="2" rounded="full" bg="var(--ui-color-bg-base)" overflow="hidden">
<Box
h="full"
rounded="full"
transition
className={
level.id === 'pro' ? 'bg-yellow-400' :
level.id === 'advanced' ? 'bg-purple-400' :
level.id === 'intermediate' ? 'bg-primary-blue' :
'bg-green-400'
}
bg={level.color}
style={{ width: `${level.percentage}%` }}
/>
</Box>
<Text size="xs" color="text-gray-500" block mt={1}>{level.percentage}% of drivers</Text>
<Text size="xs" variant="low" block mt={1}>{level.percentage}% of drivers</Text>
</Box>
);
})}

View File

@@ -3,8 +3,8 @@
import React, { Component, ReactNode, useState } from 'react';
import { ApiError } from '@/lib/api/base/ApiError';
import { connectionMonitor } from '@/lib/api/base/ApiConnectionMonitor';
import { ErrorDisplay } from '@/ui/ErrorDisplay';
import { DevErrorPanel } from '@/ui/DevErrorPanel';
import { ErrorDisplay } from '@/components/shared/ErrorDisplay';
import { DevErrorPanel } from '@/components/shared/DevErrorPanel';
interface Props {
children: ReactNode;

View File

@@ -3,8 +3,8 @@
import React, { Component, ReactNode, ErrorInfo, useState, version } from 'react';
import { ApiError } from '@/lib/api/base/ApiError';
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
import { DevErrorPanel } from '@/ui/DevErrorPanel';
import { ErrorDisplay } from '@/ui/ErrorDisplay';
import { DevErrorPanel } from '@/components/shared/DevErrorPanel';
import { ErrorDisplay } from '@/components/shared/ErrorDisplay';
interface Props {
children: ReactNode;

View File

@@ -6,7 +6,7 @@ import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Accordion } from '@/ui/Accordion';
import { Accordion } from '@/components/shared/Accordion';
import { Copy } from 'lucide-react';
import React, { useState } from 'react';

View File

@@ -6,7 +6,7 @@ import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Accordion } from '@/ui/Accordion';
import { Accordion } from '@/components/shared/Accordion';
import { Copy } from 'lucide-react';
import React, { useState } from 'react';

View File

@@ -2,7 +2,7 @@
import React from 'react';
import { ApiError } from '@/lib/api/base/ApiError';
import { ErrorDisplay as UiErrorDisplay } from '@/ui/ErrorDisplay';
import { ErrorDisplay as UiErrorDisplay } from '@/components/shared/ErrorDisplay';
interface ErrorDisplayProps {
error: ApiError;

View File

@@ -3,7 +3,7 @@
import { ErrorPageContainer } from '@/ui/ErrorPageContainer';
import { Glow } from '@/ui/Glow';
import { Text } from '@/ui/Text';
import { FooterSection } from '@/ui/FooterSection';
import { Group } from '@/ui/Group';
import { NotFoundActions } from './NotFoundActions';
import { NotFoundDiagnostics } from './NotFoundDiagnostics';
import { NotFoundHelpLinks } from './NotFoundHelpLinks';
@@ -42,40 +42,40 @@ export function NotFoundScreen({
{/* Background Glow Accent */}
<Glow color="primary" size="xl" opacity={0.1} position="center" />
<NotFoundDiagnostics errorCode={errorCode} />
<Text
as="h1"
size="4xl"
weight="bold"
variant="high"
uppercase
block
align="center"
style={{ marginTop: '1rem', marginBottom: '2rem' }}
>
{title}
</Text>
<Group direction="column" align="center" gap={8} fullWidth>
<NotFoundDiagnostics errorCode={errorCode} />
<Group direction="column" align="center" gap={4} fullWidth>
<Text
as="h1"
size="4xl"
weight="bold"
variant="high"
uppercase
block
align="center"
>
{title}
</Text>
<Text
size="xl"
variant="med"
block
weight="medium"
align="center"
style={{ marginBottom: '3rem' }}
>
{message}
</Text>
<Text
size="xl"
variant="med"
block
weight="medium"
align="center"
>
{message}
</Text>
</Group>
<NotFoundActions
primaryLabel={actionLabel}
onPrimaryClick={onActionClick}
/>
<NotFoundActions
primaryLabel={actionLabel}
onPrimaryClick={onActionClick}
/>
<FooterSection>
<NotFoundHelpLinks links={helpLinks} />
</FooterSection>
</Group>
</ErrorPageContainer>
);
}

View File

@@ -0,0 +1,57 @@
import React from 'react';
import { Section } from '@/ui/Section';
import { Container } from '@/ui/Container';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Accordion } from '@/components/shared/Accordion';
import { CardStack } from '@/ui/CardStack';
import { Center } from '@/ui/Center';
interface FAQItem {
question: string;
answer: string;
}
interface FAQSectionProps {
title: string;
subtitle: string;
faqs: FAQItem[];
}
/**
* FAQSection - A semantic component for the FAQ section.
*/
export function FAQSection({
title,
subtitle,
faqs,
}: FAQSectionProps) {
return (
<Section variant="dark" padding="lg">
<Container size="md">
<Center>
<CardStack gap={4}>
<Center>
<Text size="xs" weight="bold" variant="primary" uppercase letterSpacing="widest">
{subtitle}
</Text>
</Center>
<Heading level={2} weight="bold" align="center">
{title}
</Heading>
</CardStack>
</Center>
<CardStack gap={4}>
{faqs.map((faq, index) => (
<Accordion key={index} title={faq.question}>
<Text size="sm" variant="low" leading="relaxed">
{faq.answer}
</Text>
</Accordion>
))}
</CardStack>
</Container>
</Section>
);
}

View File

@@ -1,7 +1,9 @@
'use client';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { CardStack } from '@/ui/CardStack';
import { FeatureList } from '@/ui/FeatureList';
import { FeatureQuote } from '@/ui/FeatureQuote';
interface HomeFeatureDescriptionProps {
lead: string;
@@ -12,7 +14,7 @@ interface HomeFeatureDescriptionProps {
/**
* HomeFeatureDescription - A semantic component for feature descriptions on the home page.
* Refactored to use semantic HTML and Tailwind.
* Refactored to use semantic UI components.
*/
export function HomeFeatureDescription({
lead,
@@ -20,40 +22,21 @@ export function HomeFeatureDescription({
quote,
accentColor = 'primary',
}: HomeFeatureDescriptionProps) {
const borderColor = {
primary: 'primary-accent',
aqua: 'telemetry-aqua',
amber: 'warning-amber',
gray: 'border-gray',
}[accentColor];
const bgColor = {
primary: 'primary-accent/5',
aqua: 'telemetry-aqua/5',
amber: 'warning-amber/5',
gray: 'white/5',
}[accentColor];
const intent = accentColor === 'gray' ? 'low' : accentColor;
return (
<Stack gap={6}>
<Text size="lg" color="text-gray-400" weight="medium" leading="relaxed">
<CardStack gap={6}>
<Text size="lg" variant="med" leading="relaxed">
{lead}
</Text>
<Stack as="ul" gap={2}>
{items.map((item, index) => (
<Stack as="li" key={index} direction="row" align="start" gap={2}>
<Text color="text-primary-accent"></Text>
<Text size="sm" color="text-gray-500">{item}</Text>
</Stack>
))}
</Stack>
<FeatureList items={items} intent={intent} />
{quote && (
<Stack borderLeft borderStyle="solid" borderWidth="2px" borderColor={borderColor} pl={4} py={1} bg={bgColor}>
<Text color="text-gray-600" font="mono" size="xs" uppercase letterSpacing="widest" leading="relaxed">
{quote}
</Text>
</Stack>
<FeatureQuote intent={intent}>
{quote}
</FeatureQuote>
)}
</Stack>
</CardStack>
);
}

View File

@@ -1,13 +1,7 @@
'use client';
import React from 'react';
import { Panel } from '@/ui/Panel';
import { Glow } from '@/ui/Glow';
import { Stack } from '@/ui/Stack';
import { Grid } from '@/ui/Grid';
import { Container } from '@/ui/Container';
import { Heading } from '@/ui/Heading';
import { Section } from '@/ui/Section';
import { FeatureSection } from '@/ui/FeatureSection';
interface HomeFeatureSectionProps {
heading: string;
@@ -27,43 +21,13 @@ export function HomeFeatureSection({
layout,
accentColor = 'primary',
}: HomeFeatureSectionProps) {
const glowColor = ({
primary: 'primary',
aqua: 'aqua',
amber: 'amber',
}[accentColor] || 'primary') as 'primary' | 'aqua' | 'amber' | 'purple';
return (
<Section variant="dark" padding="lg">
<Glow
color={glowColor}
size="lg"
position={layout === 'text-left' ? 'bottom-left' : 'top-right'}
opacity={0.02}
/>
<Container>
<Grid cols={{ base: 1, lg: 2 }} gap={12}>
{/* Text Content */}
<Stack gap={8}>
<Stack gap={4}>
<Heading level={2} weight="bold">
{heading}
</Heading>
</Stack>
<Stack>
{description}
</Stack>
</Stack>
{/* Mockup Panel */}
<Panel variant="dark">
<Stack align="center" justify="center">
{mockup}
</Stack>
</Panel>
</Grid>
</Container>
</Section>
<FeatureSection
heading={heading}
description={description}
mockup={mockup}
layout={layout}
intent={accentColor}
/>
);
}

View File

@@ -1,109 +1,42 @@
'use client';
import React from 'react';
import { Button } from '@/ui/Button';
import { Glow } from '@/ui/Glow';
import { Icon } from '@/ui/Icon';
import { DiscordIcon } from '@/ui/icons/DiscordIcon';
import { Code, Lightbulb, LucideIcon, MessageSquare, Users } from 'lucide-react';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Grid } from '@/ui/Grid';
import { Card } from '@/ui/Card';
import { Section } from '@/ui/Section';
import { Container } from '@/ui/Container';
import { DiscordCTA } from '@/ui/DiscordCTA';
import { Code, Lightbulb, MessageSquare, Users } from 'lucide-react';
export function HomeFooterCTA() {
const discordUrl = process.env.NEXT_PUBLIC_DISCORD_URL || '#';
const benefits = [
{
icon: MessageSquare,
title: "Share your pain points",
description: "Tell us what frustrates you about league racing today."
},
{
icon: Lightbulb,
title: "Shape the product",
description: "Your ideas directly influence our roadmap."
},
{
icon: Users,
title: "Connect with racers",
description: "Join a community of like-minded competitive drivers."
},
{
icon: Code,
title: "Early Access",
description: "Test new features before they go public."
},
];
return (
<Section variant="dark" padding="lg">
<Glow color="primary" size="xl" position="center" opacity={0.05} />
<Container>
<Card variant="outline">
<Stack align="center" gap={12}>
{/* Header */}
<Stack align="center" gap={6}>
<DiscordIcon size={40} />
<Stack gap={4} align="center">
<Heading level={2} weight="bold" align="center">
Join the Grid on Discord
</Heading>
</Stack>
</Stack>
{/* Personal message */}
<Stack align="center" gap={6}>
<Text size="lg" variant="high" weight="medium" align="center">
GridPilot is a <Text as="span" variant="high" weight="bold">solo developer project</Text> built for the community.
</Text>
<Text size="base" variant="low" align="center">
We are in early alpha. Join us to help shape the future of motorsport infrastructure. Your feedback directly influences the roadmap.
</Text>
</Stack>
{/* Benefits grid */}
<Grid cols={{ base: 1, md: 2 }} gap={6}>
<BenefitItem
icon={MessageSquare}
title="Share your pain points"
description="Tell us what frustrates you about league racing today."
/>
<BenefitItem
icon={Lightbulb}
title="Shape the product"
description="Your ideas directly influence our roadmap."
/>
<BenefitItem
icon={Users}
title="Connect with racers"
description="Join a community of like-minded competitive drivers."
/>
<BenefitItem
icon={Code}
title="Early Access"
description="Test new features before they go public."
/>
</Grid>
{/* CTA Button */}
<Stack gap={6} align="center">
<Button
as="a"
href={discordUrl}
target="_blank"
rel="noopener noreferrer"
variant="primary"
size="lg"
icon={<DiscordIcon size={24} />}
>
Join Discord
</Button>
<Text size="xs" variant="primary" weight="bold" font="mono" uppercase>
Early Alpha Access Available
</Text>
</Stack>
</Stack>
</Card>
</Container>
</Section>
);
}
function BenefitItem({ icon, title, description }: { icon: LucideIcon, title: string, description: string }) {
return (
<Card variant="dark">
<Stack align="start" gap={5}>
<Icon icon={icon} size={5} intent="primary" />
<Stack gap={2}>
<Text size="base" weight="bold" variant="high">{title}</Text>
<Text size="sm" variant="low">{description}</Text>
</Stack>
</Stack>
</Card>
<DiscordCTA
title="Join the Grid on Discord"
lead="GridPilot is a solo developer project built for the community."
description="We are in early alpha. Join us to help shape the future of motorsport infrastructure. Your feedback directly influences the roadmap."
benefits={benefits}
discordUrl={discordUrl}
/>
);
}

View File

@@ -1,13 +1,6 @@
'use client';
import { Button } from '@/ui/Button';
import { Container } from '@/ui/Container';
import { Glow } from '@/ui/Glow';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Section } from '@/ui/Section';
import { ButtonGroup } from '@/ui/ButtonGroup';
import { LandingHero } from '@/ui/LandingHero';
interface HomeHeaderProps {
title: string;
@@ -26,51 +19,6 @@ interface HomeHeaderProps {
/**
* HomeHeader - Semantic hero section for the landing page.
*/
export function HomeHeader({
title,
subtitle,
description,
primaryAction,
secondaryAction,
}: HomeHeaderProps) {
return (
<Section variant="dark" padding="lg">
<Glow color="primary" size="xl" position="top-right" opacity={0.1} />
<Container>
<Stack gap={8}>
<Text size="xs" weight="bold" uppercase variant="primary">
{subtitle}
</Text>
<Heading level={1} weight="bold">
{title}
</Heading>
<Text size="lg" variant="low">
{description}
</Text>
<ButtonGroup gap={4}>
<Button
as="a"
href={primaryAction.href}
variant="primary"
size="lg"
>
{primaryAction.label}
</Button>
<Button
as="a"
href={secondaryAction.href}
variant="secondary"
size="lg"
>
{secondaryAction.label}
</Button>
</ButtonGroup>
</Stack>
</Container>
</Section>
);
export function HomeHeader(props: HomeHeaderProps) {
return <LandingHero {...props} />;
}

View File

@@ -1,43 +1,39 @@
'use client';
import React from 'react';
import { MetricCard } from '@/ui/MetricCard';
import { StatsStrip } from '@/ui/StatsStrip';
import { Activity, Users, Trophy, Calendar } from 'lucide-react';
import { Container } from '@/ui/Container';
import { Grid } from '@/ui/Grid';
/**
* HomeStatsStrip - A thin strip showing some status or quick info.
* Part of the "Telemetry-workspace" feel.
*/
export function HomeStatsStrip() {
return (
<Container>
<Grid cols={{ base: 2, md: 4 }} gap={4}>
<MetricCard
label="Active Drivers"
value="1,284"
icon={Users}
trend={{ value: 12, isPositive: true }}
/>
<MetricCard
label="Live Sessions"
value="42"
icon={Activity}
intent="telemetry"
/>
<MetricCard
label="Total Races"
value="15,402"
icon={Trophy}
intent="warning"
/>
<MetricCard
label="Next Event"
value="14:00"
icon={Calendar}
/>
</Grid>
</Container>
);
const stats = [
{
label: "Active Drivers",
value: "1,284",
icon: Users,
trend: { value: 12, isPositive: true }
},
{
label: "Live Sessions",
value: "42",
icon: Activity,
intent: "telemetry" as const
},
{
label: "Total Races",
value: "15,402",
icon: Trophy,
intent: "warning" as const
},
{
label: "Next Event",
value: "14:00",
icon: Calendar
},
];
return <StatsStrip stats={stats} />;
}

View File

@@ -2,9 +2,12 @@
import { LeagueCard } from '@/components/leagues/LeagueCard';
import { routes } from '@/lib/routing/RouteConfig';
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';
import { Link } from '@/ui/Link';
import { Panel } from '@/ui/Panel';
import { Box } from '@/ui/Box';
import { CardStack } from '@/ui/CardStack';
interface League {
id: string;
@@ -20,26 +23,26 @@ interface LeagueSummaryPanelProps {
* LeagueSummaryPanel - Semantic section for featured leagues.
*/
export function LeagueSummaryPanel({ leagues }: LeagueSummaryPanelProps) {
return (
<Box as="section" bg="surface-charcoal" p={6} border borderColor="border-gray" rounded="none">
<Box display="flex" alignItems="center" justifyContent="between" mb={6}>
<Heading level={3} fontSize="xs" weight="bold" letterSpacing="widest" color="text-white">
FEATURED LEAGUES
</Heading>
<Link
href={routes.public.leagues}
size="xs"
weight="bold"
letterSpacing="widest"
variant="primary"
hoverColor="text-white"
transition
>
VIEW ALL
</Link>
</Box>
const actions = (
<Link
href={routes.public.leagues}
size="xs"
weight="bold"
letterSpacing="widest"
variant="primary"
>
VIEW ALL
</Link>
);
<Box display="flex" flexDirection="col" gap={4}>
return (
<Panel
variant="dark"
padding={6}
title="FEATURED LEAGUES"
actions={actions}
>
<CardStack gap={4}>
{leagues.slice(0, 2).map((league) => (
<LeagueCard
key={league.id}
@@ -54,7 +57,7 @@ export function LeagueSummaryPanel({ leagues }: LeagueSummaryPanelProps) {
openSlotsCount={6}
/>
))}
</Box>
</Box>
</CardStack>
</Panel>
);
}

View File

@@ -5,9 +5,13 @@ import { routes } from '@/lib/routing/RouteConfig';
import { Heading } from '@/ui/Heading';
import { Link } from '@/ui/Link';
import { Panel } from '@/ui/Panel';
import { Stack } from '@/ui/Stack';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { CardStack } from '@/ui/CardStack';
import { Center } from '@/ui/Center';
interface Race {
id: string;
track: string;
@@ -23,33 +27,33 @@ interface RecentRacesPanelProps {
* RecentRacesPanel - Semantic section for upcoming/recent races.
*/
export function RecentRacesPanel({ races }: RecentRacesPanelProps) {
return (
<Panel variant="dark" padding={6}>
<Stack direction="row" align="center" justify="between" mb={6}>
<Heading level={3} fontSize="xs" weight="bold" letterSpacing="widest" color="text-white">
UPCOMING RACES
</Heading>
<Link
href={routes.public.races}
size="xs"
weight="bold"
letterSpacing="widest"
variant="primary"
hoverColor="text-white"
transition
>
FULL SCHEDULE
</Link>
</Stack>
const actions = (
<Link
href={routes.public.races}
size="xs"
weight="bold"
letterSpacing="widest"
variant="primary"
>
FULL SCHEDULE
</Link>
);
<Stack gap={3}>
return (
<Panel
variant="dark"
padding={6}
title="UPCOMING RACES"
actions={actions}
>
<CardStack gap={3}>
{races.length === 0 ? (
<Panel variant="muted" padding={12} border>
<Stack center>
<Text size="xs" font="mono" uppercase letterSpacing="widest" color="text-gray-600">
<Center>
<Text size="xs" font="mono" uppercase letterSpacing="widest" variant="low">
No races scheduled
</Text>
</Stack>
</Center>
</Panel>
) : (
races.slice(0, 3).map((race) => (
@@ -63,7 +67,7 @@ export function RecentRacesPanel({ races }: RecentRacesPanelProps) {
/>
))
)}
</Stack>
</CardStack>
</Panel>
);
}

View File

@@ -2,9 +2,12 @@
import { TeamCard } from '@/components/teams/TeamCard';
import { routes } from '@/lib/routing/RouteConfig';
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';
import { Link } from '@/ui/Link';
import { Panel } from '@/ui/Panel';
import { Box } from '@/ui/Box';
import { CardStack } from '@/ui/CardStack';
interface Team {
id: string;
@@ -21,26 +24,26 @@ interface TeamSummaryPanelProps {
* TeamSummaryPanel - Semantic section for teams.
*/
export function TeamSummaryPanel({ teams }: TeamSummaryPanelProps) {
return (
<Box as="section" bg="surface-charcoal" p={6} border borderColor="border-gray" rounded="none">
<Box display="flex" alignItems="center" justifyContent="between" mb={6}>
<Heading level={3} fontSize="xs" weight="bold" letterSpacing="widest" color="text-white">
TEAMS ON THE GRID
</Heading>
<Link
href={routes.public.teams}
size="xs"
weight="bold"
letterSpacing="widest"
variant="primary"
hoverColor="text-white"
transition
>
BROWSE TEAMS
</Link>
</Box>
const actions = (
<Link
href={routes.public.teams}
size="xs"
weight="bold"
letterSpacing="widest"
variant="primary"
>
BROWSE TEAMS
</Link>
);
<Box display="flex" flexDirection="col" gap={4}>
return (
<Panel
variant="dark"
padding={6}
title="TEAMS ON THE GRID"
actions={actions}
>
<CardStack gap={4}>
{teams.slice(0, 2).map((team) => (
<TeamCard
key={team.id}
@@ -51,7 +54,7 @@ export function TeamSummaryPanel({ teams }: TeamSummaryPanelProps) {
isRecruiting={true}
/>
))}
</Box>
</Box>
</CardStack>
</Panel>
);
}

View File

@@ -44,8 +44,13 @@ export function BenefitCard({
rounded="xl"
border={true}
padding={6}
className={`relative h-full transition-all duration-300 group ${isHighlight ? 'border-primary-blue/30' : 'border-charcoal-outline hover:border-charcoal-outline/80'}`}
style={isHighlight ? { background: 'linear-gradient(to bottom right, rgba(25, 140, 255, 0.1), rgba(25, 140, 255, 0.05))' } : {}}
borderColor={isHighlight ? 'rgba(25, 140, 255, 0.3)' : 'var(--ui-color-border-low)'}
hoverBorderColor={isHighlight ? 'rgba(25, 140, 255, 0.5)' : 'var(--ui-color-border-default)'}
transition="all 0.3s ease"
group
position="relative"
fullHeight
bg={isHighlight ? 'linear-gradient(to bottom right, rgba(25, 140, 255, 0.1), rgba(25, 140, 255, 0.05))' : undefined}
>
{/* Icon */}
<Box
@@ -55,25 +60,25 @@ export function BenefitCard({
display="flex"
center
mb={4}
bg={isHighlight ? 'bg-primary-blue/20' : 'bg-iron-gray'}
bg={isHighlight ? 'rgba(25, 140, 255, 0.2)' : 'var(--ui-color-bg-surface-muted)'}
border={!isHighlight}
borderColor="border-charcoal-outline"
borderColor="var(--ui-color-border-low)"
>
<Icon icon={icon} size={6} className={isHighlight ? 'text-primary-blue' : 'text-gray-400'} />
<Icon icon={icon} size={6} intent={isHighlight ? 'primary' : 'low'} />
</Box>
{/* Content */}
<Heading level={3} mb={2}>{title}</Heading>
<Text size="sm" color="text-gray-400" block style={{ lineHeight: 1.625 }}>{description}</Text>
<Text size="sm" variant="low" block leading="relaxed">{description}</Text>
{/* Stats */}
{stats && (
<Box mt={4} pt={4} borderTop={true} borderColor="border-charcoal-outline/50">
<Box mt={4} pt={4} borderTop={true} borderColor="var(--ui-color-border-low)">
<Box display="flex" alignItems="baseline" gap={2}>
<Text size="2xl" weight="bold" color={isHighlight ? 'text-primary-blue' : 'text-white'}>
<Text size="2xl" weight="bold" variant={isHighlight ? 'primary' : 'high'}>
{stats.value}
</Text>
<Text size="sm" color="text-gray-500">{stats.label}</Text>
<Text size="sm" variant="low">{stats.label}</Text>
</Box>
</Box>
)}
@@ -84,7 +89,11 @@ export function BenefitCard({
position="absolute"
inset="0"
rounded="xl"
className="bg-gradient-to-br from-primary-blue/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"
bg="linear-gradient(to bottom right, rgba(25, 140, 255, 0.2), transparent)"
opacity={0}
groupHoverOpacity={1}
transition="opacity 0.3s ease"
pointerEvents="none"
/>
)}
</Surface>

View File

@@ -24,11 +24,11 @@ export function DiscoverySection({ viewData }: DiscoverySectionProps) {
<Stack gap={16}>
<Stack maxWidth="2xl">
<Stack borderLeft borderStyle="solid" borderColor="primary-accent" pl={4} mb={4}>
<Text size="xs" weight="bold" color="text-primary-accent" className="uppercase tracking-[0.2em]">
<Text size="xs" weight="bold" variant="primary" uppercase letterSpacing="0.2em">
Live Ecosystem
</Text>
</Stack>
<Heading level={2} weight="bold" fontSize={{ base: '3xl', md: '4xl' }} className="tracking-tight">
<Heading level={2} weight="bold" fontSize={{ base: '3xl', md: '4xl' }} letterSpacing="-0.025em">
Discover the Grid
</Heading>
<Text size="lg" color="text-gray-400" block mt={6} leading="relaxed">
@@ -39,10 +39,10 @@ export function DiscoverySection({ viewData }: DiscoverySectionProps) {
<Grid cols={1} lgCols={3} gap={12}>
{/* Top leagues */}
<Stack gap={8}>
<Stack direction="row" align="center" justify="between" className="border-b border-border-gray/30 pb-4">
<Heading level={5} color="text-gray-400" weight="bold" className="tracking-widest">FEATURED LEAGUES</Heading>
<Stack direction="row" align="center" justify="between" borderBottom borderColor="var(--ui-color-border-low)" pb={4}>
<Heading level={5} color="var(--ui-color-text-low)" weight="bold" letterSpacing="widest" uppercase>FEATURED LEAGUES</Heading>
<Link href={routes.public.leagues}>
<Text size="xs" weight="bold" color="text-primary-accent" className="uppercase tracking-widest hover:text-white transition-colors">View all</Text>
<Text size="xs" weight="bold" variant="primary" uppercase letterSpacing="widest" hoverVariant="high">View all</Text>
</Link>
</Stack>
<Stack gap={4}>
@@ -65,10 +65,10 @@ export function DiscoverySection({ viewData }: DiscoverySectionProps) {
{/* Teams */}
<Stack gap={8}>
<Stack direction="row" align="center" justify="between" className="border-b border-border-gray/30 pb-4">
<Heading level={5} color="text-gray-400" weight="bold" className="tracking-widest">TEAMS ON THE GRID</Heading>
<Stack direction="row" align="center" justify="between" borderBottom borderColor="var(--ui-color-border-low)" pb={4}>
<Heading level={5} color="var(--ui-color-text-low)" weight="bold" letterSpacing="widest" uppercase>TEAMS ON THE GRID</Heading>
<Link href={routes.public.teams}>
<Text size="xs" weight="bold" color="text-primary-accent" className="uppercase tracking-widest hover:text-white transition-colors">Browse</Text>
<Text size="xs" weight="bold" variant="primary" uppercase letterSpacing="widest" hoverVariant="high">Browse</Text>
</Link>
</Stack>
<Stack gap={4}>
@@ -87,10 +87,10 @@ export function DiscoverySection({ viewData }: DiscoverySectionProps) {
{/* Upcoming races */}
<Stack gap={8}>
<Stack direction="row" align="center" justify="between" className="border-b border-border-gray/30 pb-4">
<Heading level={5} color="text-gray-400" weight="bold" className="tracking-widest">UPCOMING RACES</Heading>
<Stack direction="row" align="center" justify="between" borderBottom borderColor="var(--ui-color-border-low)" pb={4}>
<Heading level={5} color="var(--ui-color-text-low)" weight="bold" letterSpacing="widest" uppercase>UPCOMING RACES</Heading>
<Link href={routes.public.races}>
<Text size="xs" weight="bold" color="text-primary-accent" className="uppercase tracking-widest hover:text-white transition-colors">Schedule</Text>
<Text size="xs" weight="bold" variant="primary" uppercase letterSpacing="widest" hoverVariant="high">Schedule</Text>
</Link>
</Stack>
{viewData.upcomingRaces.length === 0 ? (

View File

@@ -1,12 +1,6 @@
'use client';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { motion } from 'framer-motion';
import { ChevronDown } from 'lucide-react';
import { useState } from 'react';
import { FAQSection } from '@/components/home/FAQSection';
const faqs = [
{
@@ -35,103 +29,12 @@ const faqs = [
}
];
function FAQItem({ faq, index }: { faq: typeof faqs[0]; index: number }) {
const [isOpen, setIsOpen] = useState(false);
return (
<Stack
as={motion.div}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1 }}
group
>
<Stack rounded="none" bg="panel-gray/40" border borderColor="border-gray/50" transition hoverBorderColor="primary-accent/30">
<Stack
as="button"
onClick={() => setIsOpen(!isOpen)}
fullWidth
p={{ base: 4, md: 6 }}
textAlign="left"
rounded="none"
minHeight="44px"
className="relative overflow-hidden"
>
<Stack display="flex" alignItems="center" justifyContent="between" gap={{ base: 2, md: 4 }}>
<Stack direction="row" align="center" gap={4}>
<Stack w="1" h="4" bg={isOpen ? "primary-accent" : "border-gray"} transition className="group-hover:bg-primary-accent" />
<Heading level={3} fontSize={{ base: 'sm', md: 'base' }} weight="bold" color="text-white" groupHoverColor="primary-accent" transition className="tracking-wide">
{faq.question}
</Heading>
</Stack>
<Stack
as={motion.div}
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.15, ease: 'easeInOut' }}
w={{ base: "4", md: "5" }}
h={{ base: "4", md: "5" }}
color={isOpen ? "text-primary-accent" : "text-gray-500"}
flexShrink={0}
>
<Icon icon={ChevronDown} size="full" />
</Stack>
</Stack>
</Stack>
<Stack
as={motion.div}
initial={false}
animate={{
height: isOpen ? 'auto' : 0,
opacity: isOpen ? 1 : 0
}}
transition={{
height: { duration: 0.2, ease: 'easeInOut' },
opacity: { duration: 0.15, ease: 'easeInOut' }
}}
overflow="hidden"
>
<Stack px={{ base: 4, md: 6 }} pb={{ base: 4, md: 6 }} pt={0} ml={5}>
<Text size="sm" color="text-gray-400" weight="normal" leading="relaxed" className="max-w-2xl">
{faq.answer}
</Text>
</Stack>
</Stack>
</Stack>
</Stack>
);
}
export function FAQ() {
return (
<Stack as="section" position="relative" py={{ base: 20, md: 32 }} bg="graphite-black" overflow="hidden" borderBottom borderColor="border-gray/50">
{/* Background image with mask */}
<Stack
position="absolute"
inset="0"
bg="url(/images/porsche.jpeg)"
backgroundSize="cover"
backgroundPosition="center"
opacity={0.03}
/>
<Stack maxWidth="4xl" mx="auto" px={{ base: 4, md: 6 }} position="relative" zIndex={10}>
<Stack textAlign="center" mb={{ base: 12, md: 16 }}>
<Stack borderLeft borderRight borderStyle="solid" borderColor="primary-accent" px={4} display="inline-block" mb={4}>
<Text size="xs" weight="bold" color="text-primary-accent" className="uppercase tracking-[0.2em]">
Support & Information
</Text>
</Stack>
<Heading level={2} fontSize={{ base: '3xl', md: '4xl' }} weight="bold" color="text-white" mb={4} className="tracking-tight">
Frequently Asked Questions
</Heading>
</Stack>
<Stack display="flex" flexDirection="column" gap={4}>
{faqs.map((faq, index) => (
<FAQItem key={faq.question} faq={faq} index={index} />
))}
</Stack>
</Stack>
</Stack>
<FAQSection
title="Frequently Asked Questions"
subtitle="Support & Information"
faqs={faqs}
/>
);
}
}

View File

@@ -12,6 +12,9 @@ import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/Stack';
import { Section } from '@/ui/Section';
import { Text } from '@/ui/Text';
import { Surface } from '@/ui/Surface';
import { Box } from '@/ui/Box';
import { Grid } from '@/ui/Grid';
const features = [
{
@@ -48,68 +51,79 @@ const features = [
function FeatureCard({ feature, index }: { feature: typeof features[0], index: number }) {
return (
<Stack
display="flex"
flexDirection="column"
gap={6}
className="p-8 bg-panel-gray/20 border border-border-gray/20 rounded-none hover:border-primary-accent/20 transition-all duration-300 ease-smooth group relative overflow-hidden"
<Surface
variant="muted"
padding={8}
rounded="none"
border
borderColor="var(--ui-color-border-low)"
hoverBorderColor="var(--ui-color-intent-primary)"
transition="all 0.3s ease"
group
position="relative"
overflow="hidden"
>
<Stack aspectRatio="video" fullWidth position="relative" className="bg-graphite-black rounded-none overflow-hidden border border-border-gray/20">
<Stack aspectRatio="video" fullWidth position="relative" bg="var(--ui-color-bg-base)" rounded="none" overflow="hidden" border borderColor="var(--ui-color-border-low)">
<MockupStack index={index}>
<feature.MockupComponent />
</MockupStack>
</Stack>
<Stack gap={4}>
<Stack display="flex" alignItems="center" gap={3}>
<Stack w="1" h="3" bg="primary-accent" />
<Heading level={3} weight="bold" fontSize="lg" className="tracking-tighter uppercase">
<Stack gap={4} mt={6}>
<Stack direction="row" align="center" gap={3}>
<Box w="1" h="3" bg="var(--ui-color-intent-primary)" />
<Heading level={3} weight="bold" fontSize="lg" letterSpacing="-0.05em" uppercase>
{feature.title}
</Heading>
</Stack>
<Text size="sm" color="text-gray-500" leading="relaxed" weight="normal" className="group-hover:text-gray-400 transition-colors">
<Text size="sm" variant="low" leading="relaxed" weight="normal" groupHoverTextColor="var(--ui-color-text-med)">
{feature.description}
</Text>
</Stack>
{/* Subtle hover effect */}
<Stack
<Box
position="absolute"
bottom="0"
left="0"
w="full"
h="0.5"
bg="primary-accent"
className="scale-x-0 group-hover:scale-x-100 transition-transform duration-500 origin-left"
bg="var(--ui-color-intent-primary)"
transform="scaleX(0)"
groupHoverScale={true}
transition="transform 0.5s ease"
style={{ transformOrigin: 'left' }}
/>
</Stack>
</Surface>
);
}
export function FeatureGrid() {
return (
<Section className="bg-graphite-black border-b border-border-gray/20 py-32">
<Container position="relative" zIndex={10}>
<Box borderBottom borderColor="var(--ui-color-border-low)">
<Section variant="dark" py={32}>
<Container position="relative" zIndex={10}>
<Stack gap={16}>
<Stack maxWidth="2xl">
<Stack borderLeft borderStyle="solid" borderColor="primary-accent" pl={4} mb={4} bg="primary-accent/5" py={1}>
<Text size="xs" weight="bold" color="text-primary-accent" className="uppercase tracking-[0.3em]">
<Stack borderLeft borderStyle="solid" borderColor="var(--ui-color-intent-primary)" pl={4} mb={4} bg="rgba(25, 140, 255, 0.05)" py={1}>
<Text size="xs" weight="bold" variant="primary" uppercase letterSpacing="0.3em">
Engineered for Competition
</Text>
</Stack>
<Heading level={2} weight="bold" fontSize={{ base: '3xl', md: '5xl' }} className="tracking-tighter uppercase leading-none">
<Heading level={2} weight="bold" fontSize={{ base: '3xl', md: '5xl' }} letterSpacing="-0.05em" uppercase>
Building for League Racing
</Heading>
<Text size="lg" color="text-gray-500" block mt={6} leading="relaxed" className="border-l border-border-gray/20 pl-6">
<Text size="lg" variant="low" block mt={6} leading="relaxed" borderLeft borderColor="var(--ui-color-border-low)" pl={6}>
Every feature is designed to reduce friction and increase immersion. Join our Discord to help shape the future of the platform.
</Text>
</Stack>
<Stack display="grid" gridCols={{ base: 1, md: 2, lg: 3 }} gap={6}>
<Grid cols={{ base: 1, md: 2, lg: 3 }} gap={6}>
{features.map((feature, index) => (
<FeatureCard key={feature.title} feature={feature} index={index} />
))}
</Stack>
</Grid>
</Stack>
</Container>
</Section>
</Section>
</Box>
);
}

View File

@@ -19,10 +19,11 @@ export function LandingHero() {
ref={sectionRef}
position="relative"
overflow="hidden"
bg="graphite-black"
bg="var(--ui-color-bg-base)"
pt={{ base: 24, md: 32, lg: 40 }}
pb={{ base: 16, md: 24, lg: 32 }}
className="border-b border-border-gray"
borderBottom
borderColor="var(--ui-color-border-low)"
>
{/* Background image layer with parallax */}
<Stack
@@ -54,8 +55,8 @@ export function LandingHero() {
<Container size="lg" position="relative" zIndex={10}>
<Stack gap={{ base: 8, md: 12 }}>
<Stack gap={6} maxWidth="3xl">
<Stack borderLeft borderStyle="solid" borderColor="primary-accent" pl={4} mb={2} bg="primary-accent/5" py={1}>
<Text size="xs" weight="bold" color="text-primary-accent" uppercase letterSpacing="0.3em">
<Stack borderLeft borderStyle="solid" borderColor="var(--ui-color-intent-primary)" pl={4} mb={2} bg="rgba(25, 140, 255, 0.05)" py={1}>
<Text size="xs" weight="bold" variant="primary" uppercase letterSpacing="0.3em">
Precision Racing Infrastructure
</Text>
</Stack>
@@ -63,13 +64,13 @@ export function LandingHero() {
level={1}
fontSize={{ base: '4xl', sm: '5xl', md: '6xl', lg: '8xl' }}
weight="bold"
color="text-white"
color="var(--ui-color-text-high)"
lineHeight="0.95"
letterSpacing="tighter"
>
MODERN MOTORSPORT INFRASTRUCTURE.
</Heading>
<Text size={{ base: 'lg', md: 'xl' }} color="text-gray-400" weight="normal" leading="relaxed" maxWidth="2xl" borderLeft borderStyle="solid" borderColor="border-gray" pl={6} opacity={0.3}>
<Text size={{ base: 'lg', md: 'xl' }} variant="low" weight="normal" leading="relaxed" maxWidth="2xl" borderLeft borderStyle="solid" borderColor="var(--ui-color-border-low)" pl={6} opacity={0.3}>
GridPilot gives your league racing a real home. Results, standings, teams, and career progression engineered for precision and control.
</Text>
</Stack>
@@ -91,8 +92,8 @@ export function LandingHero() {
variant="secondary"
size="lg"
px={12}
borderColor="border-gray"
hoverBorderColor="primary-accent/50"
borderColor="var(--ui-color-border-low)"
hoverBorderColor="var(--ui-color-intent-primary)"
letterSpacing="0.2em"
fontSize="xs"
h="14"
@@ -110,24 +111,24 @@ export function LandingHero() {
mt={12}
borderTop
borderStyle="solid"
borderColor="border-gray"
borderColor="var(--ui-color-border-low)"
opacity={0.2}
pt={12}
>
{[
{ label: 'IDENTITY', text: 'Your racing career in one place', color: 'primary' },
{ label: 'AUTOMATION', text: 'No more manual session setup', color: 'aqua' },
{ label: 'PRECISION', text: 'Real-time results and standings', color: 'amber' },
{ label: 'COMMUNITY', text: 'Built for teams and leagues', color: 'green' }
{ label: 'IDENTITY', text: 'Your racing career in one place', color: 'var(--ui-color-intent-primary)' },
{ label: 'AUTOMATION', text: 'No more manual session setup', color: 'var(--ui-color-intent-telemetry)' },
{ label: 'PRECISION', text: 'Real-time results and standings', color: 'var(--ui-color-intent-warning)' },
{ label: 'COMMUNITY', text: 'Built for teams and leagues', color: 'var(--ui-color-intent-success)' }
].map((item) => (
<Stack key={item.label} gap={3} group cursor="default">
<Stack display="flex" alignItems="center" gap={3}>
<Stack w="1" h="3" bg={item.color === 'primary' ? 'primary-accent' : item.color === 'aqua' ? 'telemetry-aqua' : item.color === 'amber' ? 'warning-amber' : 'success-green'} />
<Text size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="0.2em" groupHoverTextColor="white" transition>
<Stack w="1" h="3" bg={item.color} />
<Text size="xs" weight="bold" variant="low" uppercase letterSpacing="0.2em" groupHoverTextColor="var(--ui-color-text-high)" transition>
{item.label}
</Text>
</Stack>
<Text size="sm" color="text-gray-500" groupHoverTextColor="gray-300" transition leading="relaxed">
<Text size="sm" variant="low" groupHoverTextColor="var(--ui-color-text-med)" transition leading="relaxed">
{item.text}
</Text>
</Stack>

View File

@@ -15,6 +15,7 @@ export function AuthedNav({ pathname, direction = 'col' }: AuthedNavProps) {
const items = [
{ label: 'Dashboard', href: routes.protected.dashboard, icon: Home },
{ label: 'Leagues', href: routes.public.leagues, icon: Trophy },
{ label: 'Drivers', href: routes.public.drivers, icon: Users },
{ label: 'Leaderboards', href: routes.public.leaderboards, icon: Layout },
{ label: 'Teams', href: routes.public.teams, icon: Users },
{ label: 'Races', href: routes.public.races, icon: Calendar },

View File

@@ -15,6 +15,7 @@ export function PublicNav({ pathname, direction = 'col' }: PublicNavProps) {
const items = [
{ label: 'Home', href: routes.public.home, icon: Home },
{ label: 'Leagues', href: routes.public.leagues, icon: Trophy },
{ label: 'Drivers', href: routes.public.drivers, icon: Users },
{ label: 'Leaderboards', href: routes.public.leaderboards, icon: Layout },
{ label: 'Teams', href: routes.public.teams, icon: Users },
{ label: 'Races', href: routes.public.races, icon: Calendar },

View File

@@ -1,7 +1,7 @@
'use client';
import { InfoBanner } from '@/ui/InfoBanner';
import { Modal } from '@/ui/Modal';
import { Modal } from '@/components/shared/Modal';
import { ModalIcon } from '@/ui/ModalIcon';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';

View File

@@ -5,7 +5,7 @@ import { useEffectiveDriverId } from "@/hooks/useEffectiveDriverId";
import { getMembership } from '@/lib/leagueMembership';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Modal } from '@/ui/Modal';
import { Modal } from '@/components/shared/Modal';
import { Text } from '@/ui/Text';
import { useState } from 'react';

View File

@@ -4,7 +4,7 @@ import { Text } from '@/ui/Text';
interface JoinRequestItemProps {
driverId: string;
formattedRequestedAt: string;
requestedAt: string;
onApprove: () => void;
onReject: () => void;
isApproving?: boolean;
@@ -13,7 +13,7 @@ interface JoinRequestItemProps {
export function JoinRequestItem({
driverId,
formattedRequestedAt,
requestedAt,
onApprove,
onReject,
isApproving,
@@ -47,7 +47,7 @@ export function JoinRequestItem({
<Stack flexGrow={1}>
<Text color="text-white" weight="medium" block>{driverId}</Text>
<Text size="sm" color="text-gray-400" block>
Requested {formattedRequestedAt}
Requested {requestedAt}
</Text>
</Stack>
</Stack>

View File

@@ -8,7 +8,7 @@ import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { InfoFlyout } from '@/ui/InfoFlyout';
import { InfoFlyout } from '@/components/shared/InfoFlyout';
import { Stepper } from '@/ui/Stepper';
import { Button } from '@/ui/Button';
import { IconButton } from '@/ui/IconButton';

View File

@@ -4,7 +4,7 @@ import { useMemo, useState } from "react";
import { usePenaltyTypesReference } from "@/hooks/usePenaltyTypesReference";
import type { PenaltyValueKindDTO } from "@/lib/types/PenaltyTypesReferenceDTO";
import { ProtestViewModel } from "../../lib/view-models/ProtestViewModel";
import { Modal } from "@/ui/Modal";
import { Modal } from "@/components/shared/Modal";
import { Button } from "@/ui/Button";
import { Card } from "@/ui/Card";
import { Stack } from "@/ui/Stack";

View File

@@ -2,7 +2,7 @@
import { IconButton } from '@/ui/IconButton';
import { Image } from '@/ui/Image';
import { Modal } from '@/ui/Modal';
import { Modal } from '@/components/shared/Modal';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { ChevronLeft, ChevronRight, Download } from 'lucide-react';

View File

@@ -1,7 +1,8 @@
import { motion, useReducedMotion } from 'framer-motion';
import { ReactNode, useEffect, useState } from 'react';
import { Box } from '@/ui/Box';
import { Surface } from '@/ui/Surface';
interface MockupStackProps {
children: ReactNode;
@@ -28,9 +29,13 @@ export function MockupStack({ children, index = 0 }: MockupStackProps) {
// On mobile or before mount, render without animations
if (!isMounted || isMobile) {
return (
<div className="relative w-full h-full scale-60 sm:scale-70 md:scale-85 lg:scale-95 max-w-[85vw] mx-auto my-4 sm:my-0" style={{ perspective: '1200px' }}>
<div
className="absolute rounded-none bg-panel-gray/80 border border-border-gray/50"
<Box position="relative" fullWidth fullHeight maxWidth="85vw" marginX="auto" marginY={{ base: 4, sm: 0 }} style={{ perspective: '1200px', transform: 'scale(var(--mockup-scale, 1))' }}>
<Surface
variant="muted"
position="absolute"
rounded="none"
border
borderColor="var(--ui-color-border-low)"
style={{
rotate: `${rotation1}deg`,
zIndex: 1,
@@ -43,8 +48,12 @@ export function MockupStack({ children, index = 0 }: MockupStackProps) {
}}
/>
<div
className="absolute rounded-none bg-panel-gray/90 border border-border-gray/50"
<Surface
variant="muted"
position="absolute"
rounded="none"
border
borderColor="var(--ui-color-border-low)"
style={{
rotate: `${rotation2}deg`,
zIndex: 2,
@@ -57,24 +66,34 @@ export function MockupStack({ children, index = 0 }: MockupStackProps) {
}}
/>
<div
className="relative z-10 w-full h-full rounded-none overflow-hidden border border-border-gray/30"
<Box
position="relative"
zIndex={10}
fullWidth
fullHeight
rounded="none"
overflow="hidden"
border
borderColor="var(--ui-color-border-low)"
style={{
boxShadow: '0 20px 60px rgba(0,0,0,0.45)',
}}
>
{children}
</div>
</div>
</Box>
</Box>
);
}
// Desktop: render with animations
return (
<div className="relative w-full h-full scale-60 sm:scale-70 md:scale-85 lg:scale-95 max-w-[85vw] mx-auto my-4 sm:my-0" style={{ perspective: '1200px' }}>
<Box position="relative" fullWidth fullHeight maxWidth="85vw" marginX="auto" marginY={{ base: 4, sm: 0 }} style={{ perspective: '1200px', transform: 'scale(var(--mockup-scale, 1))' }}>
<motion.div
className="absolute rounded-none bg-panel-gray/80 border border-border-gray/50"
style={{
position: 'absolute',
borderRadius: '0',
backgroundColor: 'rgba(20, 22, 25, 0.8)',
border: '1px solid rgba(35, 39, 43, 0.5)',
rotate: `${rotation1}deg`,
zIndex: 1,
top: '-8px',
@@ -89,8 +108,11 @@ export function MockupStack({ children, index = 0 }: MockupStackProps) {
/>
<motion.div
className="absolute rounded-none bg-panel-gray/90 border border-border-gray/50"
style={{
position: 'absolute',
borderRadius: '0',
backgroundColor: 'rgba(20, 22, 25, 0.9)',
border: '1px solid rgba(35, 39, 43, 0.5)',
rotate: `${rotation2}deg`,
zIndex: 2,
top: '-4px',
@@ -105,8 +127,14 @@ export function MockupStack({ children, index = 0 }: MockupStackProps) {
/>
<motion.div
className="relative z-10 w-full h-full rounded-none overflow-hidden border border-border-gray/30"
style={{
position: 'relative',
zIndex: 10,
width: '100%',
height: '100%',
borderRadius: '0',
overflow: 'hidden',
border: '1px solid rgba(35, 39, 43, 0.3)',
boxShadow: '0 20px 60px rgba(0,0,0,0.45)',
}}
whileHover={
@@ -129,7 +157,12 @@ export function MockupStack({ children, index = 0 }: MockupStackProps) {
transition={{ duration: 0.4, delay: 0.2 }}
>
<motion.div
className="absolute inset-0 pointer-events-none rounded-none"
style={{
position: 'absolute',
inset: 0,
pointerEvents: 'none',
borderRadius: '0',
}}
whileHover={
shouldReduceMotion
? {}
@@ -141,6 +174,6 @@ export function MockupStack({ children, index = 0 }: MockupStackProps) {
/>
{children}
</motion.div>
</div>
</Box>
);
}
}

View File

@@ -35,18 +35,9 @@ export function ProtestWorkflowMockup() {
},
];
const getStatusStyles = (status: string) => {
switch (status) {
case 'pending': return 'bg-panel-gray border-gray-700 text-gray-600';
case 'active': return 'bg-warning-amber/10 border-warning-amber text-warning-amber';
case 'resolved': return 'bg-success-green/10 border-success-green text-success-green';
default: return 'bg-panel-gray border-gray-700 text-gray-600';
}
};
if (isMobile) {
return (
<Box position="relative" fullWidth fullHeight bg="graphite-black" rounded="none" overflow="hidden" p={3} display="flex" flexDirection="col" justifyContent="center" gap={4}>
<Box position="relative" fullWidth fullHeight bg="var(--ui-color-bg-base)" rounded="none" overflow="hidden" p={3} display="flex" flexDirection="col" justifyContent="center" gap={4}>
<Box display="flex" alignItems="center" justifyContent="center" gap={3}>
{steps.map((step, i) => (
<Box key={step.name} display="flex" alignItems="center">
@@ -61,17 +52,19 @@ export function ProtestWorkflowMockup() {
mb={1}
border
borderWidth="1px"
className={getStatusStyles(step.status)}
bg={step.status === 'pending' ? 'var(--ui-color-bg-surface)' : step.status === 'active' ? 'rgba(255, 190, 77, 0.1)' : 'rgba(16, 185, 129, 0.1)'}
borderColor={step.status === 'pending' ? 'var(--ui-color-border-default)' : step.status === 'active' ? 'var(--ui-color-intent-warning)' : 'var(--ui-color-intent-success)'}
color={step.status === 'pending' ? 'var(--ui-color-text-low)' : step.status === 'active' ? 'var(--ui-color-intent-warning)' : 'var(--ui-color-intent-success)'}
>
<Box as="svg" w="5" h="5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={step.icon} />
</Box>
</Box>
<Text size="xs" color="text-white" weight="bold" textAlign="center" className="uppercase tracking-widest">{step.name}</Text>
<Text size="xs" color="text-white" weight="bold" textAlign="center" uppercase letterSpacing="widest">{step.name}</Text>
</Box>
{i < steps.length - 1 && (
<Box as="svg" w="4" h="4" mx={1} viewBox="0 0 24 24" fill="none">
<path d="M5 12h14m-7-7l7 7-7 7" stroke="#198CFF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M5 12h14m-7-7l7 7-7 7" stroke="var(--ui-color-intent-primary)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</Box>
)}
</Box>
@@ -83,7 +76,7 @@ export function ProtestWorkflowMockup() {
position="absolute"
insetY="0"
left="0"
bg="primary-accent"
bg="var(--ui-color-intent-primary)"
style={{ width: `${((activeStep + 1) / steps.length) * 100}%` }}
/>
</Box>
@@ -104,7 +97,7 @@ export function ProtestWorkflowMockup() {
};
return (
<Box position="relative" fullWidth fullHeight bg="graphite-black" rounded="none" overflow="hidden" p={{ base: 1.5, sm: 3, md: 5, lg: 8 }} display="flex" flexDirection="col" justifyContent="center" gap={{ base: 2, sm: 4, md: 6, lg: 8 }}>
<Box position="relative" fullWidth fullHeight bg="var(--ui-color-bg-base)" rounded="none" overflow="hidden" p={{ base: 1.5, sm: 3, md: 5, lg: 8 }} display="flex" flexDirection="col" justifyContent="center" gap={{ base: 2, sm: 4, md: 6, lg: 8 }}>
<Box display="flex" flexDirection={{ base: 'col', md: 'row' }} alignItems="center" justifyContent="center" gap={{ base: 2, sm: 3, md: 4 }}>
{steps.map((step, i) => (
<Box key={step.name} display="flex" alignItems="center" flexShrink={0}>
@@ -131,10 +124,12 @@ export function ProtestWorkflowMockup() {
mb={{ base: 1, sm: 1.5, md: 2 }}
border
borderWidth="1px"
className={getStatusStyles(step.status)}
bg={step.status === 'pending' ? 'var(--ui-color-bg-surface)' : step.status === 'active' ? 'rgba(255, 190, 77, 0.1)' : 'rgba(16, 185, 129, 0.1)'}
borderColor={step.status === 'pending' ? 'var(--ui-color-border-default)' : step.status === 'active' ? 'var(--ui-color-intent-warning)' : 'var(--ui-color-intent-success)'}
color={step.status === 'pending' ? 'var(--ui-color-text-low)' : step.status === 'active' ? 'var(--ui-color-intent-warning)' : 'var(--ui-color-intent-success)'}
whileHover={shouldReduceMotion ? {} : {
scale: 1.05,
borderColor: '#198CFF',
borderColor: 'var(--ui-color-intent-primary)',
transition: { duration: 0.2 }
}}
>
@@ -142,7 +137,7 @@ export function ProtestWorkflowMockup() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={step.icon} />
</Box>
{step.status === 'active' && (
<Box position="absolute" top="-1px" left="-1px" w="2" h="2" borderTop borderLeft borderColor="warning-amber" />
<Box position="absolute" top="-1px" left="-1px" w="2" h="2" borderTop borderLeft borderColor="var(--ui-color-intent-warning)" />
)}
</Box>
<Text
@@ -151,7 +146,8 @@ export function ProtestWorkflowMockup() {
color="text-white"
weight="bold"
textAlign="center"
className="uppercase tracking-widest"
uppercase
letterSpacing="widest"
>
{step.name}
</Text>
@@ -159,13 +155,13 @@ export function ProtestWorkflowMockup() {
{i < steps.length - 1 && (
<Box
className="hidden md:block"
display={{ base: 'none', md: 'block' }}
position="relative"
ml={2}
mr={2}
>
<Box as="svg" w={{ base: 3, sm: 4, md: 5 }} h={{ base: 3, sm: 4, md: 5 }} viewBox="0 0 24 24" fill="none">
<path d="M5 12h14m-7-7l7 7-7 7" stroke="#198CFF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" opacity={0.5} />
<path d="M5 12h14m-7-7l7 7-7 7" stroke="var(--ui-color-intent-primary)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" opacity={0.5} />
</Box>
</Box>
)}
@@ -189,7 +185,7 @@ export function ProtestWorkflowMockup() {
position="absolute"
insetY="0"
left="0"
bg="primary-accent"
bg="var(--ui-color-intent-primary)"
initial={{ width: '0%' }}
animate={{ width: `${((activeStep + 1) / steps.length) * 100}%` }}
transition={{ duration: 0.5, ease: 'easeOut' }}

View File

@@ -2,7 +2,7 @@
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Modal } from '@/ui/Modal';
import { Modal } from '@/components/shared/Modal';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Grid } from '@/ui/Grid';

View File

@@ -1,5 +1,6 @@
import { Container } from '@/ui/Container';
import { Stack } from '@/ui/Stack';
import { Box } from '@/ui/Box';
interface OnboardingShellProps {
children: React.ReactNode;
@@ -16,40 +17,40 @@ interface OnboardingShellProps {
*/
export function OnboardingShell({ children, header, footer, sidebar }: OnboardingShellProps) {
return (
<Stack minHeight="screen" bg="bg-near-black" color="text-white">
<Box minHeight="100vh" bg="rgba(10,10,10,1)" color="white">
{header && (
<Stack borderBottom borderColor="border-charcoal-outline" py={4} bg="bg-deep-charcoal">
<Box borderBottom borderColor="rgba(255,255,255,0.1)" py={4} bg="rgba(20,22,25,1)">
<Container size="md">
{header}
</Container>
</Stack>
</Box>
)}
<Stack flex={1} py={12}>
<Box flexGrow={1} py={12}>
<Container size="md">
<Stack direction="row" gap={12}>
<Stack flex={1}>
<Box flexGrow={1}>
<Stack gap={8}>
{children}
</Stack>
</Stack>
</Box>
{sidebar && (
<Stack w="80" display={{ base: 'none', lg: 'block' }}>
<Box w="80" display={{ base: 'none', lg: 'block' }}>
{sidebar}
</Stack>
</Box>
)}
</Stack>
</Container>
</Stack>
</Box>
{footer && (
<Stack borderTop borderColor="border-charcoal-outline" py={6} bg="bg-deep-charcoal">
<Box borderTop borderColor="rgba(255,255,255,0.1)" py={6} bg="rgba(20,22,25,1)">
<Container size="md">
{footer}
</Container>
</Stack>
</Box>
)}
</Stack>
</Box>
);
}

View File

@@ -5,7 +5,7 @@ import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { InfoBox } from '@/ui/InfoBox';
import { Input } from '@/ui/Input';
import { Modal } from '@/ui/Modal';
import { Modal } from '@/components/shared/Modal';
import { Stack } from '@/ui/Stack';
import { Select } from '@/ui/Select';
import { Text } from '@/ui/Text';

View File

@@ -3,7 +3,7 @@
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Input } from '@/ui/Input';
import { Modal } from '@/ui/Modal';
import { Modal } from '@/components/shared/Modal';
import { Stack } from '@/ui/Stack';
import { Select } from '@/ui/Select';
import { Text } from '@/ui/Text';

View File

@@ -1,9 +1,9 @@
import { ChevronDown, ChevronUp } from 'lucide-react';
import { ReactNode, useState } from 'react';
import { Box } from './Box';
import { Icon } from './Icon';
import { Surface } from './Surface';
import { Text } from './Text';
import { Icon } from '@/ui/Icon';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
export interface AccordionProps {
title: string;
@@ -36,15 +36,23 @@ export const Accordion = ({
return (
<Surface variant="muted" rounded="lg" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
<button
<Box
as="button"
onClick={handleToggle}
className="w-full flex items-center justify-between px-4 py-3 hover:bg-white/5 transition-colors"
fullWidth
display="flex"
alignItems="center"
justifyContent="between"
paddingX={4}
paddingY={3}
hoverBg="rgba(255,255,255,0.05)"
transition
>
<Text weight="bold" size="sm" variant="high">
{title}
</Text>
<Icon icon={isOpen ? ChevronUp : ChevronDown} size={4} intent="low" />
</button>
</Box>
{isOpen && (
<Box padding={4} borderTop>

View File

@@ -1,5 +1,5 @@
import { Button } from '@/ui/Button';
import { Modal } from '@/ui/Modal';
import { Modal } from '@/components/shared/Modal';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
import { AlertCircle } from 'lucide-react';

View File

@@ -1,6 +1,3 @@
/* eslint-disable gridpilot-rules/no-raw-html-in-app */
import { Check, ChevronDown, Globe, Search } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { CountryFlag } from '@/ui/CountryFlag';
@@ -10,6 +7,7 @@ import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Surface } from '@/ui/Surface';
import { Icon } from '@/ui/Icon';
import { Input } from '@/ui/Input';
export interface Country {
code: string;
@@ -115,32 +113,31 @@ export function CountrySelect({
return (
<Box ref={containerRef} position="relative">
{/* Trigger Button */}
<button
<Box
as="button"
type="button"
onClick={() => !disabled && setIsOpen(!isOpen)}
disabled={disabled}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
borderRadius: 'var(--ui-radius-md)',
border: 'none',
padding: '0.75rem 1rem',
backgroundColor: 'var(--ui-color-bg-surface-muted)',
color: 'white',
boxShadow: 'var(--ui-shadow-sm)',
transition: 'all 150ms',
cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.5 : 1,
outline: 'none',
ring: '1px inset',
borderColor: error ? 'var(--ui-color-intent-critical)' : 'var(--ui-color-border-default)'
} as any}
className={error ? 'ring-warning-amber' : 'ring-charcoal-outline focus:ring-primary-blue'}
display="flex"
alignItems="center"
justifyContent="between"
fullWidth
rounded="md"
paddingX={4}
paddingY={3}
bg="var(--ui-color-bg-surface-muted)"
color="white"
shadow="sm"
transition="all 150ms"
cursor={disabled ? 'not-allowed' : 'pointer'}
opacity={disabled ? 0.5 : 1}
outline="none"
border
borderColor={error ? 'var(--ui-color-intent-critical)' : 'var(--ui-color-border-default)'}
hoverBorderColor={!disabled ? 'var(--ui-color-intent-primary)' : undefined}
>
<Group gap={3}>
<Globe className="w-4 h-4 text-gray-500" />
<Icon icon={Globe} size={4} intent="low" />
{selectedCountry ? (
<Group gap={2}>
<CountryFlag countryCode={selectedCountry.code} size="md" />
@@ -150,8 +147,16 @@ export function CountrySelect({
<Text variant="low">{placeholder}</Text>
)}
</Group>
<ChevronDown className={`w-4 h-4 text-gray-500 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
</button>
<Icon
icon={ChevronDown}
size={4}
intent="low"
style={{
transition: 'transform 200ms',
transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)'
}}
/>
</Box>
{/* Dropdown */}
{isOpen && (
@@ -167,60 +172,49 @@ export function CountrySelect({
overflow="hidden"
>
{/* Search Input */}
<Box padding={2} borderBottom="1px solid var(--ui-color-border-muted)">
<Box position="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
<input
ref={inputRef}
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search countries..."
style={{
width: '100%',
borderRadius: 'var(--ui-radius-md)',
border: 'none',
padding: '0.5rem 1rem 0.5rem 2.25rem',
backgroundColor: 'var(--ui-color-bg-base)',
color: 'white',
fontSize: '0.875rem',
outline: 'none'
}}
/>
</Box>
<Box padding={2} borderBottom>
<Input
ref={inputRef}
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search countries..."
fullWidth
size="sm"
icon={<Icon icon={Search} size={4} intent="low" />}
/>
</Box>
{/* Country List */}
<Box overflowY="auto" maxHeight="15rem">
{filteredCountries.length > 0 ? (
filteredCountries.map((country) => (
<button
<Box
as="button"
key={country.code}
type="button"
onClick={() => handleSelect(country.code)}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
padding: '0.625rem 1rem',
textAlign: 'left',
fontSize: '0.875rem',
transition: 'colors 150ms',
border: 'none',
backgroundColor: value === country.code ? 'rgba(25, 140, 255, 0.2)' : 'transparent',
color: value === country.code ? 'white' : 'var(--ui-color-text-med)',
cursor: 'pointer'
}}
display="flex"
alignItems="center"
justifyContent="between"
fullWidth
paddingX={4}
paddingY={2.5}
textAlign="left"
transition="colors 150ms"
border="none"
bg={value === country.code ? 'rgba(25, 140, 255, 0.2)' : 'transparent'}
color={value === country.code ? 'white' : 'var(--ui-color-text-med)'}
cursor="pointer"
hoverBg="rgba(255, 255, 255, 0.05)"
>
<Group gap={3}>
<CountryFlag countryCode={country.code} size="md" />
<Text as="span">{country.name}</Text>
</Group>
{value === country.code && (
<Check className="w-4 h-4 text-primary-blue" />
<Icon icon={Check} size={4} intent="primary" />
)}
</button>
</Box>
))
) : (
<Box paddingX={4} paddingY={6} textAlign="center">

View File

@@ -1,11 +1,11 @@
import { HelpCircle, X } from 'lucide-react';
import React, { ReactNode, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Box } from './Box';
import { Heading } from './Heading';
import { Icon } from './Icon';
import { IconButton } from './IconButton';
import { Surface } from './Surface';
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { IconButton } from '@/ui/IconButton';
import { Surface } from '@/ui/Surface';
export interface InfoFlyoutProps {
isOpen: boolean;

View File

@@ -1,12 +1,12 @@
import { X } from 'lucide-react';
import { ReactNode, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { Box } from './Box';
import { Button } from './Button';
import { Heading } from './Heading';
import { IconButton } from './IconButton';
import { Surface } from './Surface';
import { Text } from './Text';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading';
import { IconButton } from '@/ui/IconButton';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
export interface ModalProps {
children: ReactNode;

View File

@@ -1,8 +1,8 @@
'use client';
import React, { createContext, useContext, ReactNode } from 'react';
import { Theme } from './Theme';
import { defaultTheme } from './themes/default';
import { Theme } from '@/ui/theme/Theme';
import { defaultTheme } from '@/ui/theme/themes/default';
interface ThemeContextType {
theme: Theme;

View File

@@ -3,7 +3,7 @@ import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Container } from '@/ui/Container';
import { ConfirmDialog } from '@/ui/ConfirmDialog';
import { ConfirmDialog } from '@/components/shared/ConfirmDialog';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Card } from '@/ui/Card';
@@ -17,7 +17,7 @@ import { Skeleton } from '@/ui/Skeleton';
import { LoadingSpinner } from '@/ui/LoadingSpinner';
import { Badge } from '@/ui/Badge';
import { ProgressLine } from '@/ui/ProgressLine';
import { ProgressLine } from '@/components/shared/ProgressLine';
import { SharedEmptyState } from './SharedEmptyState';
export {

View File

@@ -107,7 +107,8 @@ export function UploadDropzone({
cursor: 'pointer'
}}
>
<input
<Box
as="input"
type="file"
ref={fileInputRef}
onChange={handleFileSelect}

View File

@@ -1,5 +1,5 @@
import { EmptyState } from '@/ui/EmptyState';
import { ErrorDisplay } from '@/ui/ErrorDisplay';
import { ErrorDisplay } from '@/components/shared/ErrorDisplay';
import { LoadingWrapper } from '@/ui/LoadingWrapper';
import { ApiError } from '@/lib/api/base/ApiError';
import { Inbox, List, LucideIcon } from 'lucide-react';

View File

@@ -1,7 +1,7 @@
'use client';
import { EmptyState } from '@/ui/EmptyState';
import { ErrorDisplay } from '@/ui/ErrorDisplay';
import { ErrorDisplay } from '@/components/shared/ErrorDisplay';
import { LoadingWrapper } from '@/ui/LoadingWrapper';
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';

View File

@@ -8,7 +8,7 @@ import {
Car,
TrendingUp,
} from 'lucide-react';
import { WorkflowMockup, WorkflowStep } from '@/ui/WorkflowMockup';
import { WorkflowMockup, WorkflowStep } from '@/components/mockups/WorkflowMockup';
const WORKFLOW_STEPS: WorkflowStep[] = [
{

View File

@@ -1,10 +1,10 @@
'use client';
import { ReactNode } from 'react';
import { Box } from '@/ui/Box';
import { Container } from '@/ui/Container';
import { Stack } from '@/ui/Stack';
import { Group } from '@/ui/Group';
import { Text } from '@/ui/Text';
import { StatusDot } from '@/ui/StatusDot';
interface TeamsDirectoryProps {
children: ReactNode;
@@ -14,32 +14,33 @@ interface TeamsDirectoryProps {
export function TeamsDirectory({ children, title, subtitle }: TeamsDirectoryProps) {
return (
<Box as="main" bg="base-black" minHeight="screen">
<Container size="lg">
<Box paddingY={12}>
<Stack gap={10}>
{title && (
<Stack direction="row" align="center" gap={2} mb={6}>
<Box w="2" h="2" bg="primary-accent" />
<Text size="xs" weight="bold" color="text-gray-400" uppercase>{title}</Text>
</Stack>
)}
{children}
</Stack>
</Box>
</Container>
</Box>
<Container size="lg" py={12}>
<Group direction="column" gap={10} fullWidth>
{title && (
<Group direction="row" align="center" gap={2}>
<StatusDot intent="primary" size="md" />
<Text size="xs" weight="bold" variant="low" uppercase>{title}</Text>
</Group>
)}
{children}
</Group>
</Container>
);
}
export function TeamsDirectorySection({ children, title, accentColor = "primary-accent" }: { children: ReactNode, title: string, accentColor?: string }) {
const intentMap: Record<string, 'primary' | 'success' | 'warning' | 'critical' | 'telemetry'> = {
'primary-accent': 'primary',
'telemetry-aqua': 'telemetry',
};
return (
<Box>
<Stack direction="row" align="center" gap={2} mb={6}>
<Box w="2" h="2" bg={accentColor} />
<Text size="xs" weight="bold" color="text-gray-400" uppercase>{title}</Text>
</Stack>
<Group direction="column" gap={6} fullWidth>
<Group direction="row" align="center" gap={2}>
<StatusDot intent={intentMap[accentColor] || 'primary'} size="md" />
<Text size="xs" weight="bold" variant="low" uppercase>{title}</Text>
</Group>
{children}
</Box>
</Group>
);
}

View File

@@ -1,8 +0,0 @@
import { describe, it, expect } from 'vitest';
import { LeagueRoleDisplay } from './LeagueRoleDisplay';
describe('LeagueRoleDisplay', () => {
it('should be defined', () => {
expect(LeagueRoleDisplay).toBeDefined();
});
});

View File

@@ -1,8 +0,0 @@
import { describe, it, expect } from 'vitest';
import { LeagueWizardValidationMessages } from './LeagueWizardValidationMessages';
describe('LeagueWizardValidationMessages', () => {
it('should be defined', () => {
expect(LeagueWizardValidationMessages).toBeDefined();
});
});

View File

@@ -6,21 +6,18 @@ import { AdminSectionHeader } from '@/components/admin/AdminSectionHeader';
import { AdminStatsPanel } from '@/components/admin/AdminStatsPanel';
import { routes } from '@/lib/routing/RouteConfig';
import { AdminDashboardViewData } from '@/lib/view-data/AdminDashboardViewData';
import {
SharedBox,
SharedButton,
SharedCard,
SharedContainer,
SharedIcon,
SharedGrid,
SharedStack,
SharedText,
SharedBadge
} from '@/components/shared/UIComponents';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Container } from '@/ui/Container';
import { Icon } from '@/ui/Icon';
import { Grid } from '@/ui/Grid';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Badge } from '@/ui/Badge';
import { QuickActionLink } from '@/ui/QuickActionLink';
import {
Activity,
ArrowRight,
Clock,
RefreshCw,
Shield,
@@ -70,93 +67,95 @@ export function AdminDashboardTemplate({
];
return (
<SharedContainer size="lg">
<SharedBox paddingY={8}>
<SharedStack gap={8}>
<Container size="lg">
<Box paddingY={8}>
<Stack gap={8}>
<AdminHeaderPanel
title="Admin Dashboard"
description="System-wide telemetry and operations control"
isLoading={isLoading}
actions={
<SharedButton
<Button
onClick={onRefresh}
disabled={isLoading}
variant="secondary"
size="sm"
icon={<SharedIcon icon={RefreshCw} size={3} animate={isLoading ? 'spin' : 'none'} />}
>
Refresh Telemetry
</SharedButton>
<Stack direction="row" align="center" gap={2}>
<Icon icon={RefreshCw} size={3} animate={isLoading ? 'spin' : 'none'} />
<Text>Refresh Telemetry</Text>
</Stack>
</Button>
}
/>
<AdminStatsPanel stats={stats} />
<SharedGrid cols={{ base: 1, md: 2 }} gap={6}>
<Grid responsiveGridCols={{ base: 1, md: 2 }} gap={6}>
{/* System Health & Status */}
<SharedCard p={6}>
<SharedStack gap={6}>
<Card padding={6}>
<Stack gap={6}>
<AdminSectionHeader
title="System Status"
actions={
<SharedBadge variant="success">
<SharedStack direction="row" align="center" gap={1.5}>
<SharedIcon icon={Activity} size={3} />
<SharedText>Operational</SharedText>
</SharedStack>
</SharedBadge>
<Badge variant="success">
<Stack direction="row" align="center" gap={1.5}>
<Icon icon={Activity} size={3} />
<Text>Operational</Text>
</Stack>
</Badge>
}
/>
<SharedStack gap={4}>
<SharedBox borderTop borderColor="border-gray" opacity={0.3} />
<SharedBox pt={0}>
<SharedStack direction="row" align="center" justify="between" py={2}>
<SharedText size="sm" color="text-gray-400">Suspended Users</SharedText>
<SharedText weight="bold" color="text-warning-amber">{viewData.stats.suspendedUsers}</SharedText>
</SharedStack>
</SharedBox>
<SharedBox borderTop borderColor="border-gray" opacity={0.3} />
<SharedBox>
<SharedStack direction="row" align="center" justify="between" py={2}>
<SharedText size="sm" color="text-gray-400">Deleted Users</SharedText>
<SharedText weight="bold" color="text-error-red">{viewData.stats.deletedUsers}</SharedText>
</SharedStack>
</SharedBox>
<SharedBox borderTop borderColor="border-gray" opacity={0.3} />
<SharedBox>
<SharedStack direction="row" align="center" justify="between" py={2}>
<SharedText size="sm" color="text-gray-400">New Registrations (24h)</SharedText>
<SharedText weight="bold" color="text-primary-blue">{viewData.stats.newUsersToday}</SharedText>
</SharedStack>
</SharedBox>
</SharedStack>
</SharedStack>
</SharedCard>
<Stack gap={4}>
<Box borderTop borderColor="rgba(255,255,255,0.1)" />
<Box pt={0}>
<Stack direction="row" align="center" justify="between" py={2}>
<Text size="sm" color="text-gray-400">Suspended Users</Text>
<Text weight="bold" color="warning-amber">{viewData.stats.suspendedUsers}</Text>
</Stack>
</Box>
<Box borderTop borderColor="rgba(255,255,255,0.1)" />
<Box>
<Stack direction="row" align="center" justify="between" py={2}>
<Text size="sm" color="text-gray-400">Deleted Users</Text>
<Text weight="bold" color="critical-red">{viewData.stats.deletedUsers}</Text>
</Stack>
</Box>
<Box borderTop borderColor="rgba(255,255,255,0.1)" />
<Box>
<Stack direction="row" align="center" justify="between" py={2}>
<Text size="sm" color="text-gray-400">New Registrations (24h)</Text>
<Text weight="bold" color="primary-accent">{viewData.stats.newUsersToday}</Text>
</Stack>
</Box>
</Stack>
</Stack>
</Card>
{/* Quick Operations */}
<SharedCard p={6}>
<SharedStack gap={6}>
<Card padding={6}>
<Stack gap={6}>
<AdminSectionHeader title="Quick Operations" />
<SharedGrid cols={1} gap={3}>
<Grid responsiveGridCols={{ base: 1 }} gap={3}>
<QuickActionLink href={routes.admin.users} label="User Management" icon={Users} />
<QuickActionLink href="/admin" label="Security & Roles" icon={Shield} />
<QuickActionLink href="/admin" label="System Audit Logs" icon={Activity} />
</SharedGrid>
</SharedStack>
</SharedCard>
</SharedGrid>
</Grid>
</Stack>
</Card>
</Grid>
<AdminDangerZonePanel
title="System Maintenance"
description="Perform destructive system-wide operations. Use with extreme caution."
>
<SharedButton variant="danger" size="sm">
<Button variant="danger" size="sm">
Enter Maintenance Mode
</SharedButton>
</Button>
</AdminDangerZonePanel>
</SharedStack>
</SharedBox>
</SharedContainer>
</Stack>
</Box>
</Container>
);
}

View File

@@ -1,32 +1,24 @@
'use client';
import { FormEvent, ReactNode } from 'react';
import { FormEvent } from 'react';
import { LeagueReviewSummary } from '@/components/leagues/LeagueReviewSummary';
import {
SharedBox,
SharedButton,
SharedStack,
SharedText,
SharedIcon,
SharedContainer
} from '@/components/shared/UIComponents';
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { Input } from '@/ui/Input';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Grid } from '@/ui/Grid';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
import {
AlertCircle,
Award,
Calendar,
Check,
CheckCircle2,
ChevronLeft,
ChevronRight,
FileText,
Loader2,
Scale,
Sparkles,
Trophy,
Users,
} from 'lucide-react';
import { LeagueBasicsSection } from '@/components/leagues/LeagueBasicsSection';
import { LeagueDropSection } from '@/components/leagues/LeagueDropSection';
@@ -90,45 +82,45 @@ export function CreateLeagueWizardTemplate({
const CurrentStepIcon = currentStepData?.icon ?? FileText;
return (
<SharedBox as="form" onSubmit={onSubmit} maxWidth="4xl" mx="auto" pb={8}>
<Box as="form" onSubmit={onSubmit} maxWidth="4xl" mx="auto" pb={8}>
{/* Header with icon */}
<SharedBox mb={8}>
<SharedStack direction="row" align="center" gap={3} mb={3}>
<SharedBox display="flex" h="11" w="11" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue/20" border borderColor="border-primary-blue/20">
<SharedIcon icon={Sparkles} size={5} color="text-primary-blue" />
</SharedBox>
<SharedBox>
<Heading level={1} fontSize={{ base: '2xl', sm: '3xl' }}>
<Box mb={8}>
<Stack direction="row" align="center" gap={3} mb={3}>
<Box display="flex" h="11" w="11" alignItems="center" justifyContent="center" rounded="xl" bg="primary-accent" opacity={0.2} border borderColor="primary-accent">
<Icon icon={Sparkles} size={5} color="primary-accent" />
</Box>
<Box>
<Heading level={1}>
Create a new league
</Heading>
<SharedText size="sm" color="text-gray-500" block>
<Text size="sm" color="text-gray-500" block>
We'll also set up your first season in {steps.length} easy steps.
</SharedText>
<SharedText size="xs" color="text-gray-500" block mt={1}>
</Text>
<Text size="xs" color="text-gray-500" block mt={1}>
A league is your long-term brand. Each season is a block of races you can run again and again.
</SharedText>
</SharedBox>
</SharedStack>
</SharedBox>
</Text>
</Box>
</Stack>
</Box>
{/* Desktop Progress Bar */}
<SharedBox display={{ base: 'none', md: 'block' }} mb={8}>
<SharedBox position="relative">
<Box display={{ base: 'none', md: 'block' }} mb={8}>
<Box position="relative">
{/* Background track */}
<SharedBox position="absolute" top="5" left="6" right="6" h="0.5" bg="bg-charcoal-outline" rounded="full" />
<Box position="absolute" top="5" left="6" right="6" h="0.5" bg="rgba(255,255,255,0.1)" rounded="full" />
{/* Progress fill */}
<SharedBox
<Box
position="absolute"
top="5"
left="6"
h="0.5"
bg="bg-gradient-to-r from-primary-blue to-neon-aqua"
bg="primary-accent"
rounded="full"
transition
width={`calc(${((step - 1) / (steps.length - 1)) * 100}% - 48px)`}
width={`${((step - 1) / (steps.length - 1)) * 100}%`}
/>
<SharedBox position="relative" display="flex" justifyContent="between">
<Box position="relative" display="flex" justifyContent="between">
{steps.map((wizardStep) => {
const isCompleted = wizardStep.id < step;
const isCurrent = wizardStep.id === step;
@@ -136,7 +128,7 @@ export function CreateLeagueWizardTemplate({
const StepIcon = wizardStep.icon;
return (
<SharedBox
<Box
as="button"
key={wizardStep.id}
type="button"
@@ -145,12 +137,11 @@ export function CreateLeagueWizardTemplate({
display="flex"
flexDirection="col"
alignItems="center"
bg="bg-transparent"
borderStyle="none"
bg="transparent"
cursor={isAccessible ? 'pointer' : 'not-allowed'}
opacity={!isAccessible ? 0.6 : 1}
>
<SharedBox
<Box
position="relative"
zIndex={10}
display="flex"
@@ -160,129 +151,83 @@ export function CreateLeagueWizardTemplate({
justifyContent="center"
rounded="full"
transition
bg={isCurrent || isCompleted ? 'bg-primary-blue' : 'bg-iron-gray'}
color={isCurrent || isCompleted ? 'text-white' : 'text-gray-400'}
border={!isCurrent && !isCompleted}
borderColor="border-charcoal-outline"
shadow={isCurrent ? '0_0_24px_rgba(25,140,255,0.5)' : undefined}
transform={isCurrent ? 'scale-110' : isCompleted ? 'hover:scale-105' : undefined}
bg={isCurrent || isCompleted ? 'primary-accent' : 'rgba(255,255,255,0.1)'}
color={isCurrent || isCompleted ? 'white' : 'text-gray-400'}
>
{isCompleted ? (
<SharedIcon icon={Check} size={4} strokeWidth={3} />
<Icon icon={Check} size={4} />
) : (
<SharedIcon icon={StepIcon} size={4} />
<Icon icon={StepIcon} size={4} />
)}
</SharedBox>
<SharedBox mt={2} textAlign="center">
<SharedText
</Box>
<Box mt={2} textAlign="center">
<Text
size="xs"
weight="medium"
transition
color={isCurrent ? 'text-white' : isCompleted ? 'text-primary-blue' : isAccessible ? 'text-gray-400' : 'text-gray-500'}
color={isCurrent ? 'white' : isCompleted ? 'primary-accent' : isAccessible ? 'text-gray-400' : 'text-gray-500'}
>
{wizardStep.label}
</SharedText>
</SharedBox>
</SharedBox>
</Text>
</Box>
</Box>
);
})}
</SharedBox>
</SharedBox>
</SharedBox>
{/* Mobile Progress */}
<SharedBox display={{ base: 'block', md: 'none' }} mb={6}>
<SharedBox display="flex" alignItems="center" justifyContent="between" mb={2}>
<SharedStack direction="row" align="center" gap={2}>
<SharedIcon icon={CurrentStepIcon} size={4} color="text-primary-blue" />
<SharedText size="sm" weight="medium" color="text-white">{currentStepData?.label}</SharedText>
</SharedStack>
<SharedText size="xs" color="text-gray-500">
{step}/{steps.length}
</SharedText>
</SharedBox>
<SharedBox h="1.5" bg="bg-charcoal-outline" rounded="full" overflow="hidden">
<SharedBox
h="full"
bg="bg-gradient-to-r from-primary-blue to-neon-aqua"
rounded="full"
transition
height="full"
width={`${(step / steps.length) * 100}%`}
/>
</SharedBox>
{/* Step dots */}
<SharedBox display="flex" justifyContent="between" mt={2} px={0.5}>
{steps.map((s) => (
<SharedBox
key={s.id}
h="1.5"
rounded="full"
transition
width={s.id === step ? '4' : '1.5'}
bg={s.id === step ? 'bg-primary-blue' : s.id < step ? 'bg-primary-blue/60' : 'bg-charcoal-outline'}
/>
))}
</SharedBox>
</SharedBox>
</Box>
</Box>
</Box>
{/* Main Card */}
<Card position="relative" overflow="hidden">
{/* Top gradient accent */}
<SharedBox position="absolute" top="0" left="0" right="0" h="1" bg="bg-gradient-to-r from-transparent via-primary-blue to-transparent" />
{/* Step header */}
<SharedBox display="flex" alignItems="start" gap={4} mb={6}>
<SharedBox display="flex" h="12" w="12" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue/10" flexShrink={0} transition>
<SharedIcon icon={CurrentStepIcon} size={6} color="text-primary-blue" />
</SharedBox>
<SharedBox flexGrow={1} minWidth="0">
<Heading level={2} fontSize={{ base: 'xl', md: '2xl' }} color="text-white">
<SharedStack direction="row" align="center" gap={2} flexWrap="wrap">
<SharedText>{getStepTitle(step)}</SharedText>
<SharedText size="xs" weight="medium" px={2} py={0.5} rounded="full" border borderColor="border-charcoal-outline" bg="bg-iron-gray/60" color="text-gray-300">
{getStepContextLabel(step)}
</SharedText>
</SharedStack>
<Box display="flex" alignItems="start" gap={4} mb={6}>
<Box display="flex" h="12" w="12" alignItems="center" justifyContent="center" rounded="xl" bg="rgba(25,140,255,0.1)" flexShrink={0} transition>
<Icon icon={CurrentStepIcon} size={6} color="primary-accent" />
</Box>
<Box flexGrow={1} minWidth="0">
<Heading level={2} color="white">
<Stack direction="row" align="center" gap={2} flexWrap="wrap">
<Text>{getStepTitle(step)}</Text>
<Box px={2} py={0.5} rounded="full" border borderColor="rgba(255,255,255,0.1)" bg="rgba(255,255,255,0.05)">
<Text size="xs" weight="medium" color="text-gray-300">
{getStepContextLabel(step)}
</Text>
</Box>
</Stack>
</Heading>
<SharedText size="sm" color="text-gray-400" block mt={1}>
<Text size="sm" color="text-gray-400" block mt={1}>
{getStepSubtitle(step)}
</SharedText>
</SharedBox>
<SharedBox display={{ base: 'none', sm: 'flex' }} alignItems="center" gap={1.5} px={3} py={1.5} rounded="full" bg="bg-deep-graphite" border borderColor="border-charcoal-outline">
<SharedText size="xs" color="text-gray-500">Step</SharedText>
<SharedText size="sm" weight="semibold" color="text-white">{step}</SharedText>
<SharedText size="xs" color="text-gray-500">/ {steps.length}</SharedText>
</SharedBox>
</SharedBox>
</Text>
</Box>
<Box display={{ base: 'none', sm: 'flex' }} alignItems="center" gap={1.5} px={3} py={1.5} rounded="full" bg="rgba(0,0,0,0.2)" border borderColor="rgba(255,255,255,0.1)">
<Text size="xs" color="text-gray-500">Step</Text>
<Text size="sm" weight="semibold" color="white">{step}</Text>
<Text size="xs" color="text-gray-500">/ {steps.length}</Text>
</Box>
</Box>
{/* Divider */}
<SharedBox h="px" bg="bg-gradient-to-r from-transparent via-charcoal-outline to-transparent" mb={6} />
{/* Step content with min-height for consistency */}
<SharedBox minHeight="320px">
{/* Step content */}
<Box minHeight="320px">
{step === 1 && (
<SharedBox animate="fade-in" gap={8} display="flex" flexDirection="col">
<Stack gap={8}>
<LeagueBasicsSection
form={form}
onChange={onFormChange}
errors={errors.basics ?? {}}
/>
<SharedBox rounded="xl" border borderColor="border-charcoal-outline" bg="bg-iron-gray/40" p={4}>
<SharedBox display="flex" alignItems="center" justifyContent="between" gap={2} mb={2}>
<SharedBox>
<SharedText size="xs" weight="semibold" color="text-gray-300" uppercase letterSpacing="wide">
First season
</SharedText>
<SharedText size="xs" color="text-gray-500" block>
Name the first season that will run in this league.
</SharedText>
</SharedBox>
</SharedBox>
<SharedBox mt={2} display="flex" flexDirection="col" gap={2}>
<SharedText as="label" size="sm" weight="medium" color="text-gray-300" block>
<Box rounded="xl" border borderColor="rgba(255,255,255,0.1)" bg="rgba(255,255,255,0.05)" p={4}>
<Box mb={2}>
<Text size="xs" weight="semibold" color="text-gray-300" uppercase letterSpacing="wide">
First season
</Text>
<Text size="xs" color="text-gray-500" block>
Name the first season that will run in this league.
</Text>
</Box>
<Stack gap={2}>
<Text as="label" size="sm" weight="medium" color="text-gray-300" block>
Season name
</SharedText>
</Text>
<Input
value={form.seasonName ?? ''}
onChange={(e) =>
@@ -293,75 +238,63 @@ export function CreateLeagueWizardTemplate({
}
placeholder="e.g., Season 1 (2025)"
/>
<SharedText size="xs" color="text-gray-500" block>
<Text size="xs" color="text-gray-500" block>
Seasons are the individual competitive runs inside your league. You can run Season 2, Season 3, or parallel seasons later.
</SharedText>
</SharedBox>
</SharedBox>
</SharedBox>
</Text>
</Stack>
</Box>
</Stack>
)}
{step === 2 && (
<SharedBox animate="fade-in">
<LeagueVisibilitySection
form={form}
onChange={onFormChange}
errors={
errors.basics?.visibility
? { visibility: errors.basics.visibility }
: {}
}
/>
</SharedBox>
<LeagueVisibilitySection
form={form}
onChange={onFormChange}
errors={
errors.basics?.visibility
? { visibility: errors.basics.visibility }
: {}
}
/>
)}
{step === 3 && (
<SharedBox animate="fade-in" display="flex" flexDirection="col" gap={4}>
<SharedBox mb={2}>
<SharedText size="xs" color="text-gray-500" block>
<Stack gap={4}>
<Box>
<Text size="xs" color="text-gray-500" block>
Applies to: First season of this league.
</SharedText>
<SharedText size="xs" color="text-gray-500" block>
These settings only affect this season. Future seasons can use different formats.
</SharedText>
</SharedBox>
</Text>
</Box>
<LeagueStructureSection
form={form}
onChange={onFormChange}
readOnly={false}
/>
</SharedBox>
</Stack>
)}
{step === 4 && (
<SharedBox animate="fade-in" display="flex" flexDirection="col" gap={4}>
<SharedBox mb={2}>
<SharedText size="xs" color="text-gray-500" block>
<Stack gap={4}>
<Box>
<Text size="xs" color="text-gray-500" block>
Applies to: First season of this league.
</SharedText>
<SharedText size="xs" color="text-gray-500" block>
These settings only affect this season. Future seasons can use different formats.
</SharedText>
</SharedBox>
</Text>
</Box>
<LeagueTimingsSection
form={form}
onChange={onFormChange}
errors={errors.timings ?? {}}
/>
</SharedBox>
</Stack>
)}
{step === 5 && (
<SharedBox animate="fade-in" display="flex" flexDirection="col" gap={8}>
<SharedBox mb={2}>
<SharedText size="xs" color="text-gray-500" block>
<Stack gap={8}>
<Box>
<Text size="xs" color="text-gray-500" block>
Applies to: First season of this league.
</SharedText>
<SharedText size="xs" color="text-gray-500" block>
These settings only affect this season. Future seasons can use different formats.
</SharedText>
</SharedBox>
{/* Scoring Pattern Selection */}
</Text>
</Box>
<ScoringPatternSection
scoring={form.scoring || {}}
presets={presets}
@@ -370,116 +303,96 @@ export function CreateLeagueWizardTemplate({
onChangePatternId={onScoringPresetChange}
onToggleCustomScoring={onToggleCustomScoring}
/>
{/* Divider */}
<SharedBox h="px" bg="bg-gradient-to-r from-transparent via-charcoal-outline to-transparent" />
{/* Championships & Drop Rules side by side on larger screens */}
<SharedBox display="grid" gridCols={{ base: 1, lg: 2 }} gap={6}>
<Grid responsiveGridCols={{ base: 1, lg: 2 }} gap={6}>
<ChampionshipsSection form={form} onChange={onFormChange} readOnly={presetsLoading} />
<LeagueDropSection form={form} onChange={onFormChange} readOnly={false} />
</SharedBox>
</Grid>
{errors.submit && (
<SharedBox display="flex" alignItems="start" gap={3} rounded="lg" bg="bg-warning-amber/10" p={4} border borderColor="border-warning-amber/20">
<SharedIcon icon={AlertCircle} size={5} color="text-warning-amber" flexShrink={0} mt={0.5} />
<SharedText size="sm" color="text-warning-amber">{errors.submit}</SharedText>
</SharedBox>
<Box display="flex" alignItems="start" gap={3} rounded="lg" bg="rgba(245,158,11,0.1)" p={4} border borderColor="rgba(245,158,11,0.2)">
<Icon icon={AlertCircle} size={5} color="warning-amber" />
<Text size="sm" color="warning-amber">{errors.submit}</Text>
</Box>
)}
</SharedBox>
</Stack>
)}
{step === 6 && (
<SharedBox animate="fade-in" display="flex" flexDirection="col" gap={4}>
<SharedBox mb={2}>
<SharedText size="xs" color="text-gray-500" block>
<Stack gap={4}>
<Box>
<Text size="xs" color="text-gray-500" block>
Applies to: First season of this league.
</SharedText>
<SharedText size="xs" color="text-gray-500" block>
These settings only affect this season. Future seasons can use different formats.
</SharedText>
</SharedBox>
</Text>
</Box>
<LeagueStewardingSection
form={form}
onChange={onFormChange}
readOnly={false}
/>
</SharedBox>
</Stack>
)}
{step === 7 && (
<SharedBox animate="fade-in" display="flex" flexDirection="col" gap={6}>
<Stack gap={6}>
<LeagueReviewSummary form={form} presets={presets} />
{errors.submit && (
<SharedBox display="flex" alignItems="start" gap={3} rounded="lg" bg="bg-warning-amber/10" p={4} border borderColor="border-warning-amber/20">
<SharedIcon icon={AlertCircle} size={5} color="text-warning-amber" flexShrink={0} mt={0.5} />
<SharedText size="sm" color="text-warning-amber">{errors.submit}</SharedText>
</SharedBox>
<Box display="flex" alignItems="start" gap={3} rounded="lg" bg="rgba(245,158,11,0.1)" p={4} border borderColor="rgba(245,158,11,0.2)">
<Icon icon={AlertCircle} size={5} color="warning-amber" />
<Text size="sm" color="warning-amber">{errors.submit}</Text>
</Box>
)}
</SharedBox>
</Stack>
)}
</SharedBox>
</Box>
</Card>
{/* Navigation */}
<SharedBox display="flex" alignItems="center" justifyContent="between" mt={6}>
<SharedButton
<Box display="flex" alignItems="center" justifyContent="between" mt={6}>
<Button
type="button"
variant="secondary"
disabled={step === 1 || loading}
onClick={onPreviousStep}
icon={<SharedIcon icon={ChevronLeft} size={4} />}
>
<SharedText display={{ base: 'none', md: 'inline-block' }}>Back</SharedText>
</SharedButton>
<SharedBox display="flex" alignItems="center" gap={3}>
{/* Mobile step dots */}
<SharedBox display={{ base: 'flex', sm: 'none' }} alignItems="center" gap={1}>
{steps.map((s) => (
<SharedBox
key={s.id}
h="1.5"
rounded="full"
transition
width={s.id === step ? '3' : '1.5'}
bg={s.id === step ? 'bg-primary-blue' : s.id < step ? 'bg-primary-blue/50' : 'bg-charcoal-outline'}
/>
))}
</SharedBox>
<Stack direction="row" align="center" gap={2}>
<Icon icon={ChevronLeft} size={4} />
<Text display={{ base: 'none', md: 'inline-block' }}>Back</Text>
</Stack>
</Button>
<Box display="flex" alignItems="center" gap={3}>
{step < 7 ? (
<SharedButton
<Button
type="button"
variant="primary"
disabled={loading}
onClick={onNextStep}
icon={<SharedIcon icon={ChevronRight} size={4} />}
>
<SharedText>Continue</SharedText>
</SharedButton>
<Stack direction="row" align="center" gap={2}>
<Text>Continue</Text>
<Icon icon={ChevronRight} size={4} />
</Stack>
</Button>
) : (
<SharedButton
<Button
type="submit"
variant="primary"
disabled={loading}
style={{ minWidth: '150px' }}
icon={loading ? <SharedIcon icon={Loader2} size={4} animate="spin" /> : <SharedIcon icon={Sparkles} size={4} />}
>
{loading ? (
<SharedText>Creating</SharedText>
) : (
<SharedText>Create League</SharedText>
)}
</SharedButton>
<Stack direction="row" align="center" gap={2}>
{loading ? <Icon icon={Loader2} size={4} animate="spin" /> : <Icon icon={Sparkles} size={4} />}
<Text>{loading ? 'Creating' : 'Create League'}</Text>
</Stack>
</Button>
)}
</SharedBox>
</SharedBox>
</Box>
</Box>
{/* Helper text */}
<SharedText size="xs" color="text-gray-500" align="center" block mt={4}>
<Text size="xs" color="text-gray-500" align="center" block mt={4}>
This will create your league and its first season. You can edit both later.
</SharedText>
</SharedBox>
</Text>
</Box>
);
}

View File

@@ -1,25 +1,18 @@
'use client';
import { DashboardControlBar } from '@/components/dashboard/DashboardControlBar';
import { DashboardKpiRow } from '@/components/dashboard/DashboardKpiRow';
import { DashboardRail } from '@/components/dashboard/DashboardRail';
import { DashboardShell } from '@/components/dashboard/DashboardShell';
import { RecentActivityTable, type ActivityItem } from '@/components/dashboard/RecentActivityTable';
import { TelemetryPanel } from '@/components/dashboard/TelemetryPanel';
import { routes } from '@/lib/routing/RouteConfig';
import type { DashboardViewData } from '@/lib/view-data/DashboardViewData';
import { Avatar } from '@/ui/Avatar';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { IconButton } from '@/ui/IconButton';
import { Grid } from '@/ui/Grid';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Bell, Calendar, LayoutDashboard, Search, Settings, Trophy, Users } from 'lucide-react';
import { useRouter } from 'next/navigation';
interface DashboardTemplateProps {
viewData: DashboardViewData;
onNavigateToRaces: () => void;
}
/**
@@ -29,8 +22,10 @@ interface DashboardTemplateProps {
* Composes semantic dashboard components into a high-density data environment.
* Complies with architectural constraints by using UI primitives.
*/
export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
const router = useRouter();
export function DashboardTemplate({
viewData,
onNavigateToRaces,
}: DashboardTemplateProps) {
const {
currentDriver,
nextRace,
@@ -43,11 +38,11 @@ export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
} = viewData;
const kpiItems = [
{ label: 'Rating', value: currentDriver.rating, color: 'var(--color-telemetry)' },
{ label: 'Rank', value: `#${currentDriver.rank}`, color: 'var(--color-warning)' },
{ label: 'Rating', value: currentDriver.rating, intent: 'primary' as const },
{ label: 'Rank', value: `#${currentDriver.rank}`, intent: 'warning' as const },
{ label: 'Starts', value: currentDriver.totalRaces },
{ label: 'Wins', value: currentDriver.wins, color: 'var(--color-success)' },
{ label: 'Podiums', value: currentDriver.podiums, color: 'var(--color-warning)' },
{ label: 'Wins', value: currentDriver.wins, intent: 'success' as const },
{ label: 'Podiums', value: currentDriver.podiums, intent: 'warning' as const },
{ label: 'Leagues', value: activeLeaguesCount },
];
@@ -59,68 +54,8 @@ export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
status: item.type === 'race_result' ? 'success' : 'info'
}));
const railContent = (
<DashboardRail>
<Stack direction="col" align="center" gap={6} fullWidth>
<Box h="8" w="8" rounded="sm" bg="primary-accent" display="flex" alignItems="center" justifyContent="center">
<Text size="xs" weight="bold">GP</Text>
</Box>
<IconButton
icon={LayoutDashboard}
onClick={() => router.push(routes.protected.dashboard)}
variant="ghost"
color="primary-accent"
/>
<IconButton
icon={Trophy}
onClick={() => router.push(routes.public.leagues)}
variant="ghost"
color="var(--color-text-low)"
/>
<IconButton
icon={Calendar}
onClick={() => router.push(routes.public.races)}
variant="ghost"
color="var(--color-text-low)"
/>
<IconButton
icon={Users}
onClick={() => router.push(routes.public.teams)}
variant="ghost"
color="var(--color-text-low)"
/>
</Stack>
<Box mt="auto" display="flex" flexDirection="col" alignItems="center" gap={6} pb={4}>
<IconButton
icon={Settings}
onClick={() => router.push(routes.protected.profile)}
variant="ghost"
color="var(--color-text-low)"
/>
</Box>
</DashboardRail>
);
const controlBarActions = (
<Stack direction="row" align="center" gap={4}>
<IconButton icon={Search} variant="ghost" color="var(--color-text-low)" />
<Box position="relative">
<IconButton icon={Bell} variant="ghost" color="var(--color-text-low)" />
<Box position="absolute" top="0" right="0" h="1.5" w="1.5" rounded="full" bg="critical-red" />
</Box>
<Avatar
src={currentDriver.avatarUrl}
alt={currentDriver.name}
size={32}
/>
</Stack>
);
return (
<DashboardShell
rail={railContent}
controlBar={<DashboardControlBar title="Telemetry Workspace" actions={controlBarActions} />}
>
<Stack gap={6}>
{/* KPI Overview */}
<DashboardKpiRow items={kpiItems} />
@@ -132,14 +67,14 @@ export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
<TelemetryPanel title="Active Session">
<Box display="flex" alignItems="center" justifyContent="between">
<Box>
<Text size="xs" color="var(--color-text-low)" mb={1} block>Next Event</Text>
<Text size="xs" color="text-gray-500" mb={1} block>Next Event</Text>
<Text size="lg" weight="bold" block>{nextRace.track}</Text>
<Text size="xs" color="var(--color-telemetry)" font="mono" block>{nextRace.car}</Text>
<Text size="xs" color="primary-accent" font="mono" block>{nextRace.car}</Text>
</Box>
<Box textAlign="right">
<Text size="xs" color="var(--color-text-low)" mb={1} block>Starts In</Text>
<Text size="xl" font="mono" weight="bold" color="var(--color-warning)" block>{nextRace.timeUntil}</Text>
<Text size="xs" color="var(--color-text-low)" block>{nextRace.formattedDate} @ {nextRace.formattedTime}</Text>
<Text size="xs" color="text-gray-500" mb={1} block>Starts In</Text>
<Text size="xl" font="mono" weight="bold" color="warning-amber" block>{nextRace.timeUntil}</Text>
<Text size="xs" color="text-gray-500" block>{nextRace.formattedDate} @ {nextRace.formattedTime}</Text>
</Box>
</Box>
</TelemetryPanel>
@@ -150,7 +85,7 @@ export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
<RecentActivityTable items={activityItems} />
) : (
<Box py={8} textAlign="center">
<Text italic color="var(--color-text-low)">No recent activity recorded.</Text>
<Text italic color="text-gray-500">No recent activity recorded.</Text>
</Box>
)}
</TelemetryPanel>
@@ -164,18 +99,18 @@ export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
{hasLeagueStandings ? (
<Stack direction="col" gap={3}>
{leagueStandings.map((standing) => (
<Box key={standing.leagueId} display="flex" alignItems="center" justifyContent="between" borderBottom borderColor="rgba(35, 39, 43, 0.3)" pb={2}>
<Box key={standing.leagueId} display="flex" alignItems="center" justifyContent="between" borderBottom borderColor="rgba(255, 255, 255, 0.1)" pb={2}>
<Box>
<Text size="xs" weight="bold" truncate block maxWidth="180px">{standing.leagueName}</Text>
<Text size="xs" color="var(--color-text-low)" block>Pos: {standing.position} / {standing.totalDrivers}</Text>
<Text size="xs" color="text-gray-500" block>Pos: {standing.position} / {standing.totalDrivers}</Text>
</Box>
<Text size="sm" font="mono" weight="bold" color="var(--color-telemetry)">{standing.points} PTS</Text>
<Text size="sm" font="mono" weight="bold" color="primary-accent">{standing.points} PTS</Text>
</Box>
))}
</Stack>
) : (
<Box py={4} textAlign="center">
<Text italic color="var(--color-text-low)">No active championships.</Text>
<Text italic color="text-gray-500">No active championships.</Text>
</Box>
)}
</TelemetryPanel>
@@ -183,21 +118,21 @@ export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
<TelemetryPanel title="Upcoming Schedule">
<Stack direction="col" gap={4}>
{upcomingRaces.slice(0, 3).map((race) => (
<Box key={race.id} group cursor="pointer">
<Box key={race.id} cursor="pointer">
<Box display="flex" justifyContent="between" alignItems="start" mb={1}>
<Text size="xs" weight="bold" groupHoverTextColor="var(--color-primary)" transition>{race.track}</Text>
<Text size="xs" font="mono" color="var(--color-text-low)">{race.timeUntil}</Text>
<Text size="xs" weight="bold">{race.track}</Text>
<Text size="xs" font="mono" color="text-gray-500">{race.timeUntil}</Text>
</Box>
<Box display="flex" justifyContent="between">
<Text size="xs" color="var(--color-text-low)">{race.car}</Text>
<Text size="xs" color="var(--color-text-low)">{race.formattedDate}</Text>
<Text size="xs" color="text-gray-500">{race.car}</Text>
<Text size="xs" color="text-gray-500">{race.formattedDate}</Text>
</Box>
</Box>
))}
<Button
variant="secondary"
fullWidth
onClick={() => router.push(routes.public.races)}
onClick={onNavigateToRaces}
>
<Text size="xs" weight="bold" uppercase letterSpacing="widest">View Full Schedule</Text>
</Button>
@@ -206,6 +141,6 @@ export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
</Stack>
</Box>
</Grid>
</DashboardShell>
</Stack>
);
}

View File

@@ -7,8 +7,9 @@ import { DriverTableRow } from '@/components/drivers/DriverTableRow';
import { EmptyState } from '@/ui/EmptyState';
import type { DriversViewData } from '@/lib/types/view-data/DriversViewData';
import { Container } from '@/ui/Container';
import { Stack } from '@/ui/Stack';
import { Group } from '@/ui/Group';
import { Search } from 'lucide-react';
import React from 'react';
interface DriversTemplateProps {
viewData: DriversViewData | null;
@@ -27,14 +28,9 @@ export function DriversTemplate({
onDriverClick,
onViewLeaderboard
}: DriversTemplateProps) {
const drivers = viewData?.drivers || [];
const totalRaces = viewData?.totalRaces || 0;
const totalWins = viewData?.totalWins || 0;
const activeCount = viewData?.activeCount || 0;
return (
<Container size="lg" py={8}>
<Stack gap={10}>
<Group direction="column" gap={10} fullWidth>
<DriversDirectoryHeader
totalDriversLabel={viewData?.totalDriversLabel || '0'}
activeDriversLabel={viewData?.activeCountLabel || '0'}
@@ -73,7 +69,7 @@ export function DriversTemplate({
}}
/>
)}
</Stack>
</Group>
</Container>
);
}

View File

@@ -15,12 +15,8 @@ import { CompanionAutomationMockup } from '@/components/mockups/CompanionAutomat
import { RaceHistoryMockup } from '@/components/mockups/RaceHistoryMockup';
import { SimPlatformMockup } from '@/components/mockups/SimPlatformMockup';
import { ModeGuard } from '@/components/shared/ModeGuard';
import { DiscoverySection } from '@/ui/DiscoverySection';
import { Box } from '@/ui/Box';
import { Container } from '@/ui/Container';
import { Heading } from '@/ui/Heading';
import { Grid } from '@/ui/Grid';
import { Section } from '@/ui/Section';
import { Text } from '@/ui/Text';
export interface HomeViewData {
isAlpha: boolean;
@@ -53,7 +49,7 @@ interface HomeTemplateProps {
*/
export function HomeTemplate({ viewData }: HomeTemplateProps) {
return (
<Box color="text-white">
<Box as="main">
{/* Hero Section */}
<HomeHeader
title="Modern Motorsport Infrastructure."
@@ -146,29 +142,15 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
{/* Discovery Grid */}
<ModeGuard feature="alpha_discovery">
<Section py={24} variant="dark">
<Container>
<Box maxWidth="2xl" mb={16}>
<Box display="flex" alignItems="center" borderLeft borderStyle="solid" borderWidth="2px" borderColor="primary-accent" px={4} mb={4}>
<Text size="xs" weight="bold" uppercase letterSpacing="widest" color="text-primary-accent">
Live Ecosystem
</Text>
</Box>
<Heading level={2} fontSize={{ base: '3xl', md: '4xl' }} weight="bold" letterSpacing="tight" color="text-white" mb={6}>
DISCOVER THE GRID
</Heading>
<Text size="lg" color="text-gray-400" leading="relaxed">
Explore leagues, teams, and races that make up the GridPilot ecosystem.
</Text>
</Box>
<Grid cols={1} lgCols={3} gap={8}>
<LeagueSummaryPanel leagues={viewData.topLeagues} />
<TeamSummaryPanel teams={viewData.teams} />
<RecentRacesPanel races={viewData.upcomingRaces} />
</Grid>
</Container>
</Section>
<DiscoverySection
title="DISCOVER THE GRID"
subtitle="Live Ecosystem"
description="Explore leagues, teams, and races that make up the GridPilot ecosystem."
>
<LeagueSummaryPanel leagues={viewData.topLeagues} />
<TeamSummaryPanel teams={viewData.teams} />
<RecentRacesPanel races={viewData.upcomingRaces} />
</DiscoverySection>
</ModeGuard>
{/* CTA & FAQ */}

View File

@@ -95,7 +95,7 @@ export function LeagueAdminScheduleTemplate({
<Stack direction="row" align="center" justify="between">
<Text size="sm" color="text-gray-300">
Status: <Text weight="medium" color="text-white">{publishedLabel}</Text>
Status: <Text weight="medium" color="white">{publishedLabel}</Text>
</Text>
<Button
onClick={onPublishToggle}
@@ -107,12 +107,12 @@ export function LeagueAdminScheduleTemplate({
</Button>
</Stack>
<Box pt={6} borderTop="1px solid" borderColor="border-neutral-800">
<Box pt={6} borderTop borderColor="rgba(255,255,255,0.1)">
<Box mb={4}>
<Heading level={2}>{isEditing ? 'Edit race' : 'Add race'}</Heading>
</Box>
<Grid cols={3} gap={4}>
<Grid responsiveGridCols={{ base: 1, md: 3 }} gap={4}>
<Box>
<Text size="sm" color="text-gray-300" block mb={2}>Track</Text>
<Input
@@ -161,7 +161,7 @@ export function LeagueAdminScheduleTemplate({
</Stack>
</Box>
<Box pt={6} borderTop="1px solid" borderColor="border-neutral-800">
<Box pt={6} borderTop borderColor="rgba(255,255,255,0.1)">
<Box mb={4}>
<Heading level={2}>Races</Heading>
</Box>
@@ -178,7 +178,7 @@ export function LeagueAdminScheduleTemplate({
>
<Stack direction="row" align="center" justify="between">
<Box>
<Text weight="medium" color="text-white" block>{race.name}</Text>
<Text weight="medium" color="white" block>{race.name}</Text>
<Text size="xs" color="text-gray-400" block mt={1}>{race.scheduledAt}</Text>
</Box>

View File

@@ -23,19 +23,19 @@ export function LeagueSettingsTemplate({ viewData }: LeagueSettingsTemplateProps
<Surface variant="dark" border rounded="lg" padding={6}>
<Stack gap={6}>
<Stack direction="row" align="center" gap={3}>
<Box p={2} bg="bg-primary-blue/10" rounded="md" border borderColor="border-primary-blue/20">
<Icon icon={Settings} size={5} color="text-primary-blue" />
<Box p={2} bg="rgba(25,140,255,0.1)" rounded="md" border borderColor="rgba(25,140,255,0.2)">
<Icon icon={Settings} size={5} color="primary-accent" />
</Box>
<Box>
<Heading level={5} color="text-primary-blue">LEAGUE INFORMATION</Heading>
<Heading level={5} color="primary-accent">LEAGUE INFORMATION</Heading>
<Text size="xs" color="text-gray-500">Basic league details and identification</Text>
</Box>
</Stack>
<Grid cols={1} mdCols={2} gap={6}>
<Grid responsiveGridCols={{ base: 1, md: 2 }} gap={6}>
<InfoItem label="Name" value={viewData.league.name} />
<InfoItem label="Visibility" value={viewData.league.visibility} capitalize />
<GridItem colSpan={2}>
<GridItem responsiveColSpan={{ base: 1, md: 2 }}>
<InfoItem label="Description" value={viewData.league.description} />
</GridItem>
<InfoItem label="Created" value={new Date(viewData.league.createdAt).toLocaleDateString()} />
@@ -48,16 +48,16 @@ export function LeagueSettingsTemplate({ viewData }: LeagueSettingsTemplateProps
<Surface variant="dark" border rounded="lg" padding={6}>
<Stack gap={6}>
<Stack direction="row" align="center" gap={3}>
<Box p={2} bg="bg-performance-green/10" rounded="md" border borderColor="border-performance-green/20">
<Icon icon={Trophy} size={5} color="text-performance-green" />
<Box p={2} bg="rgba(16,185,129,0.1)" rounded="md" border borderColor="rgba(16,185,129,0.2)">
<Icon icon={Trophy} size={5} color="text-success-green" />
</Box>
<Box>
<Heading level={5} color="text-performance-green">CONFIGURATION</Heading>
<Heading level={5} color="text-success-green">CONFIGURATION</Heading>
<Text size="xs" color="text-gray-500">League rules and participation limits</Text>
</Box>
</Stack>
<Grid cols={1} mdCols={2} gap={6}>
<Grid responsiveGridCols={{ base: 1, md: 2 }} gap={6}>
<ConfigItem icon={Users} label="Max Drivers" value={viewData.config.maxDrivers} />
<ConfigItem icon={Shield} label="Require Approval" value={viewData.config.requireApproval ? 'Yes' : 'No'} />
<ConfigItem icon={Clock} label="Allow Late Join" value={viewData.config.allowLateJoin ? 'Yes' : 'No'} />
@@ -69,12 +69,12 @@ export function LeagueSettingsTemplate({ viewData }: LeagueSettingsTemplateProps
{/* Note about forms */}
<Surface variant="dark" border rounded="lg" padding={8}>
<Stack align="center" gap={4}>
<Box p={4} bg="bg-warning-amber/10" rounded="full" border borderColor="border-warning-amber/20">
<Icon icon={Settings} size={8} color="text-warning-amber" />
<Box p={4} bg="rgba(245,158,11,0.1)" rounded="full" border borderColor="rgba(245,158,11,0.2)">
<Icon icon={Settings} size={8} color="warning-amber" />
</Box>
<Box textAlign="center">
<Heading level={3}>Settings Management</Heading>
<Text size="sm" color="text-gray-400" mt={2} display="block">
<Text size="sm" color="text-gray-400" mt={2} block>
Form-based editing and ownership transfer functionality will be implemented in future updates.
</Text>
</Box>
@@ -88,8 +88,8 @@ export function LeagueSettingsTemplate({ viewData }: LeagueSettingsTemplateProps
function InfoItem({ label, value, capitalize }: { label: string, value: string, capitalize?: boolean }) {
return (
<Box>
<Text size="xs" weight="bold" color="text-gray-500" display="block" mb={1} letterSpacing="wider">{label.toUpperCase()}</Text>
<Text color="text-white" weight="medium">{capitalize ? value.toUpperCase() : value}</Text>
<Text size="xs" weight="bold" color="text-gray-500" block mb={1} letterSpacing="wider">{label.toUpperCase()}</Text>
<Text color="white" weight="medium">{capitalize ? value.toUpperCase() : value}</Text>
</Box>
);
}
@@ -97,12 +97,12 @@ function InfoItem({ label, value, capitalize }: { label: string, value: string,
function ConfigItem({ icon, label, value }: { icon: LucideIcon, label: string, value: string | number }) {
return (
<Stack direction="row" align="center" gap={4}>
<Box center w={10} h={10} rounded="lg" bg="bg-white/5">
<Box display="flex" alignItems="center" justifyContent="center" w={10} h={10} rounded="lg" bg="rgba(255,255,255,0.05)">
<Icon icon={icon} size={5} color="text-gray-400" />
</Box>
<Box>
<Text size="xs" weight="bold" color="text-gray-500" display="block" letterSpacing="wider">{label.toUpperCase()}</Text>
<Text color="text-white" weight="medium">{value}</Text>
<Text size="xs" weight="bold" color="text-gray-500" block letterSpacing="wider">{label.toUpperCase()}</Text>
<Text color="white" weight="medium">{value}</Text>
</Box>
</Stack>
);

View File

@@ -2,15 +2,13 @@
import { WalletSummaryPanel } from '@/components/leagues/WalletSummaryPanel';
import type { LeagueWalletViewData } from '@/lib/view-data/leagues/LeagueWalletViewData';
import {
SharedBox,
SharedButton,
SharedStack,
SharedText,
SharedIcon,
SharedContainer
} from '@/components/shared/UIComponents';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Container } from '@/ui/Container';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Download } from 'lucide-react';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
@@ -21,23 +19,23 @@ interface LeagueWalletTemplateProps extends TemplateProps<LeagueWalletViewData>
transactions: any[];
}
export function LeagueWalletTemplate({ viewData, onExport, transactions }: LeagueWalletTemplateProps) {
export function LeagueWalletTemplate({ viewData, onExport }: LeagueWalletTemplateProps) {
return (
<SharedContainer size="lg">
<SharedBox paddingY={8}>
<Container size="lg">
<Box paddingY={8}>
{/* Header */}
<SharedBox display="flex" alignItems="center" justifyContent="between" mb={8}>
<SharedBox>
<Box display="flex" alignItems="center" justifyContent="between" mb={8}>
<Box>
<Heading level={1}>League Wallet</Heading>
<SharedText color="text-gray-400">Manage your league's finances and payouts</SharedText>
</SharedBox>
<SharedButton variant="secondary" onClick={onExport}>
<SharedStack direction="row" align="center" gap={2}>
<SharedIcon icon={Download} size={4} />
<SharedText>Export</SharedText>
</SharedStack>
</SharedButton>
</SharedBox>
<Text color="text-gray-400">Manage your league's finances and payouts</Text>
</Box>
<Button variant="secondary" onClick={onExport}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Download} size={4} />
<Text>Export</Text>
</Stack>
</Button>
</Box>
<WalletSummaryPanel
formattedBalance={viewData.formattedBalance}
@@ -48,13 +46,13 @@ export function LeagueWalletTemplate({ viewData, onExport, transactions }: Leagu
/>
{/* Alpha Notice */}
<SharedBox mt={6} rounded="lg" bg="bg-warning-amber/10" border borderColor="border-warning-amber/30" p={4}>
<SharedText size="xs" color="text-gray-400">
<SharedText weight="bold" color="text-warning-amber">Alpha Note:</SharedText> Wallet management is demonstration-only.
<Box mt={6} rounded="lg" bg="rgba(245,158,11,0.1)" border borderColor="rgba(245,158,11,0.3)" p={4}>
<Text size="xs" color="text-gray-400">
<Text weight="bold" color="warning-amber">Alpha Note:</Text> Wallet management is demonstration-only.
Real payment processing and bank integrations will be available when the payment system is fully implemented.
</SharedText>
</SharedBox>
</SharedBox>
</SharedContainer>
</Text>
</Box>
</Box>
</Container>
);
}

View File

@@ -3,29 +3,20 @@
import { LeagueCard } from '@/components/leagues/LeagueCardWrapper';
import type { LeaguesViewData } from '@/lib/view-data/LeaguesViewData';
import { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel';
import {
SharedBox,
SharedButton,
SharedStack,
SharedText,
SharedIcon,
SharedContainer
} from '@/components/shared/UIComponents';
import { Heading } from '@/ui/Heading';
import { Input } from '@/ui/Input';
import { Button } from '@/ui/Button';
import { Group } from '@/ui/Group';
import { Grid } from '@/ui/Grid';
import { Container } from '@/ui/Container';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
import { Section } from '@/ui/Section';
import { StatusDot } from '@/ui/StatusDot';
import {
Award,
Clock,
Flag,
Flame,
Globe,
Plus,
Search,
Sparkles,
Target,
Timer,
Trophy,
Users,
type LucideIcon,
} from 'lucide-react';
import React from 'react';
@@ -77,44 +68,42 @@ export function LeaguesTemplate({
onClearFilters,
}: LeaguesTemplateProps) {
return (
<SharedBox minHeight="screen" bg="zinc-950" color="text-zinc-200">
<SharedBox maxWidth="7xl" mx="auto" px={{ base: 4, sm: 6, lg: 8 }} py={12}>
<Container size="xl" py={12}>
<Group direction="column" gap={16} fullWidth>
{/* Hero */}
<SharedBox as="header" display="flex" flexDirection={{ base: 'col', md: 'row' }} alignItems={{ base: 'start', md: 'end' }} justifyContent="between" gap={8} mb={16}>
<SharedStack gap={4}>
<SharedBox display="flex" alignItems="center" gap={3} color="text-blue-500">
<Trophy size={24} />
<SharedText fontSize="xs" weight="bold" uppercase letterSpacing="widest">Competition Hub</SharedText>
</SharedBox>
<Heading level={1} fontSize="5xl" weight="bold" color="text-white">
Find Your <SharedText as="span" color="text-blue-500">Grid</SharedText>
<Group direction={{ base: 'column', md: 'row' } as any} align={{ base: 'start', md: 'end' } as any} justify="between" gap={8} fullWidth>
<Group direction="column" gap={4}>
<Group direction="row" align="center" gap={3}>
<Icon icon={Trophy} size={6} intent="primary" />
<Text size="xs" weight="bold" uppercase letterSpacing="widest" color="text-primary-accent">Competition Hub</Text>
</Group>
<Heading level={1} size="5xl" weight="bold">
Find Your <Text as="span" color="text-primary-accent">Grid</Text>
</Heading>
<SharedText color="text-zinc-400" maxWidth="md" leading="relaxed">
<Text variant="low" maxWidth="md">
From casual sprints to epic endurance battles discover the perfect league for your racing style.
</SharedText>
</SharedStack>
</Text>
</Group>
<SharedBox display="flex" alignItems="center" gap={4}>
<SharedBox display="flex" flexDirection="col" alignItems="end">
<SharedText fontSize="2xl" weight="bold" color="text-white" font="mono">{viewData.leagues.length}</SharedText>
<SharedText weight="bold" color="text-zinc-500" uppercase letterSpacing="widest" fontSize="10px">Active Leagues</SharedText>
</SharedBox>
<SharedBox w="px" h="8" bg="zinc-800" />
<SharedButton
<Group direction="row" align="center" gap={4}>
<Group direction="column" align="end">
<Text size="2xl" weight="bold" font="mono">{viewData.leagues.length}</Text>
<Text weight="bold" variant="low" uppercase letterSpacing="widest" size="xs">Active Leagues</Text>
</Group>
<StatusDot intent="telemetry" size="lg" />
<Button
onClick={onCreateLeague}
variant="primary"
size="lg"
icon={<Plus size={16} />}
>
<SharedStack direction="row" align="center" gap={2}>
<Plus size={16} />
Create League
</SharedStack>
</SharedButton>
</SharedBox>
</SharedBox>
Create League
</Button>
</Group>
</Group>
{/* Search & Filters */}
<SharedBox as="section" display="flex" flexDirection="col" gap={8} mb={12}>
<Group direction="column" gap={8} fullWidth>
<Input
type="text"
placeholder="Search leagues by name, description, or game..."
@@ -123,35 +112,29 @@ export function LeaguesTemplate({
icon={<Search size={20} />}
/>
<SharedBox as="nav" display="flex" flexWrap="wrap" gap={2}>
<Group direction="row" wrap gap={2} fullWidth>
{categories.map((category) => {
const isActive = activeCategory === category.id;
const CategoryIcon = category.icon;
return (
<SharedButton
<Button
key={category.id}
onClick={() => onCategoryChange(category.id)}
variant={isActive ? 'primary' : 'secondary'}
size="sm"
icon={<CategoryIcon size={14} />}
>
<SharedStack direction="row" align="center" gap={2}>
<SharedBox
color={!isActive && category.color ? category.color : undefined}
>
<CategoryIcon size={14} />
</SharedBox>
<SharedText>{category.label}</SharedText>
</SharedStack>
</SharedButton>
{category.label}
</Button>
);
})}
</SharedBox>
</SharedBox>
</Group>
</Group>
{/* Grid */}
<SharedBox as="main">
<Group direction="column" fullWidth>
{filteredLeagues.length > 0 ? (
<SharedBox display="grid" responsiveGridCols={{ base: 1, md: 2, lg: 3 }} gap={6}>
<Grid cols={{ base: 1, md: 2, lg: 3 }} gap={6}>
{filteredLeagues.map((league) => (
<LeagueCard
key={league.id}
@@ -159,25 +142,25 @@ export function LeaguesTemplate({
onClick={() => onLeagueClick(league.id)}
/>
))}
</SharedBox>
</Grid>
) : (
<SharedBox display="flex" flexDirection="col" alignItems="center" justifyContent="center" py={24} border borderStyle="dashed" borderColor="zinc-800" bg="zinc-900/20">
<SharedBox color="text-zinc-800" mb={4}>
<Search size={48} />
</SharedBox>
<Heading level={3} fontSize="xl" weight="bold" color="text-zinc-500">No Leagues Found</Heading>
<SharedText color="text-zinc-600" size="sm" mt={2}>Try adjusting your search or filters</SharedText>
<SharedButton
variant="ghost"
style={{ marginTop: '1.5rem' }}
onClick={onClearFilters}
>
Clear All Filters
</SharedButton>
</SharedBox>
<Section variant="dark" padding="lg">
<Group direction="column" align="center" justify="center" fullWidth>
<Icon icon={Search} size={12} intent="low" />
<Heading level={3} weight="bold">No Leagues Found</Heading>
<Text variant="low" size="sm">Try adjusting your search or filters</Text>
<Button
variant="secondary"
onClick={onClearFilters}
style={{ marginTop: '1.5rem' }}
>
Clear All Filters
</Button>
</Group>
</Section>
)}
</SharedBox>
</SharedBox>
</SharedBox>
</Group>
</Group>
</Container>
);
}

View File

@@ -1,6 +1,6 @@
'use client';
import { UploadDropzone } from '@/ui/UploadDropzone';
import { UploadDropzone } from '@/components/shared/UploadDropzone';
import { routes } from '@/lib/routing/RouteConfig';
import {
SharedBox,

View File

@@ -39,7 +39,7 @@ export function ProfileTemplate({
return (
<Stack align="center" gap={4} mb={8}>
<Surface variant="muted" rounded="xl" border padding={4}>
<Icon icon={User} size={8} color="#3b82f6" />
<Icon icon={User} size={8} color="var(--color-primary)" />
</Surface>
<Box>
<Heading level={1}>Create Your Driver Profile</Heading>
@@ -93,7 +93,7 @@ export function ProfileTemplate({
{viewData.teamMemberships.length > 0 && (
<Box as="section" aria-labelledby="teams-heading">
<Stack gap={4}>
<Heading level={3} id="teams-heading" fontSize="1.125rem">Teams</Heading>
<Heading level={3} id="teams-heading">Teams</Heading>
<TeamMembershipGrid
memberships={viewData.teamMemberships.map(m => ({
team: { id: m.teamId, name: m.teamName },
@@ -109,8 +109,8 @@ export function ProfileTemplate({
<Box as="section" aria-labelledby="achievements-heading">
<Stack gap={4}>
<Stack direction="row" justify="between" align="center">
<Heading level={3} id="achievements-heading" fontSize="1.125rem">Achievements</Heading>
<Text size="sm" color="#6b7280">{viewData.extendedProfile.achievements.length} earned</Text>
<Heading level={3} id="achievements-heading">Achievements</Heading>
<Text size="sm" color="text-gray-500">{viewData.extendedProfile.achievements.length} earned</Text>
</Stack>
<AchievementGrid
achievements={viewData.extendedProfile.achievements.map(a => ({
@@ -128,7 +128,7 @@ export function ProfileTemplate({
{activeTab === 'history' && (
<Box as="section" aria-labelledby="history-heading">
<Stack gap={4}>
<Heading level={3} id="history-heading" fontSize="1.125rem">Race History</Heading>
<Heading level={3} id="history-heading">Race History</Heading>
<Card>
<SessionHistoryTable results={[]} />
</Card>
@@ -139,14 +139,14 @@ export function ProfileTemplate({
{activeTab === 'stats' && viewData.stats && (
<Box as="section" aria-labelledby="stats-heading">
<Stack gap={4}>
<Heading level={3} id="stats-heading" fontSize="1.125rem">Performance Overview</Heading>
<Heading level={3} id="stats-heading">Performance Overview</Heading>
<Card>
<ProfileStatGrid
stats={[
{ label: 'Races', value: viewData.stats.totalRacesLabel },
{ label: 'Wins', value: viewData.stats.winsLabel, color: '#10b981' },
{ label: 'Podiums', value: viewData.stats.podiumsLabel, color: '#f59e0b' },
{ label: 'Consistency', value: viewData.stats.consistencyLabel, color: '#3b82f6' },
{ label: 'Wins', value: viewData.stats.winsLabel, intent: 'success' },
{ label: 'Podiums', value: viewData.stats.podiumsLabel, intent: 'telemetry' },
{ label: 'Consistency', value: viewData.stats.consistencyLabel, intent: 'primary' },
]}
/>
</Card>

View File

@@ -8,12 +8,13 @@ import { RaceScheduleTable } from '@/components/races/RaceScheduleTable';
import { RaceSidebar } from '@/components/races/RaceSidebar';
import type { SessionStatus } from '@/components/races/SessionStatusBadge';
import type { RacesViewData } from '@/lib/view-data/RacesViewData';
import { Box } from '@/ui/Box';
import { Container } from '@/ui/Container';
import { Grid } from '@/ui/Grid';
import { GridItem } from '@/ui/GridItem';
import { Stack } from '@/ui/Stack';
import { Group } from '@/ui/Group';
import { Text } from '@/ui/Text';
import { Panel } from '@/ui/Panel';
import React from 'react';
export type TimeFilter = 'all' | 'upcoming' | 'live' | 'past';
export type RaceStatusFilter = 'scheduled' | 'running' | 'completed' | 'cancelled' | 'all';
@@ -50,78 +51,77 @@ export function RacesTemplate({
setShowFilterModal,
}: RacesTemplateProps) {
return (
<Box as="main" minHeight="screen" bg="bg-base-black" py={8}>
<Container size="lg">
<Stack gap={8}>
<RacePageHeader
totalCount={viewData.totalCount}
scheduledCount={viewData.scheduledCount}
runningCount={viewData.runningCount}
completedCount={viewData.completedCount}
/>
<Container size="lg" py={8}>
<Group direction="column" gap={8} fullWidth>
<RacePageHeader
totalCount={viewData.totalCount}
scheduledCount={viewData.scheduledCount}
runningCount={viewData.runningCount}
completedCount={viewData.completedCount}
/>
<LiveRacesBanner
liveRaces={viewData.liveRaces}
onRaceClick={onRaceClick}
/>
<LiveRacesBanner
liveRaces={viewData.liveRaces}
onRaceClick={onRaceClick}
/>
<Grid cols={12} gap={6}>
<GridItem colSpan={12} lgSpan={8}>
<Stack gap={6}>
<RaceFilterBar
timeFilter={timeFilter}
setTimeFilter={setTimeFilter}
leagueFilter={leagueFilter}
setLeagueFilter={setLeagueFilter}
leagues={viewData.leagues}
onShowMoreFilters={() => setShowFilterModal(true)}
/>
<Box as="section" bg="bg-surface-charcoal" border borderColor="border-outline-steel" overflow="hidden">
<Box p={4} borderBottom borderColor="border-outline-steel" bg="bg-base-black" bgOpacity={0.2}>
<Text size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="widest">Race Schedule</Text>
</Box>
<RaceScheduleTable
races={viewData.races.map(race => ({
id: race.id,
track: race.track,
car: race.car,
leagueName: race.leagueName,
time: race.timeLabel,
status: race.status as SessionStatus
}))}
onRaceClick={onRaceClick}
/>
</Box>
</Stack>
</GridItem>
<GridItem colSpan={12} lgSpan={4}>
<RaceSidebar
upcomingRaces={viewData.upcomingRaces}
recentResults={viewData.recentResults}
onRaceClick={onRaceClick}
<Grid cols={12} gap={6}>
<GridItem colSpan={12} lgSpan={8}>
<Group direction="column" gap={6} fullWidth>
<RaceFilterBar
timeFilter={timeFilter}
setTimeFilter={setTimeFilter}
leagueFilter={leagueFilter}
setLeagueFilter={setLeagueFilter}
leagues={viewData.leagues}
onShowMoreFilters={() => setShowFilterModal(true)}
/>
</GridItem>
</Grid>
<RaceFilterModal
isOpen={showFilterModal}
onClose={() => setShowFilterModal(false)}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
leagueFilter={leagueFilter}
setLeagueFilter={setLeagueFilter}
timeFilter={timeFilter}
setTimeFilter={setTimeFilter}
searchQuery=""
setSearchQuery={() => {}}
leagues={viewData.leagues}
showSearch={false}
showTimeFilter={false}
/>
</Stack>
</Container>
</Box>
<Panel
title="Race Schedule"
variant="dark"
padding={0}
>
<RaceScheduleTable
races={viewData.races.map(race => ({
id: race.id,
track: race.track,
car: race.car,
leagueName: race.leagueName,
time: race.timeLabel,
status: race.status as SessionStatus
}))}
onRaceClick={onRaceClick}
/>
</Panel>
</Group>
</GridItem>
<GridItem colSpan={12} lgSpan={4}>
<RaceSidebar
upcomingRaces={viewData.upcomingRaces}
recentResults={viewData.recentResults}
onRaceClick={onRaceClick}
/>
</GridItem>
</Grid>
<RaceFilterModal
isOpen={showFilterModal}
onClose={() => setShowFilterModal(false)}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
leagueFilter={leagueFilter}
setLeagueFilter={setLeagueFilter}
timeFilter={timeFilter}
setTimeFilter={setTimeFilter}
searchQuery=""
setSearchQuery={() => {}}
leagues={viewData.leagues}
showSearch={false}
showTimeFilter={false}
/>
</Group>
</Container>
);
}

View File

@@ -42,11 +42,11 @@ export function RosterAdminTemplate({
<Box>
<Stack direction="row" align="center" justify="between" mb={4}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={UserPlus} size={4} color="text-primary-blue" />
<Heading level={5} color="text-primary-blue">PENDING JOIN REQUESTS</Heading>
<Icon icon={UserPlus} size={4} color="primary-accent" />
<Heading level={5} color="primary-accent">PENDING JOIN REQUESTS</Heading>
</Stack>
<Box px={2} py={0.5} rounded="md" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20">
<Text size="xs" color="text-primary-blue" weight="bold">{pendingCountLabel}</Text>
<Box px={2} py={0.5} rounded="md" bg="rgba(25,140,255,0.1)" border borderColor="rgba(25,140,255,0.2)">
<Text size="xs" color="primary-accent" weight="bold">{pendingCountLabel}</Text>
</Box>
</Stack>
@@ -58,13 +58,13 @@ export function RosterAdminTemplate({
<Surface variant="dark" border rounded="lg" overflow="hidden">
<Stack gap={0}>
{joinRequests.map((req) => (
<Box key={req.id} p={4} borderBottom borderColor="border-charcoal-outline" hoverBg="bg-white/5" transition>
<Box key={req.id} p={4} borderBottom borderColor="rgba(255,255,255,0.1)">
<Stack direction={{ base: 'col', md: 'row' }} align="center" justify="between" gap={4}>
<Stack gap={1}>
<Text weight="bold" color="text-white">{req.driver.name}</Text>
<Text weight="bold" color="white">{req.driver.name}</Text>
<Text size="xs" color="text-gray-500">{req.formattedRequestedAt}</Text>
{req.message && (
<Text size="sm" color="text-gray-400" mt={1}>&quot;{req.message}&quot;</Text>
<Text size="sm" color="text-gray-400" mt={1}>"{req.message}"</Text>
)}
</Stack>
@@ -91,8 +91,8 @@ export function RosterAdminTemplate({
{/* Members Section */}
<Box>
<Stack direction="row" align="center" gap={2} mb={4}>
<Icon icon={Shield} size={4} color="text-performance-green" />
<Heading level={5} color="text-performance-green">ACTIVE ROSTER</Heading>
<Icon icon={Shield} size={4} color="text-success-green" />
<Heading level={5} color="text-success-green">ACTIVE ROSTER</Heading>
</Stack>
{loading ? (
@@ -114,7 +114,7 @@ export function RosterAdminTemplate({
{members.map((member) => (
<TableRow key={member.driverId}>
<TableCell>
<Text weight="bold" color="text-white">{member.driver.name}</Text>
<Text weight="bold" color="white">{member.driver.name}</Text>
</TableCell>
<TableCell>
<Text size="sm" color="text-gray-400">{member.formattedJoinedAt}</Text>
@@ -124,15 +124,13 @@ export function RosterAdminTemplate({
as="select"
value={member.role}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => onRoleChange(member.driverId, e.target.value as MembershipRole)}
bg="bg-iron-gray"
bg="rgba(255,255,255,0.05)"
border
borderColor="border-charcoal-outline"
borderColor="rgba(255,255,255,0.1)"
rounded="md"
px={2}
py={1}
fontSize="xs"
weight="bold"
color="text-white"
color="white"
>
{roleOptions.map((role) => (
<Box as="option" key={role} value={role}>{role.toUpperCase()}</Box>
@@ -146,8 +144,8 @@ export function RosterAdminTemplate({
size="sm"
>
<Stack direction="row" align="center" gap={1.5}>
<Icon icon={UserMinus} size={3.5} color="text-error-red" />
<Text size="xs" weight="bold" color="text-error-red">REMOVE</Text>
<Icon icon={UserMinus} size={3.5} color="critical-red" />
<Text size="xs" weight="bold" color="critical-red">REMOVE</Text>
</Stack>
</Button>
</TableCell>

View File

@@ -17,8 +17,8 @@ import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Link } from '@/ui/Link';
import { Grid } from '@/ui/Grid';
import { GridItem } from '@/ui/GridItem';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import {
Bell,
Car,
@@ -81,7 +81,7 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
description: a.formattedImpressions ? `${a.formattedImpressions} impressions` : '',
timestamp: a.time,
icon: Clock,
color: a.typeColor || 'text-primary-blue',
color: a.typeColor || 'primary-accent',
}));
return (
@@ -97,7 +97,7 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
<BillingSummaryPanel stats={billingStats} />
{/* Key Metrics */}
<Grid cols={4} gap={4}>
<Grid responsiveGridCols={{ base: 1, sm: 2, lg: 4 }} gap={4}>
<MetricCard
title="Total Impressions"
value={viewData.totalImpressions}
@@ -130,27 +130,30 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
</Grid>
{/* Main Content Grid */}
<Grid cols={12} gap={6}>
<GridItem colSpan={12} lgSpan={8}>
<Grid responsiveGridCols={{ base: 1, lg: 12 }} gap={6}>
<Box responsiveColSpan={{ base: 1, lg: 8 }}>
<Stack gap={6}>
{/* Sponsorship Categories */}
<Box>
<Stack direction="row" align="center" justify="between" mb={4}>
<Heading level={3}>Your Sponsorships</Heading>
<Link href={routes.sponsor.campaigns}>
<Button variant="secondary" size="sm" icon={<Icon icon={ChevronRight} size={4} />}>
View All
<Button variant="secondary" size="sm">
<Stack direction="row" align="center" gap={2}>
<Text>View All</Text>
<Icon icon={ChevronRight} size={4} />
</Stack>
</Button>
</Link>
</Stack>
<Grid cols={5} gap={4}>
<Grid responsiveGridCols={{ base: 2, md: 3, lg: 5 }} gap={4}>
<SponsorshipCategoryCard
icon={Trophy}
title="Leagues"
countLabel={categoryData.leagues.countLabel}
impressionsLabel={categoryData.leagues.impressionsLabel}
color="#3b82f6"
color="primary-accent"
href="/sponsor/campaigns?type=leagues"
/>
<SponsorshipCategoryCard
@@ -158,7 +161,7 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
title="Teams"
countLabel={categoryData.teams.countLabel}
impressionsLabel={categoryData.teams.impressionsLabel}
color="#a855f7"
color="var(--color-warning)"
href="/sponsor/campaigns?type=teams"
/>
<SponsorshipCategoryCard
@@ -166,7 +169,7 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
title="Drivers"
countLabel={categoryData.drivers.countLabel}
impressionsLabel={categoryData.drivers.impressionsLabel}
color="#10b981"
color="var(--color-success)"
href="/sponsor/campaigns?type=drivers"
/>
<SponsorshipCategoryCard
@@ -174,7 +177,7 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
title="Races"
countLabel={categoryData.races.countLabel}
impressionsLabel={categoryData.races.impressionsLabel}
color="#f59e0b"
color="var(--color-warning)"
href="/sponsor/campaigns?type=races"
/>
<SponsorshipCategoryCard
@@ -182,7 +185,7 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
title="Platform Ads"
countLabel={categoryData.platform.countLabel}
impressionsLabel={categoryData.platform.impressionsLabel}
color="#ef4444"
color="critical-red"
href="/sponsor/campaigns?type=platform"
/>
</Grid>
@@ -193,12 +196,15 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
<Stack direction="row" align="center" justify="between" mb={4}>
<Heading level={3}>Top Performing</Heading>
<Link href={routes.public.leagues}>
<Button variant="secondary" size="sm" icon={<Icon icon={Plus} size={4} />}>
Find More
<Button variant="secondary" size="sm">
<Stack direction="row" align="center" gap={2}>
<Icon icon={Plus} size={4} />
<Text>Find More</Text>
</Stack>
</Button>
</Link>
</Stack>
<Grid cols={2} gap={4}>
<Grid responsiveGridCols={{ base: 1, md: 2 }} gap={4}>
<SponsorContractCard
id="sample-1"
type="league"
@@ -226,9 +232,9 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
</Grid>
</Box>
</Stack>
</GridItem>
</Box>
<GridItem colSpan={12} lgSpan={4}>
<Box responsiveColSpan={{ base: 1, lg: 4 }}>
<Stack gap={6}>
{/* Recent Activity */}
<SponsorActivityPanel activities={activities} />
@@ -239,23 +245,35 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
<Heading level={3}>Quick Actions</Heading>
<Stack gap={2}>
<Link href={routes.public.leagues}>
<Button variant="secondary" fullWidth icon={<Icon icon={Trophy} size={4} />}>
Find Leagues to Sponsor
<Button variant="secondary" fullWidth>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Trophy} size={4} />
<Text>Find Leagues to Sponsor</Text>
</Stack>
</Button>
</Link>
<Link href={routes.public.teams}>
<Button variant="secondary" fullWidth icon={<Icon icon={Users} size={4} />}>
Browse Teams
<Button variant="secondary" fullWidth>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Users} size={4} />
<Text>Browse Teams</Text>
</Stack>
</Button>
</Link>
<Link href={routes.public.drivers}>
<Button variant="secondary" fullWidth icon={<Icon icon={Car} size={4} />}>
Discover Drivers
<Button variant="secondary" fullWidth>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Car} size={4} />
<Text>Discover Drivers</Text>
</Stack>
</Button>
</Link>
<Link href={routes.sponsor.billing}>
<Button variant="secondary" fullWidth icon={<Icon icon={DollarSign} size={4} />}>
Manage Billing
<Button variant="secondary" fullWidth>
<Stack direction="row" align="center" gap={2}>
<Icon icon={DollarSign} size={4} />
<Text>Manage Billing</Text>
</Stack>
</Button>
</Link>
</Stack>
@@ -266,8 +284,11 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
{viewData.upcomingRenewals.length > 0 && (
<Card>
<Stack gap={4}>
<Heading level={3} icon={<Icon icon={Bell} size={5} color="#f59e0b" />}>
Upcoming Renewals
<Heading level={3}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Bell} size={5} color="warning-amber" />
<Text>Upcoming Renewals</Text>
</Stack>
</Heading>
<Stack gap={3}>
{viewData.upcomingRenewals.map((renewal) => (
@@ -278,7 +299,7 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
</Card>
)}
</Stack>
</GridItem>
</Box>
</Grid>
</Stack>
</Container>

View File

@@ -2,15 +2,17 @@
import { LeaderboardFiltersBar } from '@/components/leaderboards/LeaderboardFiltersBar';
import type { SkillLevel, SortBy, TeamLeaderboardViewData } from '@/lib/view-data/TeamLeaderboardViewData';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Container } from '@/ui/Container';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack';
import { Group } from '@/ui/Group';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/ui/Table';
import { Text } from '@/ui/Text';
import { Panel } from '@/ui/Panel';
import { Section } from '@/ui/Section';
import { Award, ChevronLeft } from 'lucide-react';
import React from 'react';
interface TeamLeaderboardTemplateProps {
viewData: TeamLeaderboardViewData;
@@ -30,87 +32,88 @@ export function TeamLeaderboardTemplate({
const { searchQuery, filteredAndSortedTeams } = viewData;
return (
<Box bg="base-black" minH="screen">
<Container size="lg" py={12}>
<Stack gap={8}>
{/* Header */}
<Stack direction="row" align="center" justify="between">
<Stack direction="row" align="center" gap={4}>
<Button variant="secondary" size="sm" onClick={onBackToTeams} icon={<Icon icon={ChevronLeft} size={4} />}>
Back
</Button>
<Box>
<Heading level={1} weight="bold">Global Standings</Heading>
<Text color="text-gray-500" size="sm" font="mono" uppercase letterSpacing="widest">Team Performance Index</Text>
</Box>
</Stack>
<Icon icon={Award} size={8} color="warning-amber" />
</Stack>
<Container size="lg" py={12}>
<Group direction="column" gap={8} fullWidth>
{/* Header */}
<Group direction="row" align="center" justify="between" fullWidth>
<Group direction="row" align="center" gap={4}>
<Button variant="secondary" size="sm" onClick={onBackToTeams} icon={<Icon icon={ChevronLeft} size={4} />}>
Back
</Button>
<Group direction="column">
<Heading level={1} weight="bold">Global Standings</Heading>
<Text variant="low" size="sm" font="mono" uppercase letterSpacing="widest">Team Performance Index</Text>
</Group>
</Group>
<Icon icon={Award} size={8} color="var(--ui-color-intent-warning)" />
</Group>
<LeaderboardFiltersBar
searchQuery={searchQuery}
onSearchChange={onSearchChange}
placeholder="Search teams..."
/>
<LeaderboardFiltersBar
searchQuery={searchQuery}
onSearchChange={onSearchChange}
placeholder="Search teams..."
/>
<Box border borderColor="outline-steel" bg="surface-charcoal/30">
<Table>
<TableHead>
<TableRow>
<TableHeader w="20">Rank</TableHeader>
<TableHeader>Team</TableHeader>
<TableHeader textAlign="center">Personnel</TableHeader>
<TableHeader textAlign="center">Races</TableHeader>
<TableHeader textAlign="right">Rating</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{filteredAndSortedTeams.length > 0 ? (
filteredAndSortedTeams.map((team, index) => (
<TableRow
key={team.id}
onClick={() => onTeamClick(team.id)}
cursor="pointer"
hoverBg="surface-charcoal/50"
>
<TableCell>
<Text font="mono" weight="bold" color={index < 3 ? 'warning-amber' : 'text-gray-500'}>
#{index + 1}
</Text>
</TableCell>
<TableCell>
<Stack direction="row" align="center" gap={3}>
<Box w="8" h="8" bg="base-black" border borderColor="outline-steel" display="flex" center>
<Text size="xs" weight="bold" color="primary-accent">{team.name.substring(0, 2).toUpperCase()}</Text>
</Box>
<Text weight="bold" size="sm" color="text-white">{team.name}</Text>
</Stack>
</TableCell>
<TableCell textAlign="center">
<Text size="xs" color="text-gray-400" font="mono">{team.memberCount}</Text>
</TableCell>
<TableCell textAlign="center">
<Text size="xs" color="text-gray-400" font="mono">{team.totalRaces}</Text>
</TableCell>
<TableCell textAlign="right">
<Text font="mono" weight="bold" color="primary-accent">1450</Text>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={5} textAlign="center" py={12}>
<Text color="text-gray-600" font="mono" size="xs" uppercase letterSpacing="widest">
No teams found matching criteria
<Panel variant="dark" padding={0}>
<Table>
<TableHead>
<TableRow>
<TableHeader w="20">Rank</TableHeader>
<TableHeader>Team</TableHeader>
<TableHeader textAlign="center">Personnel</TableHeader>
<TableHeader textAlign="center">Races</TableHeader>
<TableHeader textAlign="right">Rating</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{filteredAndSortedTeams.length > 0 ? (
filteredAndSortedTeams.map((team, index) => (
<TableRow
key={team.id}
onClick={() => onTeamClick(team.id)}
clickable
>
<TableCell>
<Text font="mono" weight="bold" variant={index < 3 ? 'warning' : 'low'}>
#{index + 1}
</Text>
</TableCell>
<TableCell>
<Group direction="row" align="center" gap={3}>
<Panel variant="muted" padding={2}>
<Text size="xs" weight="bold" color="text-primary-accent">{team.name.substring(0, 2).toUpperCase()}</Text>
</Panel>
<Text weight="bold" size="sm">{team.name}</Text>
</Group>
</TableCell>
<TableCell textAlign="center">
<Text size="xs" variant="low" font="mono">{team.memberCount}</Text>
</TableCell>
<TableCell textAlign="center">
<Text size="xs" variant="low" font="mono">{team.totalRaces}</Text>
</TableCell>
<TableCell textAlign="right">
<Text font="mono" weight="bold" color="text-primary-accent">1450</Text>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</Box>
</Stack>
</Container>
</Box>
))
) : (
<TableRow>
<TableCell colSpan={5} textAlign="center">
<Section variant="dark" padding="lg">
<Group align="center" justify="center" fullWidth>
<Text variant="low" font="mono" size="xs" uppercase letterSpacing="widest">
No teams found matching criteria
</Text>
</Group>
</Section>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</Panel>
</Group>
</Container>
);
}

View File

@@ -5,10 +5,11 @@ import { TeamGrid } from '@/components/teams/TeamGrid';
import { TeamLeaderboardPreview } from '@/components/teams/TeamLeaderboardPreviewWrapper';
import { TeamsDirectoryHeader } from '@/components/teams/TeamsDirectoryHeader';
import { TeamsDirectory, TeamsDirectorySection } from '@/components/teams/TeamsDirectory';
import { SharedEmptyState } from '@/components/shared/UIComponents';
import { EmptyState } from '@/ui/EmptyState';
import type { TeamsViewData } from '@/lib/view-data/TeamsViewData';
import { Users } from 'lucide-react';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
import React from 'react';
interface TeamsTemplateProps extends TemplateProps<TeamsViewData> {
onTeamClick?: (teamId: string) => void;
@@ -41,7 +42,7 @@ export function TeamsTemplate({ viewData, onTeamClick, onViewFullLeaderboard, on
))}
</TeamGrid>
) : (
<SharedEmptyState
<EmptyState
icon={Users}
title="No teams yet"
description="Get started by creating your first racing team"

View File

@@ -5,13 +5,12 @@ import { AuthFooterLinks } from '@/components/auth/AuthFooterLinks';
import { AuthForm } from '@/components/auth/AuthForm';
import { ForgotPasswordViewData } from '@/lib/builders/view-data/types/ForgotPasswordViewData';
import { routes } from '@/lib/routing/RouteConfig';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Group } from '@/ui/Group';
import { Icon } from '@/ui/Icon';
import { Input } from '@/ui/Input';
import { Link } from '@/ui/Link';
import { LoadingSpinner } from '@/ui/LoadingSpinner';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { AlertCircle, ArrowLeft, CheckCircle2, Mail, Shield } from 'lucide-react';
import React from 'react';
@@ -54,12 +53,10 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
/>
{mutationState.error && (
<Box p={4} bg="critical-red/10" border borderColor="critical-red/30" rounded="md">
<Stack direction="row" align="start" gap={3}>
<Icon icon={AlertCircle} size={4.5} color="var(--color-critical)" />
<Text size="sm" color="text-critical-red">{mutationState.error}</Text>
</Stack>
</Box>
<Group direction="row" align="start" gap={3} fullWidth>
<Icon icon={AlertCircle} size={4.5} color="var(--color-critical)" />
<Text size="sm" color="text-critical-red">{mutationState.error}</Text>
</Group>
)}
<Button
@@ -72,36 +69,34 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
{isSubmitting ? 'Sending...' : 'Send Reset Link'}
</Button>
<Box textAlign="center">
<Group justify="center" fullWidth>
<Link href={routes.auth.login}>
<Stack direction="row" align="center" justify="center" gap={2} group>
<Icon icon={ArrowLeft} size={3.5} color="var(--color-primary)" groupHoverScale />
<Group direction="row" align="center" justify="center" gap={2}>
<Icon icon={ArrowLeft} size={3.5} color="var(--color-primary)" />
<Text size="sm" weight="bold" color="text-primary-accent">Back to Login</Text>
</Stack>
</Group>
</Link>
</Box>
</Group>
</AuthForm>
) : (
<Stack gap={6}>
<Box p={4} bg="success-green/10" border borderColor="success-green/30" rounded="md">
<Stack direction="row" align="start" gap={3}>
<Icon icon={CheckCircle2} size={5} color="var(--color-success)" />
<Box>
<Text size="sm" color="text-success-green" weight="bold" block>Check your email</Text>
<Text size="xs" color="text-gray-400" block mt={1}>{viewData.successMessage}</Text>
</Box>
</Stack>
</Box>
<Group direction="column" gap={6} fullWidth>
<Group direction="row" align="start" gap={3} fullWidth>
<Icon icon={CheckCircle2} size={5} color="var(--color-success)" />
<Group direction="column" gap={1}>
<Text size="sm" color="text-success-green" weight="bold" block>Check your email</Text>
<Text size="xs" color="text-gray-400" block>{viewData.successMessage}</Text>
</Group>
</Group>
{viewData.magicLink && (
<Box p={3} bg="surface-charcoal" border borderColor="outline-steel" rounded="md">
<Text size="xs" color="text-gray-500" block mb={2} weight="bold">DEVELOPMENT MAGIC LINK</Text>
<Group direction="column" gap={2} fullWidth>
<Text size="xs" color="text-gray-500" block weight="bold">DEVELOPMENT MAGIC LINK</Text>
<Link href={viewData.magicLink}>
<Text size="xs" color="text-primary-accent" block>
{viewData.magicLink}
</Text>
</Link>
</Box>
</Group>
)}
<Button
@@ -112,7 +107,7 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
>
Return to Login
</Button>
</Stack>
</Group>
)}
<AuthFooterLinks>

View File

@@ -7,14 +7,14 @@ import { EnhancedFormError } from '@/components/errors/EnhancedFormError';
import { FormState } from '@/lib/builders/view-data/types/FormState';
import { LoginViewData } from '@/lib/builders/view-data/types/LoginViewData';
import { routes } from '@/lib/routing/RouteConfig';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Checkbox } from '@/ui/Checkbox';
import { Group } from '@/ui/Group';
import { Icon } from '@/ui/Icon';
import { Input } from '@/ui/Input';
import { Link } from '@/ui/Link';
import { LoadingSpinner } from '@/ui/LoadingSpinner';
import { PasswordField } from '@/ui/PasswordField';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { AlertCircle, LogIn, Mail } from 'lucide-react';
import React from 'react';
@@ -43,7 +43,7 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
description="Sign in to access your racing dashboard"
>
<AuthForm onSubmit={formActions.handleSubmit}>
<Stack gap={4}>
<Group direction="column" gap={4} fullWidth>
<Input
label="Email Address"
id="email"
@@ -58,7 +58,7 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
icon={<Mail size={16} />}
/>
<Stack gap={1.5}>
<Group direction="column" gap={1.5} fullWidth>
<PasswordField
label="Password"
id="password"
@@ -72,50 +72,43 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
showPassword={viewData.showPassword}
onTogglePassword={() => formActions.setShowPassword(!viewData.showPassword)}
/>
<Box textAlign="right">
<Group justify="end" fullWidth>
<Link href={routes.auth.forgotPassword}>
<Text size="xs" color="text-primary-accent">
Forgot password?
</Text>
</Link>
</Box>
</Stack>
</Group>
</Group>
<Stack direction="row" align="center" gap={2}>
<Box
as="input"
id="rememberMe"
name="rememberMe"
type="checkbox"
rounded="sm"
borderColor="outline-steel"
bg="surface-charcoal"
color="text-primary-accent"
ring="focus:ring-primary-accent/50"
w="1rem"
h="1rem"
checked={viewData.formState.fields.rememberMe.value as boolean}
onChange={formActions.handleChange}
disabled={isSubmitting}
/>
<Text as="label" htmlFor="rememberMe" size="sm" color="text-med" cursor="pointer">
Keep me signed in
</Text>
</Stack>
</Stack>
<Checkbox
label="Keep me signed in"
checked={viewData.formState.fields.rememberMe.value as boolean}
onChange={(checked) => {
const event = {
target: {
name: 'rememberMe',
value: checked,
type: 'checkbox',
checked
}
} as any;
formActions.handleChange(event);
}}
disabled={isSubmitting}
/>
</Group>
{viewData.hasInsufficientPermissions && (
<Box p={4} bg="warning-amber/10" border borderColor="warning-amber/30" rounded="md">
<Stack direction="row" align="start" gap={3}>
<Icon icon={AlertCircle} size={5} color="var(--color-warning)" />
<Box>
<Text weight="bold" color="text-warning-amber" block size="sm">Insufficient Permissions</Text>
<Text size="xs" color="text-gray-400" block mt={1}>
Please log in with an account that has the required role.
</Text>
</Box>
</Stack>
</Box>
<Group direction="row" align="start" gap={3} fullWidth>
<Icon icon={AlertCircle} size={5} color="var(--color-warning)" />
<Group direction="column" gap={1}>
<Text weight="bold" color="text-warning-amber" block size="sm">Insufficient Permissions</Text>
<Text size="xs" color="text-gray-400" block>
Please log in with an account that has the required role.
</Text>
</Group>
</Group>
)}
{viewData.submitError && (
@@ -149,14 +142,14 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
</Link>
</Text>
<Box mt={2}>
<Group direction="column" gap={1} align="center" fullWidth>
<Text size="xs" color="text-gray-600">
By signing in, you agree to our{' '}
<Link href="/terms">Terms</Link>
{' '}and{' '}
<Link href="/privacy">Privacy</Link>
</Text>
</Box>
</Group>
</AuthFooterLinks>
</AuthCard>
);

View File

@@ -5,13 +5,12 @@ import { AuthFooterLinks } from '@/components/auth/AuthFooterLinks';
import { AuthForm } from '@/components/auth/AuthForm';
import { ResetPasswordViewData } from '@/lib/builders/view-data/types/ResetPasswordViewData';
import { routes } from '@/lib/routing/RouteConfig';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Group } from '@/ui/Group';
import { Icon } from '@/ui/Icon';
import { Link } from '@/ui/Link';
import { LoadingSpinner } from '@/ui/LoadingSpinner';
import { PasswordField } from '@/ui/PasswordField';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { AlertCircle, ArrowLeft, CheckCircle2, Shield } from 'lucide-react';
import React from 'react';
@@ -50,7 +49,7 @@ export function ResetPasswordTemplate({
>
{!viewData.showSuccess ? (
<AuthForm onSubmit={formActions.handleSubmit}>
<Stack gap={4}>
<Group direction="column" gap={4} fullWidth>
<PasswordField
label="New Password"
id="newPassword"
@@ -78,15 +77,13 @@ export function ResetPasswordTemplate({
showPassword={uiState.showConfirmPassword}
onTogglePassword={() => formActions.setShowConfirmPassword(!uiState.showConfirmPassword)}
/>
</Stack>
</Group>
{mutationState.error && (
<Box p={4} bg="critical-red/10" border borderColor="critical-red/30" rounded="md">
<Stack direction="row" align="start" gap={3}>
<Icon icon={AlertCircle} size={4.5} color="var(--color-critical)" />
<Text size="sm" color="text-critical-red">{mutationState.error}</Text>
</Stack>
</Box>
<Group direction="row" align="start" gap={3} fullWidth>
<Icon icon={AlertCircle} size={4.5} color="var(--color-critical)" />
<Text size="sm" color="text-critical-red">{mutationState.error}</Text>
</Group>
)}
<Button
@@ -99,26 +96,24 @@ export function ResetPasswordTemplate({
{isSubmitting ? 'Resetting...' : 'Reset Password'}
</Button>
<Box textAlign="center">
<Group justify="center" fullWidth>
<Link href={routes.auth.login}>
<Stack direction="row" align="center" justify="center" gap={2} group>
<Icon icon={ArrowLeft} size={3.5} color="var(--color-primary)" groupHoverScale />
<Group direction="row" align="center" justify="center" gap={2}>
<Icon icon={ArrowLeft} size={3.5} color="var(--color-primary)" />
<Text size="sm" weight="bold" color="text-primary-accent">Back to Login</Text>
</Stack>
</Group>
</Link>
</Box>
</Group>
</AuthForm>
) : (
<Stack gap={6}>
<Box p={4} bg="success-green/10" border borderColor="success-green/30" rounded="md">
<Stack direction="row" align="start" gap={3}>
<Icon icon={CheckCircle2} size={5} color="var(--color-success)" />
<Box>
<Text size="sm" color="text-success-green" weight="bold" block>Password Reset</Text>
<Text size="xs" color="text-gray-400" block mt={1}>{viewData.successMessage}</Text>
</Box>
</Stack>
</Box>
<Group direction="column" gap={6} fullWidth>
<Group direction="row" align="start" gap={3} fullWidth>
<Icon icon={CheckCircle2} size={5} color="var(--color-success)" />
<Group direction="column" gap={1}>
<Text size="sm" color="text-success-green" weight="bold" block>Password Reset</Text>
<Text size="xs" color="text-gray-400" block>{viewData.successMessage}</Text>
</Group>
</Group>
<Button
type="button"
@@ -128,7 +123,7 @@ export function ResetPasswordTemplate({
>
Return to Login
</Button>
</Stack>
</Group>
)}
<AuthFooterLinks>

View File

@@ -5,15 +5,16 @@ import { AuthFooterLinks } from '@/components/auth/AuthFooterLinks';
import { AuthForm } from '@/components/auth/AuthForm';
import { SignupViewData } from '@/lib/builders/view-data/types/SignupViewData';
import { checkPasswordStrength } from '@/lib/utils/validation';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Grid } from '@/ui/Grid';
import { Group } from '@/ui/Group';
import { Icon } from '@/ui/Icon';
import { Input } from '@/ui/Input';
import { Link } from '@/ui/Link';
import { LoadingSpinner } from '@/ui/LoadingSpinner';
import { PasswordField } from '@/ui/PasswordField';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { ProgressBar } from '@/ui/ProgressBar';
import { AlertCircle, Check, Mail, User, UserPlus, X } from 'lucide-react';
import React from 'react';
@@ -47,16 +48,22 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
{ met: /[^a-zA-Z\d]/.test(passwordValue), label: 'Special' },
];
const getStrengthIntent = () => {
if (passwordStrength.score <= 2) return 'critical';
if (passwordStrength.score <= 4) return 'warning';
return 'success';
};
return (
<AuthCard
title="Create Account"
description="Join the GridPilot racing community"
>
<AuthForm onSubmit={formActions.handleSubmit}>
<Stack gap={6}>
<Stack gap={4}>
<Group direction="column" gap={6} fullWidth>
<Group direction="column" gap={4} fullWidth>
<Text size="xs" weight="bold" color="text-low" uppercase letterSpacing="wide" block>Personal Information</Text>
<Box display="grid" gridCols={{ base: 1, md: 2 }} gap={4}>
<Grid cols={{ base: 1, md: 2 }} gap={4}>
<Input
label="First Name"
id="firstName"
@@ -81,16 +88,14 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
autoComplete="family-name"
icon={<User size={16} />}
/>
</Box>
</Grid>
<Box p={3} bg="warning-amber/5" border borderColor="warning-amber/20" rounded="md">
<Stack direction="row" align="start" gap={2}>
<Icon icon={AlertCircle} size={3.5} color="var(--color-warning)" mt={0.5} />
<Text size="xs" color="text-med">
<Text weight="bold" color="text-warning-amber">Note:</Text> Your name cannot be changed after signup.
</Text>
</Stack>
</Box>
<Group direction="row" align="start" gap={2} fullWidth>
<Icon icon={AlertCircle} size={3.5} color="var(--color-warning)" />
<Text size="xs" color="text-med">
<Text weight="bold" color="text-warning-amber">Note:</Text> Your name cannot be changed after signup.
</Text>
</Group>
<Input
label="Email Address"
@@ -105,9 +110,9 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
autoComplete="email"
icon={<Mail size={16} />}
/>
</Stack>
</Group>
<Stack gap={4}>
<Group direction="column" gap={4} fullWidth>
<Text size="xs" weight="bold" color="text-low" uppercase letterSpacing="wide" block>Security</Text>
<PasswordField
label="Password"
@@ -124,34 +129,30 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
/>
{passwordValue && (
<Stack gap={3}>
<Stack direction="row" align="center" gap={2}>
<Box flexGrow={1} h="1px" bg="outline-steel" rounded="full" overflow="hidden">
<Box
h="full"
transition
w={`${(passwordStrength.score / 5) * 100}%`}
bg={
passwordStrength.score <= 2 ? 'critical-red' :
passwordStrength.score <= 4 ? 'warning-amber' : 'success-green'
}
<Group direction="column" gap={3} fullWidth>
<Group direction="row" align="center" gap={2} fullWidth>
<Group fullWidth>
<ProgressBar
value={(passwordStrength.score / 5) * 100}
intent={getStrengthIntent()}
size="sm"
/>
</Box>
</Group>
<Text size="xs" weight="bold" color="text-low" uppercase>
{passwordStrength.label}
</Text>
</Stack>
<Box display="grid" gridCols={2} gap={2}>
</Group>
<Grid cols={2} gap={2}>
{passwordRequirements.map((req, index) => (
<Stack key={index} direction="row" align="center" gap={1.5}>
<Group key={index} direction="row" align="center" gap={1.5}>
<Icon icon={req.met ? Check : X} size={3} color={req.met ? 'var(--color-success)' : 'var(--color-text-low)'} />
<Text size="xs" color={req.met ? 'text-med' : 'text-low'}>
{req.label}
</Text>
</Stack>
</Group>
))}
</Box>
</Stack>
</Grid>
</Group>
)}
<PasswordField
@@ -167,16 +168,14 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
showPassword={uiState.showConfirmPassword}
onTogglePassword={() => formActions.setShowConfirmPassword(!uiState.showConfirmPassword)}
/>
</Stack>
</Stack>
</Group>
</Group>
{mutationState.error && (
<Box p={4} bg="critical-red/10" border borderColor="critical-red/30" rounded="md">
<Stack direction="row" align="start" gap={3}>
<Icon icon={AlertCircle} size={4.5} color="var(--color-critical)" />
<Text size="sm" color="text-critical-red">{mutationState.error}</Text>
</Stack>
</Box>
<Group direction="row" align="start" gap={3} fullWidth>
<Icon icon={AlertCircle} size={4.5} color="var(--color-critical)" />
<Text size="sm" color="text-critical-red">{mutationState.error}</Text>
</Group>
)}
<Button
@@ -200,14 +199,14 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
</Link>
</Text>
<Box mt={2}>
<Group direction="column" gap={1} align="center" fullWidth>
<Text size="xs" color="text-gray-600">
By creating an account, you agree to our{' '}
<Link href="/terms">Terms</Link>
{' '}and{' '}
<Link href="/privacy">Privacy</Link>
</Text>
</Box>
</Group>
</AuthFooterLinks>
</AuthCard>
);

View File

@@ -1,84 +1,77 @@
import { AppFooter } from '@/components/app/AppFooter';
import { Box } from '@/ui/Box';
import { Grid } from '@/ui/Grid';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import Image from 'next/image';
import Link from 'next/link';
import { Link } from '@/ui/Link';
import { BrandMark } from '@/ui/BrandMark';
import { Box } from '@/ui/Box';
export interface GlobalFooterViewData {}
export function GlobalFooterTemplate(_props: GlobalFooterViewData) {
return (
<AppFooter>
<Box maxWidth="7xl" mx="auto" display="grid" responsiveGridCols={{ base: 1, md: 4 }} gap={12}>
<Box colSpan={{ base: 1, md: 2 }}>
<Box mb={6} opacity={0.8}>
<Image
src="/images/logos/wordmark-rectangle-dark.svg"
alt="GridPilot"
width={140}
height={26}
/>
</Box>
<Box maxWidth="sm" mb={6}>
<Text color="text-gray-500">
<Grid cols={{ base: 1, md: 4 }} gap={12}>
<Stack colSpan={{ base: 1, md: 2 }} gap={6}>
<Stack direction="row" align="center" gap={4}>
<BrandMark />
<Box display={{ base: 'none', sm: 'flex' }} alignItems="center" gap={2} borderLeft borderColor="[#23272B]" pl={4}>
<Box w="4px" h="4px" rounded="full" bg="primary-accent" animate="pulse" />
<Text size="xs" variant="low" weight="bold" font="mono" letterSpacing="0.1em">
INFRASTRUCTURE
</Text>
</Box>
</Stack>
<Stack maxWidth="sm">
<Text variant="low" size="sm">
The professional infrastructure for serious sim racing.
Precision telemetry, automated results, and elite league management.
</Text>
</Box>
<Box display="flex" alignItems="center" gap={4}>
<Text size="xs" color="text-gray-600" font="mono" letterSpacing="widest">
© 2026 GRIDPILOT
</Text>
</Box>
</Box>
</Stack>
</Stack>
<Box>
<Box mb={4}>
<Text weight="bold" color="text-gray-300" letterSpacing="wider">PLATFORM</Text>
</Box>
<Stack gap={4}>
<Text size="xs" weight="bold" variant="high" uppercase letterSpacing="wider">PLATFORM</Text>
<Stack as="ul" direction="col" gap={2}>
<Box as="li">
<Box as={Link} href="/leagues" color="text-gray-500" hoverTextColor="primary-accent" transition>
<Stack as="li">
<Link href="/leagues" variant="secondary">
Leagues
</Box>
</Box>
<Box as="li">
<Box as={Link} href="/teams" color="text-gray-500" hoverTextColor="primary-accent" transition>
</Link>
</Stack>
<Stack as="li">
<Link href="/teams" variant="secondary">
Teams
</Box>
</Box>
<Box as="li">
<Box as={Link} href="/leaderboards" color="text-gray-500" hoverTextColor="primary-accent" transition>
</Link>
</Stack>
<Stack as="li">
<Link href="/leaderboards" variant="secondary">
Leaderboards
</Box>
</Box>
</Link>
</Stack>
</Stack>
</Box>
</Stack>
<Box>
<Box mb={4}>
<Text weight="bold" color="text-gray-300" letterSpacing="wider">SUPPORT</Text>
</Box>
<Stack gap={4}>
<Text size="xs" weight="bold" variant="high" uppercase letterSpacing="wider">SUPPORT</Text>
<Stack as="ul" direction="col" gap={2}>
<Box as="li">
<Box as={Link} href="/docs" color="text-gray-500" hoverTextColor="primary-accent" transition>
<Stack as="li">
<Link href="/docs" variant="secondary">
Documentation
</Box>
</Box>
<Box as="li">
<Box as={Link} href="/status" color="text-gray-500" hoverTextColor="primary-accent" transition>
</Link>
</Stack>
<Stack as="li">
<Link href="/status" variant="secondary">
System Status
</Box>
</Box>
<Box as="li">
<Box as={Link} href="/contact" color="text-gray-500" hoverTextColor="primary-accent" transition>
</Link>
</Stack>
<Stack as="li">
<Link href="/contact" variant="secondary">
Contact
</Box>
</Box>
</Link>
</Stack>
</Stack>
</Box>
</Box>
</Stack>
</Grid>
</AppFooter>
);
}

Some files were not shown because too many files have changed in this diff Show More