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 { notFound, redirect } from 'next/navigation';
import { DashboardPageQuery } from '@/lib/page-queries/DashboardPageQuery'; 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'; import { logger } from '@/lib/infrastructure/logging/logger';
export default async function DashboardPage() { export default async function DashboardPage() {
@@ -23,5 +23,5 @@ export default async function DashboardPage() {
// Success // Success
const viewData = result.unwrap(); 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 { CreateLeagueWizard } from '@/client-wrapper/CreateLeagueWizard';
import { Section } from '@/ui/Section'; import { Section } from '@/ui/Section';
import { Container } from '@/ui/Container'; 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'; type StepName = 'basics' | 'visibility' | 'structure' | 'schedule' | 'scoring' | 'stewarding' | 'review';
export default function CreateLeaguePage() { interface CreateLeaguePageProps {
const router = useRouter(); searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
const searchParams = useSearchParams(); }
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; const rawStep = wizardParams.step;
let currentStepName: StepName = 'basics'; let currentStepName: StepName = 'basics';
@@ -28,23 +26,11 @@ export default function CreateLeaguePage() {
currentStepName = rawStep; 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 ( return (
<Section> <Section>
<Container size="md"> <Container size="md">
<CreateLeagueWizard stepName={currentStepName} onStepChange={handleStepChange} /> <CreateLeagueWizard stepName={currentStepName} />
</Container> </Container>
</Section> </Section>
); );
} }

View File

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

View File

@@ -3,7 +3,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { SponsorSettingsTemplate } from '@/templates/SponsorSettingsTemplate'; import { SponsorSettingsTemplate } from '@/templates/SponsorSettingsTemplate';
import { logoutAction } from '@/app/actions/logoutAction'; import { logoutAction } from '@/app/actions/logoutAction';
import { ConfirmDialog } from '@/ui/ConfirmDialog'; import { ConfirmDialog } from '@/components/shared/ConfirmDialog';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { routes } from '@/lib/routing/RouteConfig'; import { routes } from '@/lib/routing/RouteConfig';
import { logger } from '@/lib/infrastructure/logging/logger'; 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'; export default async function CreateTeamPage() {
import { routes } from '@/lib/routing/RouteConfig'; return <CreateTeamPageClient />;
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>
);
} }

View File

@@ -12,6 +12,7 @@ import type { Weekday } from '@/lib/types/Weekday';
import type { WizardErrors } from '@/lib/types/WizardErrors'; import type { WizardErrors } from '@/lib/types/WizardErrors';
import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel'; import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel';
import { CreateLeagueWizardTemplate, Step } from '@/templates/CreateLeagueWizardTemplate'; import { CreateLeagueWizardTemplate, Step } from '@/templates/CreateLeagueWizardTemplate';
import { SearchParamBuilder } from '@/lib/routing/search-params/SearchParamBuilder';
import { import {
Award, Award,
Calendar, Calendar,
@@ -91,7 +92,7 @@ type LeagueWizardFormModel = LeagueConfigFormModel & {
interface CreateLeagueWizardProps { interface CreateLeagueWizardProps {
stepName: StepName; stepName: StepName;
onStepChange: (stepName: StepName) => void; onStepChange?: (stepName: StepName) => void;
} }
function stepNameToStep(stepName: StepName): Step { function stepNameToStep(stepName: StepName): Step {
@@ -200,6 +201,16 @@ export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizar
const router = useRouter(); const router = useRouter();
const { session } = useAuth(); 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 step = stepNameToStep(stepName);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [presetsLoading, setPresetsLoading] = useState(true); const [presetsLoading, setPresetsLoading] = useState(true);
@@ -330,19 +341,19 @@ export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizar
const nextStep = (step < 7 ? ((step + 1) as Step) : step); const nextStep = (step < 7 ? ((step + 1) as Step) : step);
saveHighestStep(nextStep); saveHighestStep(nextStep);
setHighestCompletedStep((prev) => Math.max(prev, nextStep)); setHighestCompletedStep((prev) => Math.max(prev, nextStep));
onStepChange(stepToStepName(nextStep)); handleStepChange(stepToStepName(nextStep));
}; };
const goToPreviousStep = () => { const goToPreviousStep = () => {
const prevStep = (step > 1 ? ((step - 1) as Step) : step); const prevStep = (step > 1 ? ((step - 1) as Step) : step);
onStepChange(stepToStepName(prevStep)); handleStepChange(stepToStepName(prevStep));
}; };
const goToStep = useCallback((targetStep: Step) => { const goToStep = useCallback((targetStep: Step) => {
if (targetStep <= highestCompletedStep) { if (targetStep <= highestCompletedStep) {
onStepChange(stepToStepName(targetStep)); handleStepChange(stepToStepName(targetStep));
} }
}, [highestCompletedStep, onStepChange]); }, [highestCompletedStep, handleStepChange]);
const handleSubmit = async (event: FormEvent) => { const handleSubmit = async (event: FormEvent) => {
event.preventDefault(); event.preventDefault();
@@ -413,7 +424,7 @@ export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizar
})); }));
if (LeagueWizardCommandModel.hasWizardErrors(allErrors)) { if (LeagueWizardCommandModel.hasWizardErrors(allErrors)) {
onStepChange('basics'); handleStepChange('basics');
return; 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 { NotificationIntegration } from '@/components/errors/NotificationIntegration';
import { EnhancedErrorBoundary } from '@/components/errors/EnhancedErrorBoundary'; import { EnhancedErrorBoundary } from '@/components/errors/EnhancedErrorBoundary';
import { DevToolbar } from '@/components/dev/DevToolbar'; import { DevToolbar } from '@/components/dev/DevToolbar';
import { ThemeProvider } from '@/ui/theme/ThemeProvider'; import { ThemeProvider } from '@/components/shared/ThemeProvider';
import React from 'react'; import React from 'react';
interface AppWrapperProps { interface AppWrapperProps {

View File

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

View File

@@ -1,5 +1,12 @@
import React from 'react'; 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 { interface AppFooterProps {
children?: React.ReactNode; children?: React.ReactNode;
@@ -7,9 +14,76 @@ interface AppFooterProps {
/** /**
* AppFooter is the bottom section of the application. * 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) { export function AppFooter({ children }: AppFooterProps) {
return ( 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'; 'use client';
import { Card } from '@/ui/Card'; import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
import { SectionHeader } from '@/ui/SectionHeader'; import { SectionHeader } from '@/ui/SectionHeader';
import React from 'react'; import React from 'react';
@@ -24,9 +23,7 @@ export function AuthCard({ children, title, description }: AuthCardProps) {
description={description} description={description}
variant="minimal" variant="minimal"
/> />
<Box> {children}
{children}
</Box>
</Card> </Card>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,10 +29,10 @@ export function DriverHeaderPanel({
return ( return (
<Stack <Stack
bg="bg-panel-gray" bg="var(--ui-color-bg-surface)"
rounded="xl" rounded="xl"
border border
borderColor="border-charcoal-outline" borderColor="var(--ui-color-border-low)"
overflow="hidden" overflow="hidden"
position="relative" position="relative"
> >
@@ -43,12 +43,12 @@ export function DriverHeaderPanel({
left={0} left={0}
right={0} right={0}
height="24" 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} opacity={0.5}
/> />
<Stack p={6} position="relative"> <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 */} {/* Avatar */}
<Stack <Stack
width="32" width="32"
@@ -56,8 +56,8 @@ export function DriverHeaderPanel({
rounded="2xl" rounded="2xl"
overflow="hidden" overflow="hidden"
border border
borderColor="border-charcoal-outline" borderColor="var(--ui-color-border-low)"
bg="bg-graphite-black" bg="var(--ui-color-bg-base)"
flexShrink={0} flexShrink={0}
> >
<Image <Image
@@ -72,25 +72,25 @@ export function DriverHeaderPanel({
<Stack flexGrow={1}> <Stack flexGrow={1}>
<Stack gap={2}> <Stack gap={2}>
<Stack direction="row" align="center" gap={3} wrap> <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} {name}
</Text> </Text>
<RatingBadge rating={rating} ratingLabel={ratingLabel} size="lg" /> <RatingBadge rating={rating} ratingLabel={ratingLabel} size="lg" />
</Stack> </Stack>
<Stack direction="row" align="center" gap={4} wrap> <Stack direction="row" align="center" gap={4} wrap>
<Text size="sm" color="text-gray-400"> <Text size="sm" variant="low">
{nationality} {nationality}
</Text> </Text>
{globalRankLabel && ( {globalRankLabel && (
<Text size="sm" color="text-gray-400"> <Text size="sm" variant="low">
Global Rank: <Text color="text-warning-amber" weight="semibold">{globalRankLabel}</Text> Global Rank: <Text variant="warning" weight="semibold">{globalRankLabel}</Text>
</Text> </Text>
)} )}
</Stack> </Stack>
{bio && ( {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} {bio}
</Text> </Text>
)} )}

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,10 +25,10 @@ interface LiveryCardProps {
export function LiveryCard({ livery, onEdit, onDownload, onDelete }: LiveryCardProps) { export function LiveryCard({ livery, onEdit, onDownload, onDelete }: LiveryCardProps) {
return ( 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 */} {/* Livery Preview */}
<Stack height={48} backgroundColor="deep-graphite" rounded="lg" mb={4} display="flex" center border borderColor="charcoal-outline"> <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} color="text-gray-600" /> <Icon icon={Car} size={16} intent="low" />
</Stack> </Stack>
{/* Livery Info */} {/* Livery Info */}

View File

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

View File

@@ -9,6 +9,8 @@ import { Link } from '@/ui/Link';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface'; import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text'; 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'; import { Calendar, Clock, ExternalLink, Globe, Star, Trophy, UserPlus } from 'lucide-react';
interface ProfileHeroProps { interface ProfileHeroProps {
@@ -54,25 +56,40 @@ export function ProfileHero({
friendRequestSent, friendRequestSent,
}: ProfileHeroProps) { }: ProfileHeroProps) {
return ( 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> <Stack direction="row" align="start" gap={6} wrap>
{/* Avatar */} {/* Avatar */}
<Stack style={{ position: 'relative' }}> <Box 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
<Stack style={{ width: '100%', height: '100%', borderRadius: '0.75rem', overflow: 'hidden', backgroundColor: '#262626' }}> 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 <Image
src={driver.avatarUrl || mediaConfig.avatars.defaultFallback} src={driver.avatarUrl || mediaConfig.avatars.defaultFallback}
alt={driver.name} alt={driver.name}
width={144} width={144}
height={144} height={144}
style={{ width: '100%', height: '100%', objectFit: 'cover' }} objectFit="cover"
fill
/> />
</Stack> </Stack>
</Stack> </Stack>
</Stack> </Box>
{/* Driver Info */} {/* Driver Info */}
<Stack style={{ flex: 1, minWidth: 0 }}> <Stack flex={1} minWidth="0">
<Stack direction="row" align="center" gap={3} wrap mb={2}> <Stack direction="row" align="center" gap={3} wrap mb={2}>
<Heading level={1}>{driver.name}</Heading> <Heading level={1}>{driver.name}</Heading>
<Text size="4xl" aria-label={`Country: ${driver.country}`}> <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}> <Stack direction="row" align="center" gap={4} wrap mb={4}>
{stats && ( {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}> <Stack direction="row" align="center" gap={2}>
<Star style={{ width: '1rem', height: '1rem', color: '#3b82f6' }} /> <Icon icon={Star} size={4} intent="primary" />
<Text font="mono" weight="bold" color="text-primary-blue">{stats.ratingLabel}</Text> <Text font="mono" weight="bold" variant="primary">{stats.ratingLabel}</Text>
<Text size="xs" color="text-gray-400">Rating</Text> <Text size="xs" variant="low">Rating</Text>
</Stack> </Stack>
</Surface> </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}> <Stack direction="row" align="center" gap={2}>
<Trophy style={{ width: '1rem', height: '1rem', color: '#facc15' }} /> <Icon icon={Trophy} size={4} intent="warning" />
<Text font="mono" weight="bold" style={{ color: '#facc15' }}>{globalRankLabel}</Text> <Text font="mono" weight="bold" color="rgba(250, 204, 21, 1)">{globalRankLabel}</Text>
<Text size="xs" color="text-gray-400">Global</Text> <Text size="xs" variant="low">Global</Text>
</Stack> </Stack>
</Surface> </Surface>
</> </>
@@ -103,19 +120,19 @@ export function ProfileHero({
</Stack> </Stack>
{/* Meta info */} {/* 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}> <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> <Text size="sm">iRacing: {driver.iracingId}</Text>
</Stack> </Stack>
<Stack direction="row" align="center" gap={1.5}> <Stack direction="row" align="center" gap={1.5}>
<Calendar style={{ width: '1rem', height: '1rem' }} /> <Icon icon={Calendar} size={4} />
<Text size="sm"> <Text size="sm">
Joined {driver.joinedAtLabel} Joined {driver.joinedAtLabel}
</Text> </Text>
</Stack> </Stack>
<Stack direction="row" align="center" gap={1.5}> <Stack direction="row" align="center" gap={1.5}>
<Clock style={{ width: '1rem', height: '1rem' }} /> <Icon icon={Clock} size={4} />
<Text size="sm">{timezone}</Text> <Text size="sm">{timezone}</Text>
</Stack> </Stack>
</Stack> </Stack>
@@ -127,7 +144,7 @@ export function ProfileHero({
variant="primary" variant="primary"
onClick={onAddFriend} onClick={onAddFriend}
disabled={friendRequestSent} disabled={friendRequestSent}
icon={<UserPlus style={{ width: '1rem', height: '1rem' }} />} icon={<Icon icon={UserPlus} size={4} />}
> >
{friendRequestSent ? 'Request Sent' : 'Add Friend'} {friendRequestSent ? 'Request Sent' : 'Add Friend'}
</Button> </Button>
@@ -136,23 +153,22 @@ export function ProfileHero({
{/* Social Handles */} {/* Social Handles */}
{socialHandles.length > 0 && ( {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> <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) => { {socialHandles.map((social) => {
const Icon = getSocialIcon(social.platform); const SocialIcon = getSocialIcon(social.platform);
return ( return (
<Stack key={social.platform}> <Stack key={social.platform}>
<Link <Link
href={social.url} href={social.url}
target="_blank" target="_blank"
rel="noopener noreferrer" 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' }}> <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 style={{ width: '1rem', height: '1rem' }} /> <Icon icon={SocialIcon} size={4} />
<Text size="sm">{social.handle}</Text> <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> </Surface>
</Link> </Link>
</Stack> </Stack>

View File

@@ -7,10 +7,10 @@ import { Text } from '@/ui/Text';
import { BarChart3 } from 'lucide-react'; import { BarChart3 } from 'lucide-react';
const SKILL_LEVELS = [ const SKILL_LEVELS = [
{ id: 'pro', label: 'Pro', icon: BarChart3, color: 'text-yellow-400', bgColor: 'bg-yellow-400/10', borderColor: 'border-yellow-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: 'text-purple-400', bgColor: 'bg-purple-400/10', borderColor: 'border-purple-400/30' }, { 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: 'text-primary-blue', bgColor: 'bg-primary-blue/10', borderColor: 'border-primary-blue/30' }, { 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: 'text-green-400', bgColor: 'bg-green-400/10', borderColor: 'border-green-400/30' }, { 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 { interface SkillDistributionProps {
@@ -38,15 +38,15 @@ export function SkillDistribution({ drivers }: SkillDistributionProps) {
alignItems="center" alignItems="center"
justifyContent="center" justifyContent="center"
rounded="xl" rounded="xl"
bg="bg-neon-aqua/10" bg="rgba(78, 212, 224, 0.1)"
border 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>
<Box> <Box>
<Heading level={2}>Skill Distribution</Heading> <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>
</Box> </Box>
@@ -58,28 +58,24 @@ export function SkillDistribution({ drivers }: SkillDistributionProps) {
p={4} p={4}
rounded="xl" rounded="xl"
border border
className={`${level.bgColor} ${level.borderColor}`} bg={level.bgColor}
borderColor={level.borderColor}
> >
<Box display="flex" alignItems="center" justifyContent="between" mb={3}> <Box display="flex" alignItems="center" justifyContent="between" mb={3}>
<Icon icon={level.icon} size={5} className={level.color} /> <Icon icon={level.icon} size={5} color={level.color} />
<Text size="2xl" weight="bold" className={level.color}>{level.count}</Text> <Text size="2xl" weight="bold" color={level.color}>{level.count}</Text>
</Box> </Box>
<Text color="text-white" weight="medium" block mb={1}>{level.label}</Text> <Text variant="high" weight="medium" block mb={1}>{level.label}</Text>
<Box fullWidth h="2" rounded="full" bg="bg-deep-graphite/50" overflow="hidden"> <Box fullWidth h="2" rounded="full" bg="var(--ui-color-bg-base)" overflow="hidden">
<Box <Box
h="full" h="full"
rounded="full" rounded="full"
transition transition
className={ bg={level.color}
level.id === 'pro' ? 'bg-yellow-400' :
level.id === 'advanced' ? 'bg-purple-400' :
level.id === 'intermediate' ? 'bg-primary-blue' :
'bg-green-400'
}
style={{ width: `${level.percentage}%` }} style={{ width: `${level.percentage}%` }}
/> />
</Box> </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> </Box>
); );
})} })}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
import { ErrorPageContainer } from '@/ui/ErrorPageContainer'; import { ErrorPageContainer } from '@/ui/ErrorPageContainer';
import { Glow } from '@/ui/Glow'; import { Glow } from '@/ui/Glow';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { FooterSection } from '@/ui/FooterSection'; import { Group } from '@/ui/Group';
import { NotFoundActions } from './NotFoundActions'; import { NotFoundActions } from './NotFoundActions';
import { NotFoundDiagnostics } from './NotFoundDiagnostics'; import { NotFoundDiagnostics } from './NotFoundDiagnostics';
import { NotFoundHelpLinks } from './NotFoundHelpLinks'; import { NotFoundHelpLinks } from './NotFoundHelpLinks';
@@ -42,40 +42,40 @@ export function NotFoundScreen({
{/* Background Glow Accent */} {/* Background Glow Accent */}
<Glow color="primary" size="xl" opacity={0.1} position="center" /> <Glow color="primary" size="xl" opacity={0.1} position="center" />
<NotFoundDiagnostics errorCode={errorCode} /> <Group direction="column" align="center" gap={8} fullWidth>
<NotFoundDiagnostics errorCode={errorCode} />
<Text
as="h1" <Group direction="column" align="center" gap={4} fullWidth>
size="4xl" <Text
weight="bold" as="h1"
variant="high" size="4xl"
uppercase weight="bold"
block variant="high"
align="center" uppercase
style={{ marginTop: '1rem', marginBottom: '2rem' }} block
> align="center"
{title} >
</Text> {title}
</Text>
<Text <Text
size="xl" size="xl"
variant="med" variant="med"
block block
weight="medium" weight="medium"
align="center" align="center"
style={{ marginBottom: '3rem' }} >
> {message}
{message} </Text>
</Text> </Group>
<NotFoundActions <NotFoundActions
primaryLabel={actionLabel} primaryLabel={actionLabel}
onPrimaryClick={onActionClick} onPrimaryClick={onActionClick}
/> />
<FooterSection>
<NotFoundHelpLinks links={helpLinks} /> <NotFoundHelpLinks links={helpLinks} />
</FooterSection> </Group>
</ErrorPageContainer> </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'; 'use client';
import { Text } from '@/ui/Text'; 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 { interface HomeFeatureDescriptionProps {
lead: string; lead: string;
@@ -12,7 +14,7 @@ interface HomeFeatureDescriptionProps {
/** /**
* HomeFeatureDescription - A semantic component for feature descriptions on the home page. * 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({ export function HomeFeatureDescription({
lead, lead,
@@ -20,40 +22,21 @@ export function HomeFeatureDescription({
quote, quote,
accentColor = 'primary', accentColor = 'primary',
}: HomeFeatureDescriptionProps) { }: HomeFeatureDescriptionProps) {
const borderColor = { const intent = accentColor === 'gray' ? 'low' : accentColor;
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];
return ( return (
<Stack gap={6}> <CardStack gap={6}>
<Text size="lg" color="text-gray-400" weight="medium" leading="relaxed"> <Text size="lg" variant="med" leading="relaxed">
{lead} {lead}
</Text> </Text>
<Stack as="ul" gap={2}>
{items.map((item, index) => ( <FeatureList items={items} intent={intent} />
<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>
{quote && ( {quote && (
<Stack borderLeft borderStyle="solid" borderWidth="2px" borderColor={borderColor} pl={4} py={1} bg={bgColor}> <FeatureQuote intent={intent}>
<Text color="text-gray-600" font="mono" size="xs" uppercase letterSpacing="widest" leading="relaxed"> {quote}
{quote} </FeatureQuote>
</Text>
</Stack>
)} )}
</Stack> </CardStack>
); );
} }

View File

@@ -1,13 +1,7 @@
'use client'; 'use client';
import React from 'react'; import React from 'react';
import { Panel } from '@/ui/Panel'; import { FeatureSection } from '@/ui/FeatureSection';
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';
interface HomeFeatureSectionProps { interface HomeFeatureSectionProps {
heading: string; heading: string;
@@ -27,43 +21,13 @@ export function HomeFeatureSection({
layout, layout,
accentColor = 'primary', accentColor = 'primary',
}: HomeFeatureSectionProps) { }: HomeFeatureSectionProps) {
const glowColor = ({
primary: 'primary',
aqua: 'aqua',
amber: 'amber',
}[accentColor] || 'primary') as 'primary' | 'aqua' | 'amber' | 'purple';
return ( return (
<Section variant="dark" padding="lg"> <FeatureSection
<Glow heading={heading}
color={glowColor} description={description}
size="lg" mockup={mockup}
position={layout === 'text-left' ? 'bottom-left' : 'top-right'} layout={layout}
opacity={0.02} intent={accentColor}
/> />
<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>
); );
} }

View File

@@ -1,109 +1,42 @@
'use client'; 'use client';
import React from 'react'; import React from 'react';
import { Button } from '@/ui/Button'; import { DiscordCTA } from '@/ui/DiscordCTA';
import { Glow } from '@/ui/Glow'; import { Code, Lightbulb, MessageSquare, Users } from 'lucide-react';
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';
export function HomeFooterCTA() { export function HomeFooterCTA() {
const discordUrl = process.env.NEXT_PUBLIC_DISCORD_URL || '#'; 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 ( return (
<Section variant="dark" padding="lg"> <DiscordCTA
<Glow color="primary" size="xl" position="center" opacity={0.05} /> title="Join the Grid on Discord"
lead="GridPilot is a solo developer project built for the community."
<Container> description="We are in early alpha. Join us to help shape the future of motorsport infrastructure. Your feedback directly influences the roadmap."
<Card variant="outline"> benefits={benefits}
<Stack align="center" gap={12}> discordUrl={discordUrl}
{/* 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>
); );
} }

View File

@@ -1,13 +1,6 @@
'use client'; 'use client';
import { Button } from '@/ui/Button'; import { LandingHero } from '@/ui/LandingHero';
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';
interface HomeHeaderProps { interface HomeHeaderProps {
title: string; title: string;
@@ -26,51 +19,6 @@ interface HomeHeaderProps {
/** /**
* HomeHeader - Semantic hero section for the landing page. * HomeHeader - Semantic hero section for the landing page.
*/ */
export function HomeHeader({ export function HomeHeader(props: HomeHeaderProps) {
title, return <LandingHero {...props} />;
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>
);
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -44,8 +44,13 @@ export function BenefitCard({
rounded="xl" rounded="xl"
border={true} border={true}
padding={6} padding={6}
className={`relative h-full transition-all duration-300 group ${isHighlight ? 'border-primary-blue/30' : 'border-charcoal-outline hover:border-charcoal-outline/80'}`} borderColor={isHighlight ? 'rgba(25, 140, 255, 0.3)' : 'var(--ui-color-border-low)'}
style={isHighlight ? { background: 'linear-gradient(to bottom right, rgba(25, 140, 255, 0.1), rgba(25, 140, 255, 0.05))' } : {}} 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 */} {/* Icon */}
<Box <Box
@@ -55,25 +60,25 @@ export function BenefitCard({
display="flex" display="flex"
center center
mb={4} 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} 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> </Box>
{/* Content */} {/* Content */}
<Heading level={3} mb={2}>{title}</Heading> <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 */}
{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}> <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} {stats.value}
</Text> </Text>
<Text size="sm" color="text-gray-500">{stats.label}</Text> <Text size="sm" variant="low">{stats.label}</Text>
</Box> </Box>
</Box> </Box>
)} )}
@@ -84,7 +89,11 @@ export function BenefitCard({
position="absolute" position="absolute"
inset="0" inset="0"
rounded="xl" 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> </Surface>

View File

@@ -24,11 +24,11 @@ export function DiscoverySection({ viewData }: DiscoverySectionProps) {
<Stack gap={16}> <Stack gap={16}>
<Stack maxWidth="2xl"> <Stack maxWidth="2xl">
<Stack borderLeft borderStyle="solid" borderColor="primary-accent" pl={4} mb={4}> <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 Live Ecosystem
</Text> </Text>
</Stack> </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 Discover the Grid
</Heading> </Heading>
<Text size="lg" color="text-gray-400" block mt={6} leading="relaxed"> <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}> <Grid cols={1} lgCols={3} gap={12}>
{/* Top leagues */} {/* Top leagues */}
<Stack gap={8}> <Stack gap={8}>
<Stack direction="row" align="center" justify="between" className="border-b border-border-gray/30 pb-4"> <Stack direction="row" align="center" justify="between" borderBottom borderColor="var(--ui-color-border-low)" pb={4}>
<Heading level={5} color="text-gray-400" weight="bold" className="tracking-widest">FEATURED LEAGUES</Heading> <Heading level={5} color="var(--ui-color-text-low)" weight="bold" letterSpacing="widest" uppercase>FEATURED LEAGUES</Heading>
<Link href={routes.public.leagues}> <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> </Link>
</Stack> </Stack>
<Stack gap={4}> <Stack gap={4}>
@@ -65,10 +65,10 @@ export function DiscoverySection({ viewData }: DiscoverySectionProps) {
{/* Teams */} {/* Teams */}
<Stack gap={8}> <Stack gap={8}>
<Stack direction="row" align="center" justify="between" className="border-b border-border-gray/30 pb-4"> <Stack direction="row" align="center" justify="between" borderBottom borderColor="var(--ui-color-border-low)" pb={4}>
<Heading level={5} color="text-gray-400" weight="bold" className="tracking-widest">TEAMS ON THE GRID</Heading> <Heading level={5} color="var(--ui-color-text-low)" weight="bold" letterSpacing="widest" uppercase>TEAMS ON THE GRID</Heading>
<Link href={routes.public.teams}> <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> </Link>
</Stack> </Stack>
<Stack gap={4}> <Stack gap={4}>
@@ -87,10 +87,10 @@ export function DiscoverySection({ viewData }: DiscoverySectionProps) {
{/* Upcoming races */} {/* Upcoming races */}
<Stack gap={8}> <Stack gap={8}>
<Stack direction="row" align="center" justify="between" className="border-b border-border-gray/30 pb-4"> <Stack direction="row" align="center" justify="between" borderBottom borderColor="var(--ui-color-border-low)" pb={4}>
<Heading level={5} color="text-gray-400" weight="bold" className="tracking-widest">UPCOMING RACES</Heading> <Heading level={5} color="var(--ui-color-text-low)" weight="bold" letterSpacing="widest" uppercase>UPCOMING RACES</Heading>
<Link href={routes.public.races}> <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> </Link>
</Stack> </Stack>
{viewData.upcomingRaces.length === 0 ? ( {viewData.upcomingRaces.length === 0 ? (

View File

@@ -1,12 +1,6 @@
'use client'; 'use client';
import { Heading } from '@/ui/Heading'; import { FAQSection } from '@/components/home/FAQSection';
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';
const faqs = [ 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() { export function FAQ() {
return ( return (
<Stack as="section" position="relative" py={{ base: 20, md: 32 }} bg="graphite-black" overflow="hidden" borderBottom borderColor="border-gray/50"> <FAQSection
{/* Background image with mask */} title="Frequently Asked Questions"
<Stack subtitle="Support & Information"
position="absolute" faqs={faqs}
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>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
import { motion, useReducedMotion } from 'framer-motion'; import { motion, useReducedMotion } from 'framer-motion';
import { ReactNode, useEffect, useState } from 'react'; import { ReactNode, useEffect, useState } from 'react';
import { Box } from '@/ui/Box';
import { Surface } from '@/ui/Surface';
interface MockupStackProps { interface MockupStackProps {
children: ReactNode; children: ReactNode;
@@ -28,9 +29,13 @@ export function MockupStack({ children, index = 0 }: MockupStackProps) {
// On mobile or before mount, render without animations // On mobile or before mount, render without animations
if (!isMounted || isMobile) { if (!isMounted || isMobile) {
return ( 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))' }}>
<div <Surface
className="absolute rounded-none bg-panel-gray/80 border border-border-gray/50" variant="muted"
position="absolute"
rounded="none"
border
borderColor="var(--ui-color-border-low)"
style={{ style={{
rotate: `${rotation1}deg`, rotate: `${rotation1}deg`,
zIndex: 1, zIndex: 1,
@@ -43,8 +48,12 @@ export function MockupStack({ children, index = 0 }: MockupStackProps) {
}} }}
/> />
<div <Surface
className="absolute rounded-none bg-panel-gray/90 border border-border-gray/50" variant="muted"
position="absolute"
rounded="none"
border
borderColor="var(--ui-color-border-low)"
style={{ style={{
rotate: `${rotation2}deg`, rotate: `${rotation2}deg`,
zIndex: 2, zIndex: 2,
@@ -57,24 +66,34 @@ export function MockupStack({ children, index = 0 }: MockupStackProps) {
}} }}
/> />
<div <Box
className="relative z-10 w-full h-full rounded-none overflow-hidden border border-border-gray/30" position="relative"
zIndex={10}
fullWidth
fullHeight
rounded="none"
overflow="hidden"
border
borderColor="var(--ui-color-border-low)"
style={{ style={{
boxShadow: '0 20px 60px rgba(0,0,0,0.45)', boxShadow: '0 20px 60px rgba(0,0,0,0.45)',
}} }}
> >
{children} {children}
</div> </Box>
</div> </Box>
); );
} }
// Desktop: render with animations // Desktop: render with animations
return ( 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 <motion.div
className="absolute rounded-none bg-panel-gray/80 border border-border-gray/50"
style={{ style={{
position: 'absolute',
borderRadius: '0',
backgroundColor: 'rgba(20, 22, 25, 0.8)',
border: '1px solid rgba(35, 39, 43, 0.5)',
rotate: `${rotation1}deg`, rotate: `${rotation1}deg`,
zIndex: 1, zIndex: 1,
top: '-8px', top: '-8px',
@@ -89,8 +108,11 @@ export function MockupStack({ children, index = 0 }: MockupStackProps) {
/> />
<motion.div <motion.div
className="absolute rounded-none bg-panel-gray/90 border border-border-gray/50"
style={{ style={{
position: 'absolute',
borderRadius: '0',
backgroundColor: 'rgba(20, 22, 25, 0.9)',
border: '1px solid rgba(35, 39, 43, 0.5)',
rotate: `${rotation2}deg`, rotate: `${rotation2}deg`,
zIndex: 2, zIndex: 2,
top: '-4px', top: '-4px',
@@ -105,8 +127,14 @@ export function MockupStack({ children, index = 0 }: MockupStackProps) {
/> />
<motion.div <motion.div
className="relative z-10 w-full h-full rounded-none overflow-hidden border border-border-gray/30"
style={{ 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)', boxShadow: '0 20px 60px rgba(0,0,0,0.45)',
}} }}
whileHover={ whileHover={
@@ -129,7 +157,12 @@ export function MockupStack({ children, index = 0 }: MockupStackProps) {
transition={{ duration: 0.4, delay: 0.2 }} transition={{ duration: 0.4, delay: 0.2 }}
> >
<motion.div <motion.div
className="absolute inset-0 pointer-events-none rounded-none" style={{
position: 'absolute',
inset: 0,
pointerEvents: 'none',
borderRadius: '0',
}}
whileHover={ whileHover={
shouldReduceMotion shouldReduceMotion
? {} ? {}
@@ -141,6 +174,6 @@ export function MockupStack({ children, index = 0 }: MockupStackProps) {
/> />
{children} {children}
</motion.div> </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) { if (isMobile) {
return ( 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}> <Box display="flex" alignItems="center" justifyContent="center" gap={3}>
{steps.map((step, i) => ( {steps.map((step, i) => (
<Box key={step.name} display="flex" alignItems="center"> <Box key={step.name} display="flex" alignItems="center">
@@ -61,17 +52,19 @@ export function ProtestWorkflowMockup() {
mb={1} mb={1}
border border
borderWidth="1px" 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"> <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} /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={step.icon} />
</Box> </Box>
</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> </Box>
{i < steps.length - 1 && ( {i < steps.length - 1 && (
<Box as="svg" w="4" h="4" mx={1} viewBox="0 0 24 24" fill="none"> <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>
)} )}
</Box> </Box>
@@ -83,7 +76,7 @@ export function ProtestWorkflowMockup() {
position="absolute" position="absolute"
insetY="0" insetY="0"
left="0" left="0"
bg="primary-accent" bg="var(--ui-color-intent-primary)"
style={{ width: `${((activeStep + 1) / steps.length) * 100}%` }} style={{ width: `${((activeStep + 1) / steps.length) * 100}%` }}
/> />
</Box> </Box>
@@ -104,7 +97,7 @@ export function ProtestWorkflowMockup() {
}; };
return ( 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 }}> <Box display="flex" flexDirection={{ base: 'col', md: 'row' }} alignItems="center" justifyContent="center" gap={{ base: 2, sm: 3, md: 4 }}>
{steps.map((step, i) => ( {steps.map((step, i) => (
<Box key={step.name} display="flex" alignItems="center" flexShrink={0}> <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 }} mb={{ base: 1, sm: 1.5, md: 2 }}
border border
borderWidth="1px" 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 ? {} : { whileHover={shouldReduceMotion ? {} : {
scale: 1.05, scale: 1.05,
borderColor: '#198CFF', borderColor: 'var(--ui-color-intent-primary)',
transition: { duration: 0.2 } transition: { duration: 0.2 }
}} }}
> >
@@ -142,7 +137,7 @@ export function ProtestWorkflowMockup() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={step.icon} /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={step.icon} />
</Box> </Box>
{step.status === 'active' && ( {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> </Box>
<Text <Text
@@ -151,7 +146,8 @@ export function ProtestWorkflowMockup() {
color="text-white" color="text-white"
weight="bold" weight="bold"
textAlign="center" textAlign="center"
className="uppercase tracking-widest" uppercase
letterSpacing="widest"
> >
{step.name} {step.name}
</Text> </Text>
@@ -159,13 +155,13 @@ export function ProtestWorkflowMockup() {
{i < steps.length - 1 && ( {i < steps.length - 1 && (
<Box <Box
className="hidden md:block" display={{ base: 'none', md: 'block' }}
position="relative" position="relative"
ml={2} ml={2}
mr={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"> <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>
</Box> </Box>
)} )}
@@ -189,7 +185,7 @@ export function ProtestWorkflowMockup() {
position="absolute" position="absolute"
insetY="0" insetY="0"
left="0" left="0"
bg="primary-accent" bg="var(--ui-color-intent-primary)"
initial={{ width: '0%' }} initial={{ width: '0%' }}
animate={{ width: `${((activeStep + 1) / steps.length) * 100}%` }} animate={{ width: `${((activeStep + 1) / steps.length) * 100}%` }}
transition={{ duration: 0.5, ease: 'easeOut' }} transition={{ duration: 0.5, ease: 'easeOut' }}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
import { ChevronDown, ChevronUp } from 'lucide-react'; import { ChevronDown, ChevronUp } from 'lucide-react';
import { ReactNode, useState } from 'react'; import { ReactNode, useState } from 'react';
import { Box } from './Box'; import { Icon } from '@/ui/Icon';
import { Icon } from './Icon'; import { Surface } from '@/ui/Surface';
import { Surface } from './Surface'; import { Text } from '@/ui/Text';
import { Text } from './Text'; import { Box } from '@/ui/Box';
export interface AccordionProps { export interface AccordionProps {
title: string; title: string;
@@ -36,15 +36,23 @@ export const Accordion = ({
return ( return (
<Surface variant="muted" rounded="lg" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}> <Surface variant="muted" rounded="lg" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
<button <Box
as="button"
onClick={handleToggle} 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"> <Text weight="bold" size="sm" variant="high">
{title} {title}
</Text> </Text>
<Icon icon={isOpen ? ChevronUp : ChevronDown} size={4} intent="low" /> <Icon icon={isOpen ? ChevronUp : ChevronDown} size={4} intent="low" />
</button> </Box>
{isOpen && ( {isOpen && (
<Box padding={4} borderTop> <Box padding={4} borderTop>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,25 +1,18 @@
'use client'; 'use client';
import { DashboardControlBar } from '@/components/dashboard/DashboardControlBar';
import { DashboardKpiRow } from '@/components/dashboard/DashboardKpiRow'; 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 { RecentActivityTable, type ActivityItem } from '@/components/dashboard/RecentActivityTable';
import { TelemetryPanel } from '@/components/dashboard/TelemetryPanel'; import { TelemetryPanel } from '@/components/dashboard/TelemetryPanel';
import { routes } from '@/lib/routing/RouteConfig';
import type { DashboardViewData } from '@/lib/view-data/DashboardViewData'; import type { DashboardViewData } from '@/lib/view-data/DashboardViewData';
import { Avatar } from '@/ui/Avatar';
import { Box } from '@/ui/Box'; import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { IconButton } from '@/ui/IconButton';
import { Grid } from '@/ui/Grid'; import { Grid } from '@/ui/Grid';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { Bell, Calendar, LayoutDashboard, Search, Settings, Trophy, Users } from 'lucide-react';
import { useRouter } from 'next/navigation';
interface DashboardTemplateProps { interface DashboardTemplateProps {
viewData: DashboardViewData; viewData: DashboardViewData;
onNavigateToRaces: () => void;
} }
/** /**
@@ -29,8 +22,10 @@ interface DashboardTemplateProps {
* Composes semantic dashboard components into a high-density data environment. * Composes semantic dashboard components into a high-density data environment.
* Complies with architectural constraints by using UI primitives. * Complies with architectural constraints by using UI primitives.
*/ */
export function DashboardTemplate({ viewData }: DashboardTemplateProps) { export function DashboardTemplate({
const router = useRouter(); viewData,
onNavigateToRaces,
}: DashboardTemplateProps) {
const { const {
currentDriver, currentDriver,
nextRace, nextRace,
@@ -43,11 +38,11 @@ export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
} = viewData; } = viewData;
const kpiItems = [ const kpiItems = [
{ label: 'Rating', value: currentDriver.rating, color: 'var(--color-telemetry)' }, { label: 'Rating', value: currentDriver.rating, intent: 'primary' as const },
{ label: 'Rank', value: `#${currentDriver.rank}`, color: 'var(--color-warning)' }, { label: 'Rank', value: `#${currentDriver.rank}`, intent: 'warning' as const },
{ label: 'Starts', value: currentDriver.totalRaces }, { label: 'Starts', value: currentDriver.totalRaces },
{ label: 'Wins', value: currentDriver.wins, color: 'var(--color-success)' }, { label: 'Wins', value: currentDriver.wins, intent: 'success' as const },
{ label: 'Podiums', value: currentDriver.podiums, color: 'var(--color-warning)' }, { label: 'Podiums', value: currentDriver.podiums, intent: 'warning' as const },
{ label: 'Leagues', value: activeLeaguesCount }, { label: 'Leagues', value: activeLeaguesCount },
]; ];
@@ -59,68 +54,8 @@ export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
status: item.type === 'race_result' ? 'success' : 'info' 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 ( return (
<DashboardShell <Stack gap={6}>
rail={railContent}
controlBar={<DashboardControlBar title="Telemetry Workspace" actions={controlBarActions} />}
>
{/* KPI Overview */} {/* KPI Overview */}
<DashboardKpiRow items={kpiItems} /> <DashboardKpiRow items={kpiItems} />
@@ -132,14 +67,14 @@ export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
<TelemetryPanel title="Active Session"> <TelemetryPanel title="Active Session">
<Box display="flex" alignItems="center" justifyContent="between"> <Box display="flex" alignItems="center" justifyContent="between">
<Box> <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="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>
<Box textAlign="right"> <Box textAlign="right">
<Text size="xs" color="var(--color-text-low)" mb={1} block>Starts In</Text> <Text size="xs" color="text-gray-500" mb={1} block>Starts In</Text>
<Text size="xl" font="mono" weight="bold" color="var(--color-warning)" block>{nextRace.timeUntil}</Text> <Text size="xl" font="mono" weight="bold" color="warning-amber" 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" block>{nextRace.formattedDate} @ {nextRace.formattedTime}</Text>
</Box> </Box>
</Box> </Box>
</TelemetryPanel> </TelemetryPanel>
@@ -150,7 +85,7 @@ export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
<RecentActivityTable items={activityItems} /> <RecentActivityTable items={activityItems} />
) : ( ) : (
<Box py={8} textAlign="center"> <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> </Box>
)} )}
</TelemetryPanel> </TelemetryPanel>
@@ -164,18 +99,18 @@ export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
{hasLeagueStandings ? ( {hasLeagueStandings ? (
<Stack direction="col" gap={3}> <Stack direction="col" gap={3}>
{leagueStandings.map((standing) => ( {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> <Box>
<Text size="xs" weight="bold" truncate block maxWidth="180px">{standing.leagueName}</Text> <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> </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> </Box>
))} ))}
</Stack> </Stack>
) : ( ) : (
<Box py={4} textAlign="center"> <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> </Box>
)} )}
</TelemetryPanel> </TelemetryPanel>
@@ -183,21 +118,21 @@ export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
<TelemetryPanel title="Upcoming Schedule"> <TelemetryPanel title="Upcoming Schedule">
<Stack direction="col" gap={4}> <Stack direction="col" gap={4}>
{upcomingRaces.slice(0, 3).map((race) => ( {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}> <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" weight="bold">{race.track}</Text>
<Text size="xs" font="mono" color="var(--color-text-low)">{race.timeUntil}</Text> <Text size="xs" font="mono" color="text-gray-500">{race.timeUntil}</Text>
</Box> </Box>
<Box display="flex" justifyContent="between"> <Box display="flex" justifyContent="between">
<Text size="xs" color="var(--color-text-low)">{race.car}</Text> <Text size="xs" color="text-gray-500">{race.car}</Text>
<Text size="xs" color="var(--color-text-low)">{race.formattedDate}</Text> <Text size="xs" color="text-gray-500">{race.formattedDate}</Text>
</Box> </Box>
</Box> </Box>
))} ))}
<Button <Button
variant="secondary" variant="secondary"
fullWidth fullWidth
onClick={() => router.push(routes.public.races)} onClick={onNavigateToRaces}
> >
<Text size="xs" weight="bold" uppercase letterSpacing="widest">View Full Schedule</Text> <Text size="xs" weight="bold" uppercase letterSpacing="widest">View Full Schedule</Text>
</Button> </Button>
@@ -206,6 +141,6 @@ export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
</Stack> </Stack>
</Box> </Box>
</Grid> </Grid>
</DashboardShell> </Stack>
); );
} }

View File

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

View File

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

View File

@@ -95,7 +95,7 @@ export function LeagueAdminScheduleTemplate({
<Stack direction="row" align="center" justify="between"> <Stack direction="row" align="center" justify="between">
<Text size="sm" color="text-gray-300"> <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> </Text>
<Button <Button
onClick={onPublishToggle} onClick={onPublishToggle}
@@ -107,12 +107,12 @@ export function LeagueAdminScheduleTemplate({
</Button> </Button>
</Stack> </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}> <Box mb={4}>
<Heading level={2}>{isEditing ? 'Edit race' : 'Add race'}</Heading> <Heading level={2}>{isEditing ? 'Edit race' : 'Add race'}</Heading>
</Box> </Box>
<Grid cols={3} gap={4}> <Grid responsiveGridCols={{ base: 1, md: 3 }} gap={4}>
<Box> <Box>
<Text size="sm" color="text-gray-300" block mb={2}>Track</Text> <Text size="sm" color="text-gray-300" block mb={2}>Track</Text>
<Input <Input
@@ -161,7 +161,7 @@ export function LeagueAdminScheduleTemplate({
</Stack> </Stack>
</Box> </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}> <Box mb={4}>
<Heading level={2}>Races</Heading> <Heading level={2}>Races</Heading>
</Box> </Box>
@@ -178,7 +178,7 @@ export function LeagueAdminScheduleTemplate({
> >
<Stack direction="row" align="center" justify="between"> <Stack direction="row" align="center" justify="between">
<Box> <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> <Text size="xs" color="text-gray-400" block mt={1}>{race.scheduledAt}</Text>
</Box> </Box>

View File

@@ -23,19 +23,19 @@ export function LeagueSettingsTemplate({ viewData }: LeagueSettingsTemplateProps
<Surface variant="dark" border rounded="lg" padding={6}> <Surface variant="dark" border rounded="lg" padding={6}>
<Stack gap={6}> <Stack gap={6}>
<Stack direction="row" align="center" gap={3}> <Stack direction="row" align="center" gap={3}>
<Box p={2} bg="bg-primary-blue/10" rounded="md" border borderColor="border-primary-blue/20"> <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="text-primary-blue" /> <Icon icon={Settings} size={5} color="primary-accent" />
</Box> </Box>
<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> <Text size="xs" color="text-gray-500">Basic league details and identification</Text>
</Box> </Box>
</Stack> </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="Name" value={viewData.league.name} />
<InfoItem label="Visibility" value={viewData.league.visibility} capitalize /> <InfoItem label="Visibility" value={viewData.league.visibility} capitalize />
<GridItem colSpan={2}> <GridItem responsiveColSpan={{ base: 1, md: 2 }}>
<InfoItem label="Description" value={viewData.league.description} /> <InfoItem label="Description" value={viewData.league.description} />
</GridItem> </GridItem>
<InfoItem label="Created" value={new Date(viewData.league.createdAt).toLocaleDateString()} /> <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}> <Surface variant="dark" border rounded="lg" padding={6}>
<Stack gap={6}> <Stack gap={6}>
<Stack direction="row" align="center" gap={3}> <Stack direction="row" align="center" gap={3}>
<Box p={2} bg="bg-performance-green/10" rounded="md" border borderColor="border-performance-green/20"> <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-performance-green" /> <Icon icon={Trophy} size={5} color="text-success-green" />
</Box> </Box>
<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> <Text size="xs" color="text-gray-500">League rules and participation limits</Text>
</Box> </Box>
</Stack> </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={Users} label="Max Drivers" value={viewData.config.maxDrivers} />
<ConfigItem icon={Shield} label="Require Approval" value={viewData.config.requireApproval ? 'Yes' : 'No'} /> <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'} /> <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 */} {/* Note about forms */}
<Surface variant="dark" border rounded="lg" padding={8}> <Surface variant="dark" border rounded="lg" padding={8}>
<Stack align="center" gap={4}> <Stack align="center" gap={4}>
<Box p={4} bg="bg-warning-amber/10" rounded="full" border borderColor="border-warning-amber/20"> <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="text-warning-amber" /> <Icon icon={Settings} size={8} color="warning-amber" />
</Box> </Box>
<Box textAlign="center"> <Box textAlign="center">
<Heading level={3}>Settings Management</Heading> <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. Form-based editing and ownership transfer functionality will be implemented in future updates.
</Text> </Text>
</Box> </Box>
@@ -88,8 +88,8 @@ export function LeagueSettingsTemplate({ viewData }: LeagueSettingsTemplateProps
function InfoItem({ label, value, capitalize }: { label: string, value: string, capitalize?: boolean }) { function InfoItem({ label, value, capitalize }: { label: string, value: string, capitalize?: boolean }) {
return ( return (
<Box> <Box>
<Text size="xs" weight="bold" color="text-gray-500" display="block" mb={1} letterSpacing="wider">{label.toUpperCase()}</Text> <Text size="xs" weight="bold" color="text-gray-500" block mb={1} letterSpacing="wider">{label.toUpperCase()}</Text>
<Text color="text-white" weight="medium">{capitalize ? value.toUpperCase() : value}</Text> <Text color="white" weight="medium">{capitalize ? value.toUpperCase() : value}</Text>
</Box> </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 }) { function ConfigItem({ icon, label, value }: { icon: LucideIcon, label: string, value: string | number }) {
return ( return (
<Stack direction="row" align="center" gap={4}> <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" /> <Icon icon={icon} size={5} color="text-gray-400" />
</Box> </Box>
<Box> <Box>
<Text size="xs" weight="bold" color="text-gray-500" display="block" letterSpacing="wider">{label.toUpperCase()}</Text> <Text size="xs" weight="bold" color="text-gray-500" block letterSpacing="wider">{label.toUpperCase()}</Text>
<Text color="text-white" weight="medium">{value}</Text> <Text color="white" weight="medium">{value}</Text>
</Box> </Box>
</Stack> </Stack>
); );

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,12 +8,13 @@ import { RaceScheduleTable } from '@/components/races/RaceScheduleTable';
import { RaceSidebar } from '@/components/races/RaceSidebar'; import { RaceSidebar } from '@/components/races/RaceSidebar';
import type { SessionStatus } from '@/components/races/SessionStatusBadge'; import type { SessionStatus } from '@/components/races/SessionStatusBadge';
import type { RacesViewData } from '@/lib/view-data/RacesViewData'; import type { RacesViewData } from '@/lib/view-data/RacesViewData';
import { Box } from '@/ui/Box';
import { Container } from '@/ui/Container'; import { Container } from '@/ui/Container';
import { Grid } from '@/ui/Grid'; import { Grid } from '@/ui/Grid';
import { GridItem } from '@/ui/GridItem'; import { GridItem } from '@/ui/GridItem';
import { Stack } from '@/ui/Stack'; import { Group } from '@/ui/Group';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { Panel } from '@/ui/Panel';
import React from 'react';
export type TimeFilter = 'all' | 'upcoming' | 'live' | 'past'; export type TimeFilter = 'all' | 'upcoming' | 'live' | 'past';
export type RaceStatusFilter = 'scheduled' | 'running' | 'completed' | 'cancelled' | 'all'; export type RaceStatusFilter = 'scheduled' | 'running' | 'completed' | 'cancelled' | 'all';
@@ -50,78 +51,77 @@ export function RacesTemplate({
setShowFilterModal, setShowFilterModal,
}: RacesTemplateProps) { }: RacesTemplateProps) {
return ( return (
<Box as="main" minHeight="screen" bg="bg-base-black" py={8}> <Container size="lg" py={8}>
<Container size="lg"> <Group direction="column" gap={8} fullWidth>
<Stack gap={8}> <RacePageHeader
<RacePageHeader totalCount={viewData.totalCount}
totalCount={viewData.totalCount} scheduledCount={viewData.scheduledCount}
scheduledCount={viewData.scheduledCount} runningCount={viewData.runningCount}
runningCount={viewData.runningCount} completedCount={viewData.completedCount}
completedCount={viewData.completedCount} />
/>
<LiveRacesBanner <LiveRacesBanner
liveRaces={viewData.liveRaces} liveRaces={viewData.liveRaces}
onRaceClick={onRaceClick} onRaceClick={onRaceClick}
/> />
<Grid cols={12} gap={6}> <Grid cols={12} gap={6}>
<GridItem colSpan={12} lgSpan={8}> <GridItem colSpan={12} lgSpan={8}>
<Stack gap={6}> <Group direction="column" gap={6} fullWidth>
<RaceFilterBar <RaceFilterBar
timeFilter={timeFilter} timeFilter={timeFilter}
setTimeFilter={setTimeFilter} setTimeFilter={setTimeFilter}
leagueFilter={leagueFilter} leagueFilter={leagueFilter}
setLeagueFilter={setLeagueFilter} setLeagueFilter={setLeagueFilter}
leagues={viewData.leagues} leagues={viewData.leagues}
onShowMoreFilters={() => setShowFilterModal(true)} 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}
/> />
</GridItem>
</Grid>
<RaceFilterModal <Panel
isOpen={showFilterModal} title="Race Schedule"
onClose={() => setShowFilterModal(false)} variant="dark"
statusFilter={statusFilter} padding={0}
setStatusFilter={setStatusFilter} >
leagueFilter={leagueFilter} <RaceScheduleTable
setLeagueFilter={setLeagueFilter} races={viewData.races.map(race => ({
timeFilter={timeFilter} id: race.id,
setTimeFilter={setTimeFilter} track: race.track,
searchQuery="" car: race.car,
setSearchQuery={() => {}} leagueName: race.leagueName,
leagues={viewData.leagues} time: race.timeLabel,
showSearch={false} status: race.status as SessionStatus
showTimeFilter={false} }))}
/> onRaceClick={onRaceClick}
</Stack> />
</Container> </Panel>
</Box> </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> <Box>
<Stack direction="row" align="center" justify="between" mb={4}> <Stack direction="row" align="center" justify="between" mb={4}>
<Stack direction="row" align="center" gap={2}> <Stack direction="row" align="center" gap={2}>
<Icon icon={UserPlus} size={4} color="text-primary-blue" /> <Icon icon={UserPlus} size={4} color="primary-accent" />
<Heading level={5} color="text-primary-blue">PENDING JOIN REQUESTS</Heading> <Heading level={5} color="primary-accent">PENDING JOIN REQUESTS</Heading>
</Stack> </Stack>
<Box px={2} py={0.5} rounded="md" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20"> <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="text-primary-blue" weight="bold">{pendingCountLabel}</Text> <Text size="xs" color="primary-accent" weight="bold">{pendingCountLabel}</Text>
</Box> </Box>
</Stack> </Stack>
@@ -58,13 +58,13 @@ export function RosterAdminTemplate({
<Surface variant="dark" border rounded="lg" overflow="hidden"> <Surface variant="dark" border rounded="lg" overflow="hidden">
<Stack gap={0}> <Stack gap={0}>
{joinRequests.map((req) => ( {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 direction={{ base: 'col', md: 'row' }} align="center" justify="between" gap={4}>
<Stack gap={1}> <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> <Text size="xs" color="text-gray-500">{req.formattedRequestedAt}</Text>
{req.message && ( {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> </Stack>
@@ -91,8 +91,8 @@ export function RosterAdminTemplate({
{/* Members Section */} {/* Members Section */}
<Box> <Box>
<Stack direction="row" align="center" gap={2} mb={4}> <Stack direction="row" align="center" gap={2} mb={4}>
<Icon icon={Shield} size={4} color="text-performance-green" /> <Icon icon={Shield} size={4} color="text-success-green" />
<Heading level={5} color="text-performance-green">ACTIVE ROSTER</Heading> <Heading level={5} color="text-success-green">ACTIVE ROSTER</Heading>
</Stack> </Stack>
{loading ? ( {loading ? (
@@ -114,7 +114,7 @@ export function RosterAdminTemplate({
{members.map((member) => ( {members.map((member) => (
<TableRow key={member.driverId}> <TableRow key={member.driverId}>
<TableCell> <TableCell>
<Text weight="bold" color="text-white">{member.driver.name}</Text> <Text weight="bold" color="white">{member.driver.name}</Text>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Text size="sm" color="text-gray-400">{member.formattedJoinedAt}</Text> <Text size="sm" color="text-gray-400">{member.formattedJoinedAt}</Text>
@@ -124,15 +124,13 @@ export function RosterAdminTemplate({
as="select" as="select"
value={member.role} value={member.role}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => onRoleChange(member.driverId, e.target.value as MembershipRole)} 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 border
borderColor="border-charcoal-outline" borderColor="rgba(255,255,255,0.1)"
rounded="md" rounded="md"
px={2} px={2}
py={1} py={1}
fontSize="xs" color="white"
weight="bold"
color="text-white"
> >
{roleOptions.map((role) => ( {roleOptions.map((role) => (
<Box as="option" key={role} value={role}>{role.toUpperCase()}</Box> <Box as="option" key={role} value={role}>{role.toUpperCase()}</Box>
@@ -146,8 +144,8 @@ export function RosterAdminTemplate({
size="sm" size="sm"
> >
<Stack direction="row" align="center" gap={1.5}> <Stack direction="row" align="center" gap={1.5}>
<Icon icon={UserMinus} size={3.5} color="text-error-red" /> <Icon icon={UserMinus} size={3.5} color="critical-red" />
<Text size="xs" weight="bold" color="text-error-red">REMOVE</Text> <Text size="xs" weight="bold" color="critical-red">REMOVE</Text>
</Stack> </Stack>
</Button> </Button>
</TableCell> </TableCell>

View File

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

View File

@@ -2,15 +2,17 @@
import { LeaderboardFiltersBar } from '@/components/leaderboards/LeaderboardFiltersBar'; import { LeaderboardFiltersBar } from '@/components/leaderboards/LeaderboardFiltersBar';
import type { SkillLevel, SortBy, TeamLeaderboardViewData } from '@/lib/view-data/TeamLeaderboardViewData'; import type { SkillLevel, SortBy, TeamLeaderboardViewData } from '@/lib/view-data/TeamLeaderboardViewData';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { Container } from '@/ui/Container'; import { Container } from '@/ui/Container';
import { Heading } from '@/ui/Heading'; import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon'; 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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/ui/Table';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { Panel } from '@/ui/Panel';
import { Section } from '@/ui/Section';
import { Award, ChevronLeft } from 'lucide-react'; import { Award, ChevronLeft } from 'lucide-react';
import React from 'react';
interface TeamLeaderboardTemplateProps { interface TeamLeaderboardTemplateProps {
viewData: TeamLeaderboardViewData; viewData: TeamLeaderboardViewData;
@@ -30,87 +32,88 @@ export function TeamLeaderboardTemplate({
const { searchQuery, filteredAndSortedTeams } = viewData; const { searchQuery, filteredAndSortedTeams } = viewData;
return ( return (
<Box bg="base-black" minH="screen"> <Container size="lg" py={12}>
<Container size="lg" py={12}> <Group direction="column" gap={8} fullWidth>
<Stack gap={8}> {/* Header */}
{/* Header */} <Group direction="row" align="center" justify="between" fullWidth>
<Stack direction="row" align="center" justify="between"> <Group direction="row" align="center" gap={4}>
<Stack direction="row" align="center" gap={4}> <Button variant="secondary" size="sm" onClick={onBackToTeams} icon={<Icon icon={ChevronLeft} size={4} />}>
<Button variant="secondary" size="sm" onClick={onBackToTeams} icon={<Icon icon={ChevronLeft} size={4} />}> Back
Back </Button>
</Button> <Group direction="column">
<Box> <Heading level={1} weight="bold">Global Standings</Heading>
<Heading level={1} weight="bold">Global Standings</Heading> <Text variant="low" size="sm" font="mono" uppercase letterSpacing="widest">Team Performance Index</Text>
<Text color="text-gray-500" size="sm" font="mono" uppercase letterSpacing="widest">Team Performance Index</Text> </Group>
</Box> </Group>
</Stack> <Icon icon={Award} size={8} color="var(--ui-color-intent-warning)" />
<Icon icon={Award} size={8} color="warning-amber" /> </Group>
</Stack>
<LeaderboardFiltersBar <LeaderboardFiltersBar
searchQuery={searchQuery} searchQuery={searchQuery}
onSearchChange={onSearchChange} onSearchChange={onSearchChange}
placeholder="Search teams..." placeholder="Search teams..."
/> />
<Box border borderColor="outline-steel" bg="surface-charcoal/30"> <Panel variant="dark" padding={0}>
<Table> <Table>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableHeader w="20">Rank</TableHeader> <TableHeader w="20">Rank</TableHeader>
<TableHeader>Team</TableHeader> <TableHeader>Team</TableHeader>
<TableHeader textAlign="center">Personnel</TableHeader> <TableHeader textAlign="center">Personnel</TableHeader>
<TableHeader textAlign="center">Races</TableHeader> <TableHeader textAlign="center">Races</TableHeader>
<TableHeader textAlign="right">Rating</TableHeader> <TableHeader textAlign="right">Rating</TableHeader>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{filteredAndSortedTeams.length > 0 ? ( {filteredAndSortedTeams.length > 0 ? (
filteredAndSortedTeams.map((team, index) => ( filteredAndSortedTeams.map((team, index) => (
<TableRow <TableRow
key={team.id} key={team.id}
onClick={() => onTeamClick(team.id)} onClick={() => onTeamClick(team.id)}
cursor="pointer" clickable
hoverBg="surface-charcoal/50" >
> <TableCell>
<TableCell> <Text font="mono" weight="bold" variant={index < 3 ? 'warning' : 'low'}>
<Text font="mono" weight="bold" color={index < 3 ? 'warning-amber' : 'text-gray-500'}> #{index + 1}
#{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
</Text> </Text>
</TableCell> </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> </TableRow>
)} ))
</TableBody> ) : (
</Table> <TableRow>
</Box> <TableCell colSpan={5} textAlign="center">
</Stack> <Section variant="dark" padding="lg">
</Container> <Group align="center" justify="center" fullWidth>
</Box> <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 { TeamLeaderboardPreview } from '@/components/teams/TeamLeaderboardPreviewWrapper';
import { TeamsDirectoryHeader } from '@/components/teams/TeamsDirectoryHeader'; import { TeamsDirectoryHeader } from '@/components/teams/TeamsDirectoryHeader';
import { TeamsDirectory, TeamsDirectorySection } from '@/components/teams/TeamsDirectory'; 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 type { TeamsViewData } from '@/lib/view-data/TeamsViewData';
import { Users } from 'lucide-react'; import { Users } from 'lucide-react';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts'; import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
import React from 'react';
interface TeamsTemplateProps extends TemplateProps<TeamsViewData> { interface TeamsTemplateProps extends TemplateProps<TeamsViewData> {
onTeamClick?: (teamId: string) => void; onTeamClick?: (teamId: string) => void;
@@ -41,7 +42,7 @@ export function TeamsTemplate({ viewData, onTeamClick, onViewFullLeaderboard, on
))} ))}
</TeamGrid> </TeamGrid>
) : ( ) : (
<SharedEmptyState <EmptyState
icon={Users} icon={Users}
title="No teams yet" title="No teams yet"
description="Get started by creating your first racing team" 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 { AuthForm } from '@/components/auth/AuthForm';
import { ForgotPasswordViewData } from '@/lib/builders/view-data/types/ForgotPasswordViewData'; import { ForgotPasswordViewData } from '@/lib/builders/view-data/types/ForgotPasswordViewData';
import { routes } from '@/lib/routing/RouteConfig'; import { routes } from '@/lib/routing/RouteConfig';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { Group } from '@/ui/Group';
import { Icon } from '@/ui/Icon'; import { Icon } from '@/ui/Icon';
import { Input } from '@/ui/Input'; import { Input } from '@/ui/Input';
import { Link } from '@/ui/Link'; import { Link } from '@/ui/Link';
import { LoadingSpinner } from '@/ui/LoadingSpinner'; import { LoadingSpinner } from '@/ui/LoadingSpinner';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { AlertCircle, ArrowLeft, CheckCircle2, Mail, Shield } from 'lucide-react'; import { AlertCircle, ArrowLeft, CheckCircle2, Mail, Shield } from 'lucide-react';
import React from 'react'; import React from 'react';
@@ -54,12 +53,10 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
/> />
{mutationState.error && ( {mutationState.error && (
<Box p={4} bg="critical-red/10" border borderColor="critical-red/30" rounded="md"> <Group direction="row" align="start" gap={3} fullWidth>
<Stack direction="row" align="start" gap={3}> <Icon icon={AlertCircle} size={4.5} color="var(--color-critical)" />
<Icon icon={AlertCircle} size={4.5} color="var(--color-critical)" /> <Text size="sm" color="text-critical-red">{mutationState.error}</Text>
<Text size="sm" color="text-critical-red">{mutationState.error}</Text> </Group>
</Stack>
</Box>
)} )}
<Button <Button
@@ -72,36 +69,34 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
{isSubmitting ? 'Sending...' : 'Send Reset Link'} {isSubmitting ? 'Sending...' : 'Send Reset Link'}
</Button> </Button>
<Box textAlign="center"> <Group justify="center" fullWidth>
<Link href={routes.auth.login}> <Link href={routes.auth.login}>
<Stack direction="row" align="center" justify="center" gap={2} group> <Group direction="row" align="center" justify="center" gap={2}>
<Icon icon={ArrowLeft} size={3.5} color="var(--color-primary)" groupHoverScale /> <Icon icon={ArrowLeft} size={3.5} color="var(--color-primary)" />
<Text size="sm" weight="bold" color="text-primary-accent">Back to Login</Text> <Text size="sm" weight="bold" color="text-primary-accent">Back to Login</Text>
</Stack> </Group>
</Link> </Link>
</Box> </Group>
</AuthForm> </AuthForm>
) : ( ) : (
<Stack gap={6}> <Group direction="column" gap={6} fullWidth>
<Box p={4} bg="success-green/10" border borderColor="success-green/30" rounded="md"> <Group direction="row" align="start" gap={3} fullWidth>
<Stack direction="row" align="start" gap={3}> <Icon icon={CheckCircle2} size={5} color="var(--color-success)" />
<Icon icon={CheckCircle2} size={5} color="var(--color-success)" /> <Group direction="column" gap={1}>
<Box> <Text size="sm" color="text-success-green" weight="bold" block>Check your email</Text>
<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>
<Text size="xs" color="text-gray-400" block mt={1}>{viewData.successMessage}</Text> </Group>
</Box> </Group>
</Stack>
</Box>
{viewData.magicLink && ( {viewData.magicLink && (
<Box p={3} bg="surface-charcoal" border borderColor="outline-steel" rounded="md"> <Group direction="column" gap={2} fullWidth>
<Text size="xs" color="text-gray-500" block mb={2} weight="bold">DEVELOPMENT MAGIC LINK</Text> <Text size="xs" color="text-gray-500" block weight="bold">DEVELOPMENT MAGIC LINK</Text>
<Link href={viewData.magicLink}> <Link href={viewData.magicLink}>
<Text size="xs" color="text-primary-accent" block> <Text size="xs" color="text-primary-accent" block>
{viewData.magicLink} {viewData.magicLink}
</Text> </Text>
</Link> </Link>
</Box> </Group>
)} )}
<Button <Button
@@ -112,7 +107,7 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
> >
Return to Login Return to Login
</Button> </Button>
</Stack> </Group>
)} )}
<AuthFooterLinks> <AuthFooterLinks>

View File

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

View File

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

View File

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

View File

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

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