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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import { useCurrentSession } from '@/hooks/auth/useCurrentSession';
import { Box } from '@/ui/Box';
import { DashboardRail } from '@/components/dashboard/DashboardRail';
import { Text } from '@/ui/Text';
import { Surface } from '@/ui/Surface';
import { usePathname } from 'next/navigation';
export interface GlobalSidebarViewData {}
@@ -16,21 +17,23 @@ export function GlobalSidebarTemplate(_props: GlobalSidebarViewData) {
const isAuthenticated = !!session;
return (
<DashboardRail>
<Box py={6}>
<Box px={6} mb={8}>
<Text size="xs" color="text-gray-500" weight="bold" font="mono" letterSpacing="0.2em">
NAVIGATION
</Text>
<Surface variant="dark" width="280px" borderRight position="sticky" top="0" height="100vh">
<DashboardRail>
<Box py={6} fullWidth>
<Box px={6} mb={8}>
<Text size="xs" color="text-gray-500" weight="bold" font="mono" letterSpacing="0.2em">
NAVIGATION
</Text>
</Box>
<Box px={3}>
{isAuthenticated ? (
<AuthedNav pathname={pathname} />
) : (
<PublicNav pathname={pathname} />
)}
</Box>
</Box>
<Box px={3}>
{isAuthenticated ? (
<AuthedNav pathname={pathname} />
) : (
<PublicNav pathname={pathname} />
)}
</Box>
</Box>
</DashboardRail>
</DashboardRail>
</Surface>
);
}

View File

@@ -41,10 +41,10 @@ export function RootAppShellTemplate({ children }: RootAppShellViewData) {
</TopNav>
</ControlBar>
<Box display="flex" flexGrow={1} overflow="hidden">
<Box display="flex" flexGrow={1}>
{showSidebar && <GlobalSidebarTemplate />}
<Box display="flex" flexGrow={1} flexDirection="col" overflow="hidden">
<Box as="main" display="flex" flexGrow={1} flexDirection="col">
<ContentViewport fullWidth={!showSidebar}>
{children}
</ContentViewport>

View File

@@ -188,8 +188,8 @@ export function OnboardingTemplate({ viewData }: OnboardingTemplateProps) {
const header = (
<Stack direction="row" align="center" justify="between">
<Stack gap={1}>
<Text size="xl" weight="bold" color="text-white" uppercase letterSpacing="tighter">
GridPilot <Text color="text-primary-blue">Onboarding</Text>
<Text size="xl" weight="bold" color="white" uppercase letterSpacing="tighter">
GridPilot <Text color="primary-accent">Onboarding</Text>
</Text>
<Text size="xs" color="text-gray-500" uppercase letterSpacing="widest">
System Initialization
@@ -204,7 +204,7 @@ export function OnboardingTemplate({ viewData }: OnboardingTemplateProps) {
const sidebar = (
<Stack gap={6}>
<OnboardingHelpPanel title="Onboarding Process">
Welcome to GridPilot. We&apos;re setting up your racing identity. This process ensures you&apos;re ready for the track with a complete profile and a unique AI-generated avatar.
Welcome to GridPilot. We're setting up your racing identity. This process ensures you're ready for the track with a complete profile and a unique AI-generated avatar.
</OnboardingHelpPanel>
{step === 2 && (