website refactor
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}>"{req.message}"</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'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.
|
||||
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 && (
|
||||
|
||||
Reference in New Issue
Block a user