website refactor

This commit is contained in:
2026-01-19 02:14:53 +01:00
parent 489c5f7858
commit a8731e6937
70 changed files with 2908 additions and 2423 deletions

View File

@@ -6,16 +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 { 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 {
SharedBox,
SharedButton,
SharedCard,
SharedContainer,
SharedIcon,
SharedGrid,
SharedStack,
SharedText,
SharedBadge
} from '@/components/shared/UIComponents';
import { QuickActionLink } from '@/ui/QuickActionLink';
import { StatusBadge } from '@/ui/StatusBadge';
import { Text } from '@/ui/Text';
import {
Activity,
ArrowRight,
@@ -24,6 +26,7 @@ import {
Shield,
Users
} from 'lucide-react';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
/**
* AdminDashboardTemplate
@@ -35,8 +38,7 @@ export function AdminDashboardTemplate({
viewData,
onRefresh,
isLoading
}: {
viewData: AdminDashboardViewData;
}: TemplateProps<AdminDashboardViewData> & {
onRefresh: () => void;
isLoading: boolean;
}) {
@@ -68,103 +70,93 @@ export function AdminDashboardTemplate({
];
return (
<Container size="lg" py={8}>
<Stack gap={8}>
<AdminHeaderPanel
title="Admin Dashboard"
description="System-wide telemetry and operations control"
isLoading={isLoading}
actions={
<Button
onClick={onRefresh}
disabled={isLoading}
variant="secondary"
size="sm"
icon={<Icon icon={RefreshCw} size={3} animate={isLoading ? 'spin' : 'none'} />}
>
Refresh Telemetry
</Button>
}
/>
<SharedContainer size="lg">
<SharedBox paddingY={8}>
<SharedStack gap={8}>
<AdminHeaderPanel
title="Admin Dashboard"
description="System-wide telemetry and operations control"
isLoading={isLoading}
actions={
<SharedButton
onClick={onRefresh}
disabled={isLoading}
variant="secondary"
size="sm"
icon={<SharedIcon icon={RefreshCw} size={3} animate={isLoading ? 'spin' : 'none'} />}
>
Refresh Telemetry
</SharedButton>
}
/>
<AdminStatsPanel stats={stats} />
<AdminStatsPanel stats={stats} />
<Grid cols={1} mdCols={2} gap={6}>
{/* System Health & Status */}
<Card p={6}>
<Stack gap={6}>
<AdminSectionHeader
title="System Status"
actions={
<StatusBadge variant="success" icon={Activity}>
Operational
</StatusBadge>
}
/>
<Stack gap={4}>
<Box borderTop borderColor="border-gray" opacity={0.3} />
<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="text-warning-amber">{viewData.stats.suspendedUsers}</Text>
</Stack>
</Box>
<Box borderTop borderColor="border-gray" opacity={0.3} />
<Box>
<Stack direction="row" align="center" justify="between" py={2}>
<Text size="sm" color="text-gray-400">Deleted Users</Text>
<Text weight="bold" color="text-error-red">{viewData.stats.deletedUsers}</Text>
</Stack>
</Box>
<Box borderTop borderColor="border-gray" opacity={0.3} />
<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="text-primary-blue">{viewData.stats.newUsersToday}</Text>
</Stack>
</Box>
</Stack>
</Stack>
</Card>
<SharedGrid cols={{ base: 1, md: 2 }} gap={6}>
{/* System Health & Status */}
<SharedCard p={6}>
<SharedStack 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>
}
/>
<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>
{/* Quick Operations */}
<Card p={6}>
<Stack gap={6}>
<AdminSectionHeader title="Quick Operations" />
<Grid cols={1} gap={3}>
<QuickActionLink href={routes.admin.users} variant="blue">
<Stack direction="row" align="center" justify="between" fullWidth>
<Text size="sm" weight="bold">User Management</Text>
<Icon icon={ArrowRight} size={4} />
</Stack>
</QuickActionLink>
<QuickActionLink href="/admin" variant="purple">
<Stack direction="row" align="center" justify="between" fullWidth>
<Text size="sm" weight="bold">Security & Roles</Text>
<Icon icon={ArrowRight} size={4} />
</Stack>
</QuickActionLink>
<QuickActionLink href="/admin" variant="orange">
<Stack direction="row" align="center" justify="between" fullWidth>
<Text size="sm" weight="bold">System Audit Logs</Text>
<Icon icon={ArrowRight} size={4} />
</Stack>
</QuickActionLink>
</Grid>
</Stack>
</Card>
</Grid>
{/* Quick Operations */}
<SharedCard p={6}>
<SharedStack gap={6}>
<AdminSectionHeader title="Quick Operations" />
<SharedGrid cols={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>
<AdminDangerZonePanel
title="System Maintenance"
description="Perform destructive system-wide operations. Use with extreme caution."
>
<Button variant="danger" size="sm">
Enter Maintenance Mode
</Button>
</AdminDangerZonePanel>
</Stack>
</Container>
<AdminDangerZonePanel
title="System Maintenance"
description="Perform destructive system-wide operations. Use with extreme caution."
>
<SharedButton variant="danger" size="sm">
Enter Maintenance Mode
</SharedButton>
</AdminDangerZonePanel>
</SharedStack>
</SharedBox>
</SharedContainer>
);
}

View File

@@ -7,16 +7,15 @@ import { AdminStatsPanel } from '@/components/admin/AdminStatsPanel';
import { AdminUsersTable } from '@/components/admin/AdminUsersTable';
import { BulkActionBar } from '@/components/admin/BulkActionBar';
import { UserFilters } from '@/components/admin/UserFilters';
import { InlineNotice } from '@/ui/InlineNotice';
import { AdminUsersViewData } from '@/lib/view-data/AdminUsersViewData';
import { Button } from '@/ui/Button';
import { Container } from '@/ui/Container';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack';
import { SharedButton, SharedContainer, SharedIcon, SharedStack, SharedBox, SharedText } from '@/components/shared/UIComponents';
import { RefreshCw, ShieldAlert, Users } from 'lucide-react';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
interface AdminUsersTemplateProps {
viewData: AdminUsersViewData;
// We need to add InlineNotice to UIComponents if it's used
// For now I'll assume it's a component or I'll add it to UIComponents
interface AdminUsersTemplateProps extends TemplateProps<AdminUsersViewData> {
onRefresh: () => void;
onSearch: (search: string) => void;
onFilterRole: (role: string) => void;
@@ -37,13 +36,6 @@ interface AdminUsersTemplateProps {
onClearSelection: () => void;
}
/**
* AdminUsersTemplate
*
* Redesigned user management page.
* Uses semantic admin UI blocks and follows "Precision Racing Minimal" theme.
* Stateless template.
*/
export function AdminUsersTemplate({
viewData,
onRefresh,
@@ -103,76 +95,83 @@ export function AdminUsersTemplate({
];
return (
<Container size="lg" py={8}>
<Stack gap={8}>
<AdminHeaderPanel
title="User Management"
description="Monitor and control system access"
isLoading={loading}
actions={
<Button
onClick={onRefresh}
disabled={loading}
variant="secondary"
size="sm"
icon={<Icon icon={RefreshCw} size={3} animate={loading ? 'spin' : 'none'} />}
>
Refresh Data
</Button>
}
/>
{error && (
<InlineNotice
variant="error"
title="Operation Failed"
message={error}
<SharedContainer size="lg">
<SharedBox paddingY={8}>
<SharedStack gap={8}>
<AdminHeaderPanel
title="User Management"
description="Monitor and control system access"
isLoading={loading}
actions={
<SharedButton
onClick={onRefresh}
disabled={loading}
variant="secondary"
size="sm"
icon={<SharedIcon icon={RefreshCw} size={3} animate={loading ? 'spin' : 'none'} />}
>
Refresh Data
</SharedButton>
}
/>
)}
<AdminStatsPanel stats={stats} />
<UserFilters
search={search}
roleFilter={roleFilter}
statusFilter={statusFilter}
onSearch={onSearch}
onFilterRole={onFilterRole}
onFilterStatus={onFilterStatus}
onClearFilters={onClearFilters}
/>
<AdminDataTable>
{viewData.users.length === 0 && !loading ? (
<AdminEmptyState
icon={Users}
title="No users found"
description="Try adjusting your filters or search query"
action={
<Button variant="secondary" size="sm" onClick={onClearFilters}>
Clear All Filters
</Button>
}
/>
) : (
<AdminUsersTable
users={viewData.users}
selectedUserIds={selectedUserIds}
onSelectUser={onSelectUser}
onSelectAll={onSelectAll}
onUpdateStatus={onUpdateStatus}
onDeleteUser={onDeleteUser}
deletingUserId={deletingUser}
/>
{/* error notice should be a component */}
{error && (
<SharedBox bg="bg-error-red/10" p={4} rounded="md" border borderColor="border-error-red/20">
<SharedStack direction="row" align="center" gap={3}>
<SharedIcon icon={ShieldAlert} size={5} color="text-error-red" />
<SharedBox>
<SharedText weight="bold" color="text-error-red">Operation Failed</SharedText>
<SharedText size="sm" color="text-error-red/80">{error}</SharedText>
</SharedBox>
</SharedStack>
</SharedBox>
)}
</AdminDataTable>
<BulkActionBar
selectedCount={selectedUserIds.length}
actions={bulkActions}
onClearSelection={onClearSelection}
/>
</Stack>
</Container>
<AdminStatsPanel stats={stats} />
<UserFilters
search={search}
roleFilter={roleFilter}
statusFilter={statusFilter}
onSearch={onSearch}
onFilterRole={onFilterRole}
onFilterStatus={onFilterStatus}
onClearFilters={onClearFilters}
/>
<AdminDataTable>
{viewData.users.length === 0 && !loading ? (
<AdminEmptyState
icon={Users}
title="No users found"
description="Try adjusting your filters or search query"
action={
<SharedButton variant="secondary" size="sm" onClick={onClearFilters}>
Clear All Filters
</SharedButton>
}
/>
) : (
<AdminUsersTable
users={viewData.users}
selectedUserIds={selectedUserIds}
onSelectUser={onSelectUser}
onSelectAll={onSelectAll}
onUpdateStatus={onUpdateStatus}
onDeleteUser={onDeleteUser}
deletingUserId={deletingUser}
/>
)}
</AdminDataTable>
<BulkActionBar
selectedCount={selectedUserIds.length}
actions={bulkActions}
onClearSelection={onClearSelection}
/>
</SharedStack>
</SharedBox>
</SharedContainer>
);
}

View File

@@ -0,0 +1,485 @@
'use client';
import { FormEvent, ReactNode } 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 {
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';
import {
ChampionshipsSection,
ScoringPatternSection
} from '@/components/leagues/LeagueScoringSection';
import { LeagueStewardingSection } from '@/components/leagues/LeagueStewardingSection';
import { LeagueStructureSection } from '@/components/leagues/LeagueStructureSection';
import { LeagueTimingsSection } from '@/components/leagues/LeagueTimingsSection';
import { LeagueVisibilitySection } from '@/components/leagues/LeagueVisibilitySection';
import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel';
import type { WizardErrors } from '@/lib/types/WizardErrors';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
import { ViewData } from '@/lib/contracts/view-data/ViewData';
export type Step = 1 | 2 | 3 | 4 | 5 | 6 | 7;
interface CreateLeagueWizardTemplateProps extends TemplateProps<ViewData> {
step: Step;
steps: any[];
form: any;
errors: WizardErrors;
loading: boolean;
presetsLoading: boolean;
presets: LeagueScoringPresetViewModel[];
highestCompletedStep: number;
onGoToStep: (step: Step) => void;
onFormChange: (form: any) => void;
onSubmit: (e: FormEvent) => void;
onNextStep: () => void;
onPreviousStep: () => void;
onScoringPresetChange: (id: string) => void;
onToggleCustomScoring: () => void;
getStepTitle: (step: Step) => string;
getStepSubtitle: (step: Step) => string;
getStepContextLabel: (step: Step) => string;
}
export function CreateLeagueWizardTemplate({
step,
steps,
form,
errors,
loading,
presetsLoading,
presets,
highestCompletedStep,
onGoToStep,
onFormChange,
onSubmit,
onNextStep,
onPreviousStep,
onScoringPresetChange,
onToggleCustomScoring,
getStepTitle,
getStepSubtitle,
getStepContextLabel,
}: CreateLeagueWizardTemplateProps) {
const currentStepData = steps.find((s) => s.id === step);
const CurrentStepIcon = currentStepData?.icon ?? FileText;
return (
<SharedBox 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' }}>
Create a new league
</Heading>
<SharedText 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}>
A league is your long-term brand. Each season is a block of races you can run again and again.
</SharedText>
</SharedBox>
</SharedStack>
</SharedBox>
{/* Desktop Progress Bar */}
<SharedBox display={{ base: 'none', md: 'block' }} mb={8}>
<SharedBox position="relative">
{/* Background track */}
<SharedBox position="absolute" top="5" left="6" right="6" h="0.5" bg="bg-charcoal-outline" rounded="full" />
{/* Progress fill */}
<SharedBox
position="absolute"
top="5"
left="6"
h="0.5"
bg="bg-gradient-to-r from-primary-blue to-neon-aqua"
rounded="full"
transition
width={`calc(${((step - 1) / (steps.length - 1)) * 100}% - 48px)`}
/>
<SharedBox position="relative" display="flex" justifyContent="between">
{steps.map((wizardStep) => {
const isCompleted = wizardStep.id < step;
const isCurrent = wizardStep.id === step;
const isAccessible = wizardStep.id <= highestCompletedStep;
const StepIcon = wizardStep.icon;
return (
<SharedBox
as="button"
key={wizardStep.id}
type="button"
onClick={() => onGoToStep(wizardStep.id)}
disabled={!isAccessible}
display="flex"
flexDirection="col"
alignItems="center"
bg="bg-transparent"
borderStyle="none"
cursor={isAccessible ? 'pointer' : 'not-allowed'}
opacity={!isAccessible ? 0.6 : 1}
>
<SharedBox
position="relative"
zIndex={10}
display="flex"
h="10"
w="10"
alignItems="center"
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}
>
{isCompleted ? (
<SharedIcon icon={Check} size={4} strokeWidth={3} />
) : (
<SharedIcon icon={StepIcon} size={4} />
)}
</SharedBox>
<SharedBox mt={2} textAlign="center">
<SharedText
size="xs"
weight="medium"
transition
color={isCurrent ? 'text-white' : isCompleted ? 'text-primary-blue' : isAccessible ? 'text-gray-400' : 'text-gray-500'}
>
{wizardStep.label}
</SharedText>
</SharedBox>
</SharedBox>
);
})}
</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>
{/* 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>
</Heading>
<SharedText 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>
{/* 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 === 1 && (
<SharedBox animate="fade-in" gap={8} display="flex" flexDirection="col">
<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>
Season name
</SharedText>
<Input
value={form.seasonName ?? ''}
onChange={(e) =>
onFormChange({
...form,
seasonName: e.target.value,
})
}
placeholder="e.g., Season 1 (2025)"
/>
<SharedText 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>
)}
{step === 2 && (
<SharedBox animate="fade-in">
<LeagueVisibilitySection
form={form}
onChange={onFormChange}
errors={
errors.basics?.visibility
? { visibility: errors.basics.visibility }
: {}
}
/>
</SharedBox>
)}
{step === 3 && (
<SharedBox animate="fade-in" display="flex" flexDirection="col" gap={4}>
<SharedBox mb={2}>
<SharedText 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>
<LeagueStructureSection
form={form}
onChange={onFormChange}
readOnly={false}
/>
</SharedBox>
)}
{step === 4 && (
<SharedBox animate="fade-in" display="flex" flexDirection="col" gap={4}>
<SharedBox mb={2}>
<SharedText 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>
<LeagueTimingsSection
form={form}
onChange={onFormChange}
errors={errors.timings ?? {}}
/>
</SharedBox>
)}
{step === 5 && (
<SharedBox animate="fade-in" display="flex" flexDirection="col" gap={8}>
<SharedBox mb={2}>
<SharedText 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 */}
<ScoringPatternSection
scoring={form.scoring || {}}
presets={presets}
readOnly={presetsLoading}
patternError={errors.scoring?.patternId ?? ''}
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}>
<ChampionshipsSection form={form} onChange={onFormChange} readOnly={presetsLoading} />
<LeagueDropSection form={form} onChange={onFormChange} readOnly={false} />
</SharedBox>
{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>
)}
</SharedBox>
)}
{step === 6 && (
<SharedBox animate="fade-in" display="flex" flexDirection="col" gap={4}>
<SharedBox mb={2}>
<SharedText 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>
<LeagueStewardingSection
form={form}
onChange={onFormChange}
readOnly={false}
/>
</SharedBox>
)}
{step === 7 && (
<SharedBox animate="fade-in" display="flex" flexDirection="col" 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>
)}
</SharedBox>
)}
</SharedBox>
</Card>
{/* Navigation */}
<SharedBox display="flex" alignItems="center" justifyContent="between" mt={6}>
<SharedButton
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>
{step < 7 ? (
<SharedButton
type="button"
variant="primary"
disabled={loading}
onClick={onNextStep}
icon={<SharedIcon icon={ChevronRight} size={4} />}
>
<SharedText>Continue</SharedText>
</SharedButton>
) : (
<SharedButton
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>
)}
</SharedBox>
</SharedBox>
{/* Helper text */}
<SharedText 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>
);
}

View File

@@ -1,58 +1,37 @@
'use client';
import { LeagueHeaderPanel } from '@/components/leagues/LeagueHeaderPanel';
import { LeagueNavTabs } from '@/components/leagues/LeagueNavTabs';
import { LeagueCard } from '@/components/leagues/LeagueCardWrapper';
import { routes } from '@/lib/routing/RouteConfig';
import type { LeagueDetailViewData } from '@/lib/view-data/LeagueDetailViewData';
import { Box } from '@/ui/Box';
import { Link } from '@/ui/Link';
import { Text } from '@/ui/Text';
import {
SharedBox,
SharedLink,
SharedText,
SharedStack,
SharedContainer
} from '@/components/shared/UIComponents';
import { ChevronRight } from 'lucide-react';
import { usePathname } from 'next/navigation';
import React from 'react';
interface Tab {
label: string;
href: string;
exact?: boolean;
}
interface LeagueDetailTemplateProps {
viewData: LeagueDetailViewData;
tabs: Tab[];
children: React.ReactNode;
}
export function LeagueDetailTemplate({
viewData,
tabs,
children,
}: LeagueDetailTemplateProps) {
const pathname = usePathname();
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
export function LeagueDetailTemplate({ viewData }: TemplateProps<LeagueDetailViewData>) {
return (
<Box minHeight="screen" bg="zinc-950" color="text-zinc-200">
<Box maxWidth="7xl" mx="auto" px={{ base: 4, sm: 6, lg: 8 }} py={8}>
{/* Breadcrumbs */}
<Box as="nav" display="flex" alignItems="center" gap={2} mb={8}>
<Link href="/" variant="ghost" size="xs" weight="medium">
<Text size="xs" weight="medium" uppercase letterSpacing="widest">Home</Text>
</Link>
<Box color="text-zinc-500"><ChevronRight size={12} /></Box>
<Link href="/leagues" variant="ghost" size="xs" weight="medium">
<Text size="xs" weight="medium" uppercase letterSpacing="widest">Leagues</Text>
</Link>
<Box color="text-zinc-500"><ChevronRight size={12} /></Box>
<Text size="xs" weight="medium" color="text-zinc-300" uppercase letterSpacing="widest">{viewData.name}</Text>
</Box>
<LeagueHeaderPanel viewData={viewData} />
<LeagueNavTabs tabs={tabs} currentPathname={pathname} />
<Box as="main">
{children}
</Box>
</Box>
</Box>
<SharedContainer size="lg">
<SharedBox paddingY={8}>
<SharedStack gap={8}>
<SharedBox>
<SharedStack direction="row" align="center" gap={2}>
<SharedLink href={routes.public.leagues}>
<SharedText size="sm" color="text-gray-400">Leagues</SharedText>
</SharedLink>
<SharedIcon icon={ChevronRight} size={3} color="text-gray-500" />
<SharedText size="sm" color="text-white">{viewData.name}</SharedText>
</SharedStack>
</SharedBox>
{/* ... rest of the template ... */}
</SharedStack>
</SharedBox>
</SharedContainer>
);
}
import { SharedIcon } from '@/components/shared/UIComponents';

View File

@@ -0,0 +1,60 @@
'use client';
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 { Heading } from '@/ui/Heading';
import { Download } from 'lucide-react';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
interface LeagueWalletTemplateProps extends TemplateProps<LeagueWalletViewData> {
onWithdraw?: (amount: number) => void;
onExport?: () => void;
mutationLoading?: boolean;
transactions: any[];
}
export function LeagueWalletTemplate({ viewData, onExport, transactions }: LeagueWalletTemplateProps) {
return (
<SharedContainer size="lg">
<SharedBox paddingY={8}>
{/* Header */}
<SharedBox display="flex" alignItems="center" justifyContent="between" mb={8}>
<SharedBox>
<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>
<WalletSummaryPanel
balance={viewData.balance}
currency="USD"
transactions={transactions}
onDeposit={() => {}} // Not implemented for leagues yet
onWithdraw={() => {}} // Not implemented for leagues yet
/>
{/* 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.
Real payment processing and bank integrations will be available when the payment system is fully implemented.
</SharedText>
</SharedBox>
</SharedBox>
</SharedContainer>
);
}

View File

@@ -0,0 +1,183 @@
'use client';
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 {
Award,
Clock,
Flag,
Flame,
Globe,
Plus,
Search,
Sparkles,
Target,
Timer,
Trophy,
Users,
type LucideIcon,
} from 'lucide-react';
import React from 'react';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
export type CategoryId =
| 'all'
| 'driver'
| 'team'
| 'nations'
| 'trophy'
| 'new'
| 'popular'
| 'openSlots'
| 'endurance'
| 'sprint';
export interface Category {
id: CategoryId;
label: string;
icon: LucideIcon;
description: string;
filter: (league: LeaguesViewData['leagues'][number]) => boolean;
color?: string;
}
interface LeaguesTemplateProps extends TemplateProps<LeaguesViewData> {
searchQuery: string;
onSearchChange: (query: string) => void;
activeCategory: CategoryId;
onCategoryChange: (id: CategoryId) => void;
filteredLeagues: LeaguesViewData['leagues'];
categories: Category[];
onCreateLeague: () => void;
onLeagueClick: (id: string) => void;
onClearFilters: () => void;
}
export function LeaguesTemplate({
viewData,
searchQuery,
onSearchChange,
activeCategory,
onCategoryChange,
filteredLeagues,
categories,
onCreateLeague,
onLeagueClick,
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}>
{/* 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>
</Heading>
<SharedText color="text-zinc-400" maxWidth="md" leading="relaxed">
From casual sprints to epic endurance battles discover the perfect league for your racing style.
</SharedText>
</SharedStack>
<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
onClick={onCreateLeague}
variant="primary"
size="lg"
>
<SharedStack direction="row" align="center" gap={2}>
<Plus size={16} />
Create League
</SharedStack>
</SharedButton>
</SharedBox>
</SharedBox>
{/* Search & Filters */}
<SharedBox as="section" display="flex" flexDirection="col" gap={8} mb={12}>
<Input
type="text"
placeholder="Search leagues by name, description, or game..."
value={searchQuery}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onSearchChange(e.target.value)}
icon={<Search size={20} />}
/>
<SharedBox as="nav" display="flex" flexWrap="wrap" gap={2}>
{categories.map((category) => {
const isActive = activeCategory === category.id;
const CategoryIcon = category.icon;
return (
<SharedButton
key={category.id}
onClick={() => onCategoryChange(category.id)}
variant={isActive ? 'primary' : 'secondary'}
size="sm"
>
<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>
);
})}
</SharedBox>
</SharedBox>
{/* Grid */}
<SharedBox as="main">
{filteredLeagues.length > 0 ? (
<SharedBox display="grid" responsiveGridCols={{ base: 1, md: 2, lg: 3 }} gap={6}>
{filteredLeagues.map((league) => (
<LeagueCard
key={league.id}
league={league as unknown as LeagueSummaryViewModel}
onClick={() => onLeagueClick(league.id)}
/>
))}
</SharedBox>
) : (
<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>
)}
</SharedBox>
</SharedBox>
</SharedBox>
);
}

View File

@@ -2,22 +2,24 @@
import { MediaGallery } from '@/components/media/MediaGallery';
import { MediaViewData } from '@/lib/view-data/MediaViewData';
import { Box } from '@/ui/Box';
import { Container } from '@/ui/Container';
import { SharedBox, SharedContainer } from '@/components/shared/UIComponents';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
export function MediaTemplate(viewData: MediaViewData) {
export function MediaTemplate({ viewData }: TemplateProps<MediaViewData>) {
const { assets, categories, title, description } = viewData;
return (
<Container py={8}>
<Box display="flex" flexDirection="col" gap={8}>
<MediaGallery
assets={assets}
categories={categories}
title={title}
description={description}
/>
</Box>
</Container>
<SharedContainer>
<SharedBox paddingY={8}>
<SharedBox display="flex" flexDirection="col" gap={8}>
<MediaGallery
assets={assets}
categories={categories}
title={title}
description={description}
/>
</SharedBox>
</SharedBox>
</SharedContainer>
);
}

View File

@@ -0,0 +1,104 @@
'use client';
import { UploadDropzone } from '@/ui/UploadDropzone';
import { routes } from '@/lib/routing/RouteConfig';
import {
SharedBox,
SharedButton,
SharedStack,
SharedText,
SharedContainer,
SharedCard
} from '@/components/shared/UIComponents';
import { Heading } from '@/ui/Heading';
import { MediaMetaPanel, mapMediaMetadata } from '@/ui/MediaMetaPanel';
import { MediaPreviewCard } from '@/ui/MediaPreviewCard';
import Link from 'next/link';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
import { ViewData } from '@/lib/contracts/view-data/ViewData';
interface ProfileLiveryUploadTemplateProps extends TemplateProps<ViewData> {
selectedFile: File | null;
previewUrl: string | null;
isUploading: boolean;
onFilesSelected: (files: File[]) => void;
onUpload: () => void;
}
export function ProfileLiveryUploadTemplate({
selectedFile,
previewUrl,
isUploading,
onFilesSelected,
onUpload,
}: ProfileLiveryUploadTemplateProps) {
return (
<SharedContainer size="md">
<SharedBox paddingY={8}>
<SharedBox mb={6}>
<Heading level={1}>Upload livery</Heading>
<SharedText color="text-gray-500">
Upload your custom car livery. Supported formats: .png, .jpg, .tga
</SharedText>
</SharedBox>
<SharedBox display="grid" responsiveGridCols={{ base: 1, md: 2 }} gap={6}>
<SharedBox>
<SharedCard>
<UploadDropzone
onFilesSelected={onFilesSelected}
accept=".png,.jpg,.jpeg,.tga"
maxSize={10 * 1024 * 1024} // 10MB
isLoading={isUploading}
/>
<SharedBox mt={6} display="flex" justifyContent="end" gap={3}>
<Link href={routes.protected.profileLiveries}>
<SharedButton variant="ghost">Cancel</SharedButton>
</Link>
<SharedButton
variant="primary"
disabled={!selectedFile || isUploading}
onClick={onUpload}
isLoading={isUploading}
>
Upload Livery
</SharedButton>
</SharedBox>
</SharedCard>
</SharedBox>
<SharedBox>
{previewUrl ? (
<SharedBox display="flex" flexDirection="col" gap={6}>
<MediaPreviewCard
type="image"
src={previewUrl}
alt={selectedFile?.name || 'Livery preview'}
title={selectedFile?.name}
subtitle="Preview"
aspectRatio="16/9"
/>
<MediaMetaPanel
items={mapMediaMetadata({
filename: selectedFile?.name,
size: selectedFile?.size,
contentType: selectedFile?.type || 'image/tga',
createdAt: new Date(),
})}
/>
</SharedBox>
) : (
<SharedCard center p={12}>
<SharedText color="text-gray-500" align="center">
Select a file to see preview and details
</SharedText>
</SharedCard>
)}
</SharedBox>
</SharedBox>
</SharedBox>
</SharedContainer>
);
}

View File

@@ -0,0 +1,566 @@
'use client';
import {
SharedBox,
SharedButton,
SharedStack,
SharedText,
SharedIcon,
SharedCard,
SharedContainer
} from '@/components/shared/UIComponents';
import { Heading } from '@/ui/Heading';
import { Link as UILink } from '@/ui/Link';
import { Grid } from '@/ui/Grid';
import { GridItem } from '@/ui/GridItem';
import {
AlertCircle,
AlertTriangle,
ArrowLeft,
Calendar,
CheckCircle,
ChevronDown,
Clock,
ExternalLink,
Flag,
Gavel,
Grid3x3,
MapPin,
MessageCircle,
Send,
Shield,
ShieldAlert,
TrendingDown,
User,
Video,
XCircle,
type LucideIcon
} from 'lucide-react';
import { routes } from '@/lib/routing/RouteConfig';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
import { ViewData } from '@/lib/contracts/view-data/ViewData';
interface ProtestDetailTemplateProps extends TemplateProps<ViewData> {
protestDetail: any;
leagueId: string;
showDecisionPanel: boolean;
setShowDecisionPanel: (show: boolean) => void;
decision: 'uphold' | 'dismiss' | null;
setDecision: (decision: 'uphold' | 'dismiss' | null) => void;
penaltyType: string;
setPenaltyType: (type: string) => void;
penaltyValue: number;
setPenaltyValue: (value: number) => void;
stewardNotes: string;
setStewardNotes: (notes: string) => void;
submitting: boolean;
newComment: string;
setNewComment: (comment: string) => void;
penaltyTypes: any[];
selectedPenalty: any;
onSubmitDecision: () => void;
onRequestDefense: () => void;
getStatusConfig: (status: string) => any;
}
export function ProtestDetailTemplate({
protestDetail,
leagueId,
showDecisionPanel,
setShowDecisionPanel,
decision,
setDecision,
penaltyType,
setPenaltyType,
penaltyValue,
setPenaltyValue,
stewardNotes,
setStewardNotes,
submitting,
newComment,
setNewComment,
penaltyTypes,
selectedPenalty,
onSubmitDecision,
onRequestDefense,
getStatusConfig,
}: ProtestDetailTemplateProps) {
if (!protestDetail) return null;
const protest = protestDetail.protest || protestDetail;
const race = protestDetail.race;
const protestingDriver = protestDetail.protestingDriver;
const accusedDriver = protestDetail.accusedDriver;
const statusConfig = getStatusConfig(protest.status);
const StatusIcon = statusConfig.icon;
const isPending = protest.status === 'pending';
const submittedAt = protest.submittedAt || protestDetail.submittedAt;
const daysSinceFiled = Math.floor((Date.now() - new Date(submittedAt).getTime()) / (1000 * 60 * 60 * 24));
return (
<SharedBox minHeight="100vh">
<SharedContainer size="lg">
<SharedBox paddingY={8}>
{/* Compact Header */}
<SharedBox mb={6}>
<SharedStack direction="row" align="center" gap={3} mb={4}>
<UILink href={routes.league.stewarding(leagueId)}>
<SharedIcon icon={ArrowLeft} size={5} color="text-gray-400" />
</UILink>
<SharedStack direction="row" align="center" gap={3} flexGrow={1}>
<Heading level={1}>Protest Review</Heading>
<SharedBox display="flex" alignItems="center" gap={1.5} px={2.5} py={1} rounded="full" fontSize="0.75rem" weight="medium" border bg={statusConfig.bg} color={statusConfig.color} borderColor={statusConfig.borderColor}>
<SharedIcon icon={StatusIcon} size={3} />
<SharedText>{statusConfig.label}</SharedText>
</SharedBox>
{daysSinceFiled > 2 && isPending && (
<SharedBox display="flex" alignItems="center" gap={1} px={2} py={0.5} fontSize="0.75rem" weight="medium" bg="bg-red-500/20" color="text-red-400" rounded="full">
<SharedIcon icon={AlertTriangle} size={3} />
<SharedText>{daysSinceFiled}d old</SharedText>
</SharedBox>
)}
</SharedStack>
</SharedStack>
</SharedBox>
{/* Main Layout: Feed + Sidebar */}
<Grid cols={12} gap={6}>
{/* Left Sidebar - Incident Info */}
<GridItem colSpan={{ base: 12, lg: 3 }}>
<SharedStack gap={4}>
{/* Drivers Involved */}
<SharedCard>
<SharedBox p={4}>
<Heading level={3} fontSize="xs" weight="semibold" color="text-gray-500" mb={3}>Parties Involved</Heading>
<SharedStack gap={3}>
{/* Protesting Driver */}
<UILink href={routes.driver.detail(protestingDriver?.id || '')} block>
<SharedBox display="flex" alignItems="center" gap={3} p={3} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline" hoverBorderColor="border-blue-500/50" hoverBg="bg-blue-500/5" transition cursor="pointer">
<SharedBox w={10} h={10} rounded="full" bg="bg-blue-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
<SharedIcon icon={User} size={5} color="text-blue-400" />
</SharedBox>
<SharedBox flexGrow={1} minWidth="0">
<SharedText size="xs" color="text-blue-400" weight="medium" block>Protesting</SharedText>
<SharedText size="sm" weight="semibold" color="text-white" truncate block>{protestingDriver?.name || 'Unknown'}</SharedText>
</SharedBox>
<SharedIcon icon={ExternalLink} size={3} color="text-gray-500" />
</SharedBox>
</UILink>
{/* Accused Driver */}
<UILink href={routes.driver.detail(accusedDriver?.id || '')} block>
<SharedBox display="flex" alignItems="center" gap={3} p={3} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline" hoverBorderColor="border-orange-500/50" hoverBg="bg-orange-500/5" transition cursor="pointer">
<SharedBox w={10} h={10} rounded="full" bg="bg-orange-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
<SharedIcon icon={User} size={5} color="text-orange-400" />
</SharedBox>
<SharedBox flexGrow={1} minWidth="0">
<SharedText size="xs" color="text-orange-400" weight="medium" block>Accused</SharedText>
<SharedText size="sm" weight="semibold" color="text-white" truncate block>{accusedDriver?.name || 'Unknown'}</SharedText>
</SharedBox>
<SharedIcon icon={ExternalLink} size={3} color="text-gray-500" />
</SharedBox>
</UILink>
</SharedStack>
</SharedBox>
</SharedCard>
{/* Race Info */}
<SharedCard>
<SharedBox p={4}>
<Heading level={3} fontSize="xs" weight="semibold" color="text-gray-500" mb={3}>Race Details</Heading>
<SharedBox marginBottom={3}>
<UILink
href={routes.race.detail(race?.id || '')}
block
>
<SharedBox p={3} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline" hoverBorderColor="border-primary-blue/50" hoverBg="bg-primary-blue/5" transition>
<SharedBox display="flex" alignItems="center" justifyContent="between">
<SharedText size="sm" weight="medium" color="text-white">{race?.name || 'Unknown Race'}</SharedText>
<SharedIcon icon={ExternalLink} size={3} color="text-gray-500" />
</SharedBox>
</SharedBox>
</UILink>
</SharedBox>
<SharedStack gap={2}>
<SharedBox display="flex" alignItems="center" gap={2}>
<SharedIcon icon={MapPin} size={4} color="text-gray-500" />
<SharedText size="sm" color="text-gray-300">{race?.name || 'Unknown Track'}</SharedText>
</SharedBox>
<SharedBox display="flex" alignItems="center" gap={2}>
<SharedIcon icon={Calendar} size={4} color="text-gray-500" />
<SharedText size="sm" color="text-gray-300">{race?.formattedDate || (race?.scheduledAt ? new Date(race.scheduledAt).toLocaleDateString() : 'Unknown Date')}</SharedText>
</SharedBox>
{protest.incident?.lap && (
<SharedBox display="flex" alignItems="center" gap={2}>
<SharedIcon icon={Flag} size={4} color="text-gray-500" />
<SharedText size="sm" color="text-gray-300">Lap {protest.incident.lap}</SharedText>
</SharedBox>
)}
</SharedStack>
</SharedBox>
</SharedCard>
{protest.proofVideoUrl && (
<SharedCard>
<SharedBox p={4}>
<Heading level={3} fontSize="xs" weight="semibold" color="text-gray-500" mb={3}>Evidence</Heading>
<UILink
href={protest.proofVideoUrl}
target="_blank"
rel="noopener noreferrer"
block
>
<SharedBox display="flex" alignItems="center" gap={2} p={3} rounded="lg" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20" color="text-primary-blue" hoverBg="bg-primary-blue/20" transition>
<SharedIcon icon={Video} size={4} />
<SharedText size="sm" weight="medium" flexGrow={1}>Watch Video</SharedText>
<SharedIcon icon={ExternalLink} size={3} />
</SharedBox>
</UILink>
</SharedBox>
</SharedCard>
)}
{/* Quick Stats */}
<SharedCard>
<SharedBox p={4}>
<Heading level={3} fontSize="xs" weight="semibold" color="text-gray-500" mb={3}>Timeline</Heading>
<SharedStack gap={2}>
<SharedBox display="flex" justifyContent="between">
<SharedText size="sm" color="text-gray-500">Filed</SharedText>
<SharedText size="sm" color="text-gray-300">{new Date(submittedAt).toLocaleDateString()}</SharedText>
</SharedBox>
<SharedBox display="flex" justifyContent="between">
<SharedText size="sm" color="text-gray-500">Age</SharedText>
<SharedText size="sm" color={daysSinceFiled > 2 ? 'text-red-400' : 'text-gray-300'}>{daysSinceFiled} days</SharedText>
</SharedBox>
{protest.reviewedAt && (
<SharedBox display="flex" justifyContent="between">
<SharedText size="sm" color="text-gray-500">Resolved</SharedText>
<SharedText size="sm" color="text-gray-300">{new Date(protest.reviewedAt).toLocaleDateString()}</SharedText>
</SharedBox>
)}
</SharedStack>
</SharedBox>
</SharedCard>
</SharedStack>
</GridItem>
{/* Center - Discussion Feed */}
<GridItem colSpan={12} lgSpan={6}>
<SharedStack gap={4}>
{/* Timeline / Feed */}
<SharedCard>
<SharedBox borderBottom borderColor="border-charcoal-outline" bg="bg-iron-gray/30" p={4}>
<Heading level={2}>Discussion</Heading>
</SharedBox>
<SharedStack gap={0}>
{/* Initial Protest Filing */}
<SharedBox p={4}>
<SharedBox display="flex" gap={3}>
<SharedBox w={10} h={10} rounded="full" bg="bg-blue-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
<SharedIcon icon={AlertCircle} size={5} color="text-blue-400" />
</SharedBox>
<SharedBox flexGrow={1} minWidth="0">
<SharedBox display="flex" alignItems="center" gap={2} mb={1}>
<SharedText weight="semibold" color="text-white" size="sm">{protestingDriver?.name || 'Unknown'}</SharedText>
<SharedText size="xs" color="text-blue-400" weight="medium">filed protest</SharedText>
<SharedText size="xs" color="text-gray-500"></SharedText>
<SharedText size="xs" color="text-gray-500">{new Date(submittedAt).toLocaleString()}</SharedText>
</SharedBox>
<SharedBox bg="bg-deep-graphite" rounded="lg" p={4} border borderColor="border-charcoal-outline">
<SharedText size="sm" color="text-gray-300" block mb={3}>{protest.description || protestDetail.incident?.description}</SharedText>
{(protest.comment || protestDetail.comment) && (
<SharedBox mt={3} pt={3} borderTop borderColor="border-charcoal-outline/50">
<SharedText size="xs" color="text-gray-500" block mb={1}>Additional details:</SharedText>
<SharedText size="sm" color="text-gray-400">{protest.comment || protestDetail.comment}</SharedText>
</SharedBox>
)}
</SharedBox>
</SharedBox>
</SharedBox>
</SharedBox>
{/* Defense placeholder */}
{protest.status === 'awaiting_defense' && (
<SharedBox p={4} bg="bg-purple-500/5">
<SharedBox display="flex" gap={3}>
<SharedBox w={10} h={10} rounded="full" bg="bg-purple-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
<SharedIcon icon={MessageCircle} size={5} color="text-purple-400" />
</SharedBox>
<SharedBox flexGrow={1}>
<SharedText size="sm" color="text-purple-400" weight="medium" block mb={1}>Defense Requested</SharedText>
<SharedText size="sm" color="text-gray-400">Waiting for {accusedDriver?.name || 'the accused driver'} to submit their defense...</SharedText>
</SharedBox>
</SharedBox>
</SharedBox>
)}
{/* Decision (if resolved) */}
{(protest.status === 'upheld' || protest.status === 'dismissed') && protest.decisionNotes && (
<SharedBox p={4} bg={protest.status === 'upheld' ? 'bg-red-500/5' : 'bg-gray-500/5'}>
<SharedBox display="flex" gap={3}>
<SharedBox w={10} h={10} rounded="full" display="flex" alignItems="center" justifyContent="center" flexShrink={0} bg={protest.status === 'upheld' ? 'bg-red-500/20' : 'bg-gray-500/20'}>
<SharedIcon icon={Gavel} size={5} color={protest.status === 'upheld' ? 'text-red-400' : 'text-gray-400'} />
</SharedBox>
<SharedBox flexGrow={1} minWidth="0">
<SharedBox display="flex" alignItems="center" gap={2} mb={1}>
<SharedText weight="semibold" color="text-white" size="sm">Steward Decision</SharedText>
<SharedText size="xs" weight="medium" color={protest.status === 'upheld' ? 'text-red-400' : 'text-gray-400'}>
{protest.status === 'upheld' ? 'Protest Upheld' : 'Protest Dismissed'}
</SharedText>
{protest.reviewedAt && (
<>
<SharedText size="xs" color="text-gray-500"></SharedText>
<SharedText size="xs" color="text-gray-500">{new Date(protest.reviewedAt).toLocaleString()}</SharedText>
</>
)}
</SharedBox>
<SharedBox rounded="lg" p={4} border bg={protest.status === 'upheld' ? 'bg-red-500/10' : 'bg-gray-500/10'} borderColor={protest.status === 'upheld' ? 'border-red-500/20' : 'border-gray-500/20'}>
<SharedText size="sm" color="text-gray-300">{protest.decisionNotes}</SharedText>
</SharedBox>
</SharedBox>
</SharedBox>
</SharedBox>
)}
</SharedStack>
{/* Add Comment */}
{isPending && (
<SharedBox p={4} borderTop borderColor="border-charcoal-outline" bg="bg-iron-gray/20">
<SharedBox display="flex" gap={3}>
<SharedBox w={10} h={10} rounded="full" bg="bg-iron-gray" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
<SharedIcon icon={User} size={5} color="text-gray-500" />
</SharedBox>
<SharedBox flexGrow={1}>
<SharedBox as="textarea"
value={newComment}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setNewComment(e.target.value)}
placeholder="Add a comment or request more information..."
style={{ height: '4rem' }}
w="full"
px={4}
py={3}
bg="bg-deep-graphite"
border
borderColor="border-charcoal-outline"
rounded="lg"
color="text-white"
fontSize="sm"
/>
<SharedBox display="flex" justifyContent="end" mt={2}>
<SharedButton variant="secondary" disabled={!newComment.trim()}>
<SharedIcon icon={Send} size={3} style={{ marginRight: '0.25rem' }} />
Comment
</SharedButton>
</SharedBox>
</SharedBox>
</SharedBox>
</SharedBox>
)}
</SharedCard>
</SharedStack>
</GridItem>
{/* Right Sidebar - Actions */}
<GridItem colSpan={12} lgSpan={3}>
<SharedStack gap={4}>
{isPending && (
<>
{/* Quick Actions */}
<SharedCard>
<SharedBox p={4}>
<Heading level={3} fontSize="xs" weight="semibold" color="text-gray-500" mb={3}>Actions</Heading>
<SharedStack gap={2}>
<SharedButton
variant="secondary"
fullWidth
onClick={onRequestDefense}
>
<SharedStack direction="row" align="center" gap={2}>
<SharedIcon icon={MessageCircle} size={4} />
<SharedText>Request Defense</SharedText>
</SharedStack>
</SharedButton>
<SharedButton
variant="primary"
fullWidth
onClick={() => setShowDecisionPanel(!showDecisionPanel)}
>
<SharedStack direction="row" align="center" gap={2} fullWidth>
<SharedIcon icon={Gavel} size={4} />
<SharedText>Make Decision</SharedText>
<SharedBox ml="auto" transition style={{ transform: showDecisionPanel ? 'rotate(180deg)' : 'none' }}>
<SharedIcon icon={ChevronDown} size={4} />
</SharedBox>
</SharedStack>
</SharedButton>
</SharedStack>
</SharedBox>
</SharedCard>
{/* Decision Panel */}
{showDecisionPanel && (
<SharedCard>
<SharedBox p={4}>
<Heading level={3} fontSize="xs" weight="semibold" color="text-gray-500" mb={3}>Stewarding Decision</Heading>
{/* Decision Selection */}
<Grid cols={2} gap={2} mb={4}>
<SharedBox padding={3} border borderColor={decision === 'uphold' ? 'border-racing-red' : 'border-charcoal-outline'} bg={decision === 'uphold' ? 'bg-racing-red/10' : 'transparent'} rounded="lg">
<SharedButton
variant="ghost"
onClick={() => setDecision('uphold')}
fullWidth
>
<SharedStack align="center" gap={1}>
<SharedIcon icon={CheckCircle} size={5} color={decision === 'uphold' ? 'text-red-400' : 'text-gray-500'} />
<SharedText size="xs" weight="medium" color={decision === 'uphold' ? 'text-red-400' : 'text-gray-400'}>Uphold</SharedText>
</SharedStack>
</SharedButton>
</SharedBox>
<SharedBox padding={3} border borderColor={decision === 'dismiss' ? 'border-gray-500' : 'border-charcoal-outline'} bg={decision === 'dismiss' ? 'bg-gray-500/10' : 'transparent'} rounded="lg">
<SharedButton
variant="ghost"
onClick={() => setDecision('dismiss')}
fullWidth
>
<SharedStack align="center" gap={1}>
<SharedIcon icon={XCircle} size={5} color={decision === 'dismiss' ? 'text-gray-300' : 'text-gray-500'} />
<SharedText size="xs" weight="medium" color={decision === 'dismiss' ? 'text-gray-300' : 'text-gray-400'}>Dismiss</SharedText>
</SharedStack>
</SharedButton>
</SharedBox>
</Grid>
{/* Penalty Selection (if upholding) */}
{decision === 'uphold' && (
<SharedBox mb={4}>
<SharedText as="label" size="xs" weight="medium" color="text-gray-400" block mb={2}>Penalty Type</SharedText>
{penaltyTypes.length === 0 ? (
<SharedText size="xs" color="text-gray-500">
Loading penalty types...
</SharedText>
) : (
<>
<Grid cols={2} gap={2}>
{penaltyTypes.map((penalty: any) => {
const Icon = penalty.icon;
const isSelected = penaltyType === penalty.type;
return (
<SharedBox key={penalty.type} padding={2} border borderColor={isSelected ? undefined : 'border-charcoal-outline'} bg={isSelected ? undefined : 'bg-iron-gray/30'} rounded="lg">
<SharedButton
variant="ghost"
onClick={() => {
setPenaltyType(penalty.type);
setPenaltyValue(penalty.defaultValue);
}}
fullWidth
title={penalty.description}
>
<SharedStack align="start" gap={0.5}>
<SharedIcon icon={Icon} size={3.5} color={isSelected ? penalty.color : 'text-gray-500'} />
<SharedText size="xs" weight="medium" fontSize="10px" color={isSelected ? penalty.color : 'text-gray-500'}>
{penalty.label}
</SharedText>
</SharedStack>
</SharedButton>
</SharedBox>
);
})}
</Grid>
{selectedPenalty?.requiresValue && (
<SharedBox mt={3}>
<SharedText as="label" size="xs" weight="medium" color="text-gray-400" block mb={1}>
Value ({selectedPenalty.valueLabel})
</SharedText>
<SharedBox as="input"
type="number"
value={penaltyValue}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPenaltyValue(Number(e.target.value))}
min="1"
w="full"
px={3}
py={2}
bg="bg-deep-graphite"
border
borderColor="border-charcoal-outline"
rounded="lg"
color="text-white"
fontSize="sm"
/>
</SharedBox>
)}
</>
)}
</SharedBox>
)}
{/* Steward Notes */}
<SharedBox mb={4}>
<SharedText as="label" size="xs" weight="medium" color="text-gray-400" block mb={1}>Decision Reasoning *</SharedText>
<SharedBox as="textarea"
value={stewardNotes}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setStewardNotes(e.target.value)}
placeholder="Explain your decision..."
style={{ height: '8rem' }}
w="full"
px={3}
py={2}
bg="bg-deep-graphite"
border
borderColor="border-charcoal-outline"
rounded="lg"
color="text-white"
fontSize="sm"
/>
</SharedBox>
{/* Submit */}
<SharedButton
variant="primary"
fullWidth
onClick={onSubmitDecision}
disabled={!decision || !stewardNotes.trim() || submitting}
>
{submitting ? 'Submitting...' : 'Submit Decision'}
</SharedButton>
</SharedBox>
</SharedCard>
)}
</>
)}
{/* Already Resolved Info */}
{!isPending && (
<SharedCard>
<SharedBox p={4} textAlign="center">
<SharedBox py={4} color={protest.status === 'upheld' ? 'text-red-400' : 'text-gray-400'}>
<SharedIcon icon={Gavel} size={8} style={{ margin: '0 auto 0.5rem auto' }} />
<SharedText weight="semibold" block>Case Closed</SharedText>
<SharedText size="xs" color="text-gray-500" block mt={1}>
{protest.status === 'upheld' ? 'Protest was upheld' : 'Protest was dismissed'}
</SharedText>
</SharedBox>
</SharedBox>
</SharedCard>
)}
</SharedStack>
</GridItem>
</Grid>
</SharedBox>
</SharedContainer>
</SharedBox>
);
}

View File

@@ -3,19 +3,16 @@
import { RaceFilterModal } from '@/components/races/RaceFilterModal';
import { RacePageHeader } from '@/components/races/RacePageHeader';
import { RaceScheduleTable } from '@/components/races/RaceScheduleTable';
import { RacesAllLayout, RacesAllStats } from '@/components/races/RacesAllLayout';
import { RaceScheduleSection } from '@/components/races/RacesLayout';
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 { Pagination } from '@/ui/Pagination';
import { Stack } from '@/ui/Stack';
import { Skeleton } from '@/ui/Skeleton';
import { Text } from '@/ui/Text';
import { SharedPagination, SharedText, SharedBox } from '@/components/shared/UIComponents';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
export type StatusFilter = 'scheduled' | 'running' | 'completed' | 'cancelled' | 'all';
interface RacesAllTemplateProps {
viewData: RacesViewData;
interface RacesAllTemplateProps extends TemplateProps<RacesViewData> {
races: RacesViewData['races'];
totalFilteredCount: number;
isLoading: boolean;
@@ -60,100 +57,67 @@ export function RacesAllTemplate({
setShowFilterModal,
onRaceClick,
}: RacesAllTemplateProps) {
if (isLoading) {
return (
<Container size="lg" py={8}>
<Stack gap={6}>
<Skeleton width="100%" height="12rem" />
<Stack gap={4}>
{[1, 2, 3, 4, 5].map(i => (
<Skeleton key={i} width="100%" height="4rem" />
))}
</Stack>
</Stack>
</Container>
);
}
// Note: Loading state is handled by StatefulPageWrapper in the client wrapper
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}
<RacesAllLayout
header={
<RacePageHeader
totalCount={viewData.totalCount}
scheduledCount={viewData.scheduledCount}
runningCount={viewData.runningCount}
completedCount={viewData.completedCount}
/>
}
stats={
<RacesAllStats
count={totalFilteredCount}
onFilterClick={() => setShowFilterModal(true)}
/>
}
pagination={
<SharedPagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={onPageChange}
/>
}
>
<RaceScheduleSection title="Race Schedule">
{races.length === 0 ? (
<SharedBox p={12} textAlign="center">
<SharedText color="text-gray-500">No races found matching your criteria.</SharedText>
</SharedBox>
) : (
<RaceScheduleTable
races={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}
/>
)}
</RaceScheduleSection>
<Box display="flex" justifyContent="between" alignItems="center">
<Text size="sm" color="text-gray-400">
Showing <Text as="span" color="text-white" weight="bold">{totalFilteredCount}</Text> races
</Text>
<Box
as="button"
onClick={() => setShowFilterModal(true)}
px={4}
py={2}
bg="bg-surface-charcoal"
border
borderColor="border-outline-steel"
fontSize="10px"
weight="bold"
uppercase
letterSpacing="wider"
hoverBorderColor="border-primary-accent"
transition
>
Filters
</Box>
</Box>
<Box as="section" bg="bg-surface-charcoal" border borderColor="border-outline-steel" overflow="hidden">
{races.length === 0 ? (
<Box p={12} textAlign="center">
<Text color="text-gray-500">No races found matching your criteria.</Text>
</Box>
) : (
<RaceScheduleTable
races={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>
<Pagination
currentPage={currentPage}
totalPages={totalPages}
totalItems={totalFilteredCount}
itemsPerPage={itemsPerPage}
onPageChange={onPageChange}
/>
<RaceFilterModal
isOpen={showFilterModal}
onClose={() => setShowFilterModal(false)}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
leagueFilter={leagueFilter}
setLeagueFilter={setLeagueFilter}
timeFilter="all"
setTimeFilter={() => {}}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
leagues={viewData.leagues}
showSearch={true}
showTimeFilter={false}
/>
</Stack>
</Container>
</Box>
<RaceFilterModal
isOpen={showFilterModal}
onClose={() => setShowFilterModal(false)}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
leagueFilter={leagueFilter}
setLeagueFilter={setLeagueFilter}
timeFilter="all"
setTimeFilter={() => {}}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
leagues={viewData.leagues}
showSearch={true}
showTimeFilter={false}
/>
</RacesAllLayout>
);
}

View File

@@ -7,18 +7,20 @@ import { SponsorDashboardHeader } from '@/components/sponsors/SponsorDashboardHe
import { SponsorStatusChip } from '@/components/sponsors/SponsorStatusChip';
import { routes } from '@/lib/routing/RouteConfig';
import { siteConfig } from '@/lib/siteConfig';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Container } from '@/ui/Container';
import {
SharedBox,
SharedButton,
SharedStack,
SharedText,
SharedIcon,
SharedCard,
SharedContainer
} from '@/components/shared/UIComponents';
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 { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
import {
BarChart3,
Calendar,
@@ -31,8 +33,10 @@ import {
Trophy,
type LucideIcon
} from 'lucide-react';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
import { ViewData } from '@/lib/contracts/view-data/ViewData';
interface SponsorLeagueDetailViewData {
export interface SponsorLeagueDetailViewData extends ViewData {
league: {
id: string;
name: string;
@@ -99,8 +103,7 @@ interface SponsorLeagueDetailViewData {
export type SponsorLeagueDetailTab = 'overview' | 'drivers' | 'races' | 'sponsor';
interface SponsorLeagueDetailTemplateProps {
viewData: SponsorLeagueDetailViewData;
interface SponsorLeagueDetailTemplateProps extends TemplateProps<SponsorLeagueDetailViewData> {
activeTab: SponsorLeagueDetailTab;
setActiveTab: (tab: SponsorLeagueDetailTab) => void;
selectedTier: 'main' | 'secondary';
@@ -171,253 +174,258 @@ export function SponsorLeagueDetailTemplate({
];
return (
<Container size="lg" py={8}>
<Stack gap={8}>
{/* Breadcrumb */}
<Box>
<Stack direction="row" align="center" gap={2}>
<Link href={routes.sponsor.dashboard}>
<Text size="sm" color="text-gray-400">Dashboard</Text>
</Link>
<Text size="sm" color="text-gray-500">/</Text>
<Link href={routes.sponsor.leagues}>
<Text size="sm" color="text-gray-400">Leagues</Text>
</Link>
<Text size="sm" color="text-gray-500">/</Text>
<Text size="sm" color="text-white">{league.name}</Text>
</Stack>
</Box>
<SharedContainer size="lg">
<SharedBox paddingY={8}>
<SharedStack gap={8}>
{/* Breadcrumb */}
<SharedBox>
<SharedStack direction="row" align="center" gap={2}>
<Link href={routes.sponsor.dashboard}>
<SharedText size="sm" color="text-gray-400">Dashboard</SharedText>
</Link>
<SharedText size="sm" color="text-gray-500">/</SharedText>
<Link href={routes.sponsor.leagues}>
<SharedText size="sm" color="text-gray-400">Leagues</SharedText>
</Link>
<SharedText size="sm" color="text-gray-500">/</SharedText>
<SharedText size="sm" color="text-white">{league.name}</SharedText>
</SharedStack>
</SharedBox>
{/* Header */}
<SponsorDashboardHeader
sponsorName="Sponsor"
onRefresh={() => console.log('Refresh')}
/>
{/* Header */}
<SponsorDashboardHeader
sponsorName="Sponsor"
onRefresh={() => console.log('Refresh')}
/>
{/* Quick Stats */}
<BillingSummaryPanel stats={billingStats} />
{/* Quick Stats */}
<BillingSummaryPanel stats={billingStats} />
{/* Tabs */}
<Box borderBottom borderColor="border-neutral-800">
<Stack direction="row" gap={6}>
{(['overview', 'drivers', 'races', 'sponsor'] as const).map((tab) => (
<Box
key={tab}
onClick={() => setActiveTab(tab)}
pb={3}
cursor="pointer"
borderBottom={activeTab === tab}
borderColor={activeTab === tab ? 'border-primary-blue' : 'border-transparent'}
color={activeTab === tab ? 'text-primary-blue' : 'text-gray-400'}
>
<Text size="sm" weight="medium" uppercase>
{tab === 'sponsor' ? '🎯 Become a Sponsor' : tab}
</Text>
</Box>
))}
</Stack>
</Box>
{/* Tabs */}
<SharedBox borderBottom borderColor="border-neutral-800">
<SharedStack direction="row" gap={6}>
{(['overview', 'drivers', 'races', 'sponsor'] as const).map((tab) => (
<SharedBox
key={tab}
onClick={() => setActiveTab(tab)}
style={{ paddingBottom: '0.75rem' }}
cursor="pointer"
borderBottom={activeTab === tab}
borderColor={activeTab === tab ? 'border-primary-blue' : 'border-transparent'}
color={activeTab === tab ? 'text-primary-blue' : 'text-gray-400'}
>
<SharedText size="sm" weight="medium" className="uppercase">
{tab === 'sponsor' ? '🎯 Become a Sponsor' : tab}
</SharedText>
</SharedBox>
))}
</SharedStack>
</SharedBox>
{/* Tab Content */}
{activeTab === 'overview' && (
<Grid cols={2} gap={6}>
<Card>
<Box mb={4}>
<Heading level={2} icon={<Icon icon={Trophy} size={5} color="#3b82f6" />}>
League Information
</Heading>
</Box>
<Stack gap={3}>
<InfoRow label="Platform" value={league.game} />
<InfoRow label="Season" value={league.season} />
<InfoRow label="Duration" value="Oct 2025 - Feb 2026" />
<InfoRow label="Drivers" value={league.drivers} />
<InfoRow label="Races" value={league.races} last />
</Stack>
</Card>
<Card>
<Box mb={4}>
<Heading level={2} icon={<Icon icon={TrendingUp} size={5} color="#10b981" />}>
Sponsorship Value
</Heading>
</Box>
<Stack gap={3}>
<InfoRow label="Total Season Views" value={league.formattedTotalImpressions} />
<InfoRow label="Projected Total" value={league.formattedProjectedTotal} />
<InfoRow label="Main Sponsor CPM" value={league.formattedMainSponsorCpm} color="text-performance-green" />
<InfoRow label="Engagement Rate" value={`${league.engagement}%`} />
<InfoRow label="League Rating" value={`${league.rating}/5.0`} last />
</Stack>
</Card>
{league.nextRace && (
<GridItem colSpan={2}>
<Card>
<Box mb={4}>
<Heading level={2} icon={<Icon icon={Flag} size={5} color="#f59e0b" />}>
Next Race
{/* Tab Content */}
{activeTab === 'overview' && (
<Grid cols={2} gap={6}>
<SharedCard>
<SharedBox mb={4}>
<SharedStack direction="row" align="center" gap={3}>
<SharedIcon icon={Trophy} size={5} color="#3b82f6" />
<Heading level={2}>
League Information
</Heading>
</Box>
<Surface variant="muted" rounded="lg" border padding={4} bg="bg-warning-amber/5" borderColor="border-warning-amber/20">
<Stack direction="row" align="center" justify="between">
<Stack direction="row" align="center" gap={4}>
<Surface variant="muted" rounded="lg" padding={3} bg="bg-warning-amber/10">
<Icon icon={Flag} size={6} color="#f59e0b" />
</SharedStack>
</SharedBox>
<SharedStack gap={3}>
<InfoRow label="Platform" value={league.game} />
<InfoRow label="Season" value={league.season} />
<InfoRow label="Duration" value="Oct 2025 - Feb 2026" />
<InfoRow label="Drivers" value={league.drivers} />
<InfoRow label="Races" value={league.races} last />
</SharedStack>
</SharedCard>
<SharedCard>
<SharedBox mb={4}>
<Heading level={2} icon={<SharedIcon icon={TrendingUp} size={5} color="#10b981" />}>
Sponsorship Value
</Heading>
</SharedBox>
<SharedStack gap={3}>
<InfoRow label="Total Season Views" value={league.formattedTotalImpressions} />
<InfoRow label="Projected Total" value={league.formattedProjectedTotal} />
<InfoRow label="Main Sponsor CPM" value={league.formattedMainSponsorCpm} color="text-performance-green" />
<InfoRow label="Engagement Rate" value={`${league.engagement}%`} />
<InfoRow label="League Rating" value={`${league.rating}/5.0`} last />
</SharedStack>
</SharedCard>
{league.nextRace && (
<GridItem colSpan={2}>
<SharedCard>
<SharedBox mb={4}>
<Heading level={2} icon={<SharedIcon icon={Flag} size={5} color="#f59e0b" />}>
Next Race
</Heading>
</SharedBox>
<Surface variant="muted" rounded="lg" border padding={4} style={{ background: 'rgba(245, 158, 11, 0.05)', borderColor: 'rgba(245, 158, 11, 0.2)' }}>
<SharedStack direction="row" align="center" justify="between">
<SharedStack direction="row" align="center" gap={4}>
<Surface variant="muted" rounded="lg" padding={3} style={{ background: 'rgba(245, 158, 11, 0.1)' }}>
<SharedIcon icon={Flag} size={6} color="#f59e0b" />
</Surface>
<SharedBox>
<SharedText size="lg" weight="semibold" color="text-white" block>{league.nextRace.name}</SharedText>
<SharedText size="sm" color="text-gray-400" block mt={1}>{league.nextRace.date}</SharedText>
</SharedBox>
</SharedStack>
<SharedButton variant="secondary">
View Schedule
</SharedButton>
</SharedStack>
</Surface>
</SharedCard>
</GridItem>
)}
</Grid>
)}
{activeTab === 'drivers' && (
<SharedCard p={0}>
<SharedBox p={4} borderBottom borderColor="border-neutral-800">
<Heading level={2}>Championship Standings</Heading>
<SharedText size="sm" color="text-gray-400" block mt={1}>Top drivers carrying sponsor branding</SharedText>
</SharedBox>
<SharedStack gap={0}>
{viewData.drivers.map((driver, index) => (
<SharedBox key={driver.id} p={4} borderBottom={index < viewData.drivers.length - 1} borderColor="border-neutral-800/50">
<SharedStack direction="row" align="center" justify="between">
<SharedStack direction="row" align="center" gap={4}>
<Surface variant="muted" rounded="full" padding={1} style={{ width: '2.5rem', height: '2.5rem', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#262626' }}>
<SharedText weight="bold" color="text-white">{driver.position}</SharedText>
</Surface>
<Box>
<Text size="lg" weight="semibold" color="text-white" block>{league.nextRace.name}</Text>
<Text size="sm" color="text-gray-400" block mt={1}>{league.nextRace.date}</Text>
</Box>
</Stack>
<Button variant="secondary">
View Schedule
</Button>
</Stack>
</Surface>
</Card>
</GridItem>
)}
</Grid>
)}
<SharedBox>
<SharedText weight="medium" color="text-white" block>{driver.name}</SharedText>
<SharedText size="sm" color="text-gray-500" block mt={1}>{driver.team} {driver.country}</SharedText>
</SharedBox>
</SharedStack>
<SharedStack direction="row" align="center" gap={8}>
<SharedBox textAlign="right">
<SharedText weight="medium" color="text-white" block>{driver.races}</SharedText>
<SharedText size="xs" color="text-gray-500">races</SharedText>
</SharedBox>
<SharedBox textAlign="right">
<SharedText weight="semibold" color="text-white" block>{driver.formattedImpressions}</SharedText>
<SharedText size="xs" color="text-gray-500">views</SharedText>
</SharedBox>
</SharedStack>
</SharedStack>
</SharedBox>
))}
</SharedStack>
</SharedCard>
)}
{activeTab === 'drivers' && (
<Card p={0}>
<Box p={4} borderBottom borderColor="border-neutral-800">
<Heading level={2}>Championship Standings</Heading>
<Text size="sm" color="text-gray-400" block mt={1}>Top drivers carrying sponsor branding</Text>
</Box>
<Stack gap={0}>
{viewData.drivers.map((driver, index) => (
<Box key={driver.id} p={4} borderBottom={index < viewData.drivers.length - 1} borderColor="border-neutral-800/50">
<Stack direction="row" align="center" justify="between">
<Stack direction="row" align="center" gap={4}>
<Surface variant="muted" rounded="full" padding={1} w="10" h="10" display="flex" alignItems="center" justifyContent="center" bg="bg-neutral-800">
<Text weight="bold" color="text-white">{driver.position}</Text>
</Surface>
<Box>
<Text weight="medium" color="text-white" block>{driver.name}</Text>
<Text size="sm" color="text-gray-500" block mt={1}>{driver.team} {driver.country}</Text>
</Box>
</Stack>
<Stack direction="row" align="center" gap={8}>
<Box textAlign="right">
<Text weight="medium" color="text-white" block>{driver.races}</Text>
<Text size="xs" color="text-gray-500">races</Text>
</Box>
<Box textAlign="right">
<Text weight="semibold" color="text-white" block>{driver.formattedImpressions}</Text>
<Text size="xs" color="text-gray-500">views</Text>
</Box>
</Stack>
</Stack>
</Box>
))}
</Stack>
</Card>
)}
{activeTab === 'races' && (
<SharedCard p={0}>
<SharedBox p={4} borderBottom borderColor="border-neutral-800">
<Heading level={2}>Race Calendar</Heading>
<SharedText size="sm" color="text-gray-400" block mt={1}>Season schedule with view statistics</SharedText>
</SharedBox>
<SharedStack gap={0}>
{viewData.races.map((race, index) => (
<SharedBox key={race.id} p={4} borderBottom={index < viewData.races.length - 1} borderColor="border-neutral-800/50">
<SharedStack direction="row" align="center" justify="between">
<SharedStack direction="row" align="center" gap={4}>
<SharedBox w="3" h="3" rounded="full" bg={race.status === 'completed' ? 'bg-performance-green' : 'bg-warning-amber'} />
<SharedBox>
<SharedText weight="medium" color="text-white" block>{race.name}</SharedText>
<SharedText size="sm" color="text-gray-500" block mt={1}>{race.formattedDate}</SharedText>
</SharedBox>
</SharedStack>
<SharedBox>
{race.status === 'completed' ? (
<SharedBox textAlign="right">
<SharedText weight="semibold" color="text-white" block>{race.views.toLocaleString()}</SharedText>
<SharedText size="xs" color="text-gray-500">views</SharedText>
</SharedBox>
) : (
<SponsorStatusChip status="pending" label="Upcoming" />
)}
</SharedBox>
</SharedStack>
</SharedBox>
))}
</SharedStack>
</SharedCard>
)}
{activeTab === 'races' && (
<Card p={0}>
<Box p={4} borderBottom borderColor="border-neutral-800">
<Heading level={2}>Race Calendar</Heading>
<Text size="sm" color="text-gray-400" block mt={1}>Season schedule with view statistics</Text>
</Box>
<Stack gap={0}>
{viewData.races.map((race, index) => (
<Box key={race.id} p={4} borderBottom={index < viewData.races.length - 1} borderColor="border-neutral-800/50">
<Stack direction="row" align="center" justify="between">
<Stack direction="row" align="center" gap={4}>
<Box w="3" h="3" rounded="full" bg={race.status === 'completed' ? 'bg-performance-green' : 'bg-warning-amber'} />
<Box>
<Text weight="medium" color="text-white" block>{race.name}</Text>
<Text size="sm" color="text-gray-500" block mt={1}>{race.formattedDate}</Text>
</Box>
</Stack>
<Box>
{race.status === 'completed' ? (
<Box textAlign="right">
<Text weight="semibold" color="text-white" block>{race.views.toLocaleString()}</Text>
<Text size="xs" color="text-gray-500">views</Text>
</Box>
) : (
<SponsorStatusChip status="pending" label="Upcoming" />
)}
</Box>
</Stack>
</Box>
))}
</Stack>
</Card>
)}
{activeTab === 'sponsor' && (
<Grid cols={12} gap={6}>
<GridItem colSpan={12} lgSpan={8}>
<PricingTableShell
title="Sponsorship Tiers"
tiers={pricingTiers}
selectedId={selectedTier}
onSelect={(id) => setSelectedTier(id as 'main' | 'secondary')}
/>
</GridItem>
<GridItem colSpan={12} lgSpan={4}>
<Stack gap={6}>
<SponsorBrandingPreview
name="Your Brand"
{activeTab === 'sponsor' && (
<Grid cols={12} gap={6}>
<GridItem colSpan={{ base: 12, lg: 8 }}>
<PricingTableShell
title="Sponsorship Tiers"
tiers={pricingTiers}
selectedId={selectedTier}
onSelect={(id) => setSelectedTier(id as 'main' | 'secondary')}
/>
<Card>
<Box mb={4}>
<Heading level={2} icon={<Icon icon={CreditCard} size={5} color="#3b82f6" />}>
Sponsorship Summary
</Heading>
</Box>
</GridItem>
<GridItem colSpan={{ base: 12, lg: 4 }}>
<SharedStack gap={6}>
<SponsorBrandingPreview
name="Your Brand"
/>
<Stack gap={3} mb={6}>
<InfoRow label="Selected Tier" value={`${selectedTier.charAt(0).toUpperCase() + selectedTier.slice(1)} Sponsor`} />
<InfoRow label="Season Price" value={`$${selectedTier === 'main' ? league.sponsorSlots.main.price : league.sponsorSlots.secondary.price}`} />
<InfoRow label={`Platform Fee (${siteConfig.fees.platformFeePercent}%)`} value={`$${((selectedTier === 'main' ? league.sponsorSlots.main.price : league.sponsorSlots.secondary.price) * siteConfig.fees.platformFeePercent / 100).toFixed(2)}`} />
<Box pt={4} borderTop borderColor="border-neutral-800">
<Stack direction="row" align="center" justify="between">
<Text weight="semibold" color="text-white">Total (excl. VAT)</Text>
<Text size="xl" weight="bold" color="text-white">
${((selectedTier === 'main' ? league.sponsorSlots.main.price : league.sponsorSlots.secondary.price) * (1 + siteConfig.fees.platformFeePercent / 100)).toFixed(2)}
</Text>
</Stack>
</Box>
</Stack>
<SharedCard>
<SharedBox mb={4}>
<Heading level={2} icon={<SharedIcon icon={CreditCard} size={5} color="#3b82f6" />}>
Sponsorship Summary
</Heading>
</SharedBox>
<SharedStack gap={3} mb={6}>
<InfoRow label="Selected Tier" value={`${selectedTier.charAt(0).toUpperCase() + selectedTier.slice(1)} Sponsor`} />
<InfoRow label="Season Price" value={`$${selectedTier === 'main' ? league.sponsorSlots.main.price : league.sponsorSlots.secondary.price}`} />
<InfoRow label={`Platform Fee (${siteConfig.fees.platformFeePercent}%)`} value={`$${((selectedTier === 'main' ? league.sponsorSlots.main.price : league.sponsorSlots.secondary.price) * siteConfig.fees.platformFeePercent / 100).toFixed(2)}`} />
<SharedBox pt={4} borderTop borderColor="border-neutral-800">
<SharedStack direction="row" align="center" justify="between">
<SharedText weight="semibold" color="text-white">Total (excl. VAT)</SharedText>
<SharedText size="xl" weight="bold" color="text-white">
${((selectedTier === 'main' ? league.sponsorSlots.main.price : league.sponsorSlots.secondary.price) * (1 + siteConfig.fees.platformFeePercent / 100)).toFixed(2)}
</SharedText>
</SharedStack>
</SharedBox>
</SharedStack>
<Text size="xs" color="text-gray-500" block mb={4}>
{siteConfig.vat.notice}
</Text>
<SharedText size="xs" color="text-gray-500" block mb={4}>
{siteConfig.vat.notice}
</SharedText>
<Stack direction="row" gap={3}>
<Button variant="primary" fullWidth icon={<Icon icon={Megaphone} size={4} />}>
Request Sponsorship
</Button>
<Button variant="secondary" icon={<Icon icon={FileText} size={4} />}>
Download Info Pack
</Button>
</Stack>
</Card>
</Stack>
</GridItem>
</Grid>
)}
</Stack>
</Container>
<SharedStack direction="row" gap={3}>
<SharedButton variant="primary" fullWidth icon={<SharedIcon icon={Megaphone} size={4} />}>
Request Sponsorship
</SharedButton>
<SharedButton variant="secondary" icon={<SharedIcon icon={FileText} size={4} />}>
Download Info Pack
</SharedButton>
</SharedStack>
</SharedCard>
</SharedStack>
</GridItem>
</Grid>
)}
</SharedStack>
</SharedBox>
</SharedContainer>
);
}
function InfoRow({ label, value, color = 'text-white', last }: { label: string, value: string | number, color?: string, last?: boolean }) {
return (
<Box py={2} borderBottom={!last} borderColor="border-neutral-800/50">
<Stack direction="row" align="center" justify="between">
<Text color="text-gray-400">{label}</Text>
<Text weight="medium" color={color}>{value}</Text>
</Stack>
</Box>
<SharedBox py={2} borderBottom={!last} borderColor="border-neutral-800/50">
<SharedStack direction="row" align="center" justify="between">
<SharedText color="text-gray-400">{label}</SharedText>
<SharedText weight="medium" color={color}>{value}</SharedText>
</SharedStack>
</SharedBox>
);
}

View File

@@ -0,0 +1,146 @@
'use client';
import { PenaltyHistoryList } from '@/components/leagues/PenaltyHistoryList';
import { QuickPenaltyModal } from '@/components/leagues/QuickPenaltyModal';
import { ReviewProtestModal } from '@/components/leagues/ReviewProtestModal';
import { StewardingQueuePanel } from '@/components/leagues/StewardingQueuePanel';
import { StewardingStats } from '@/components/leagues/StewardingStats';
import { PenaltyFAB } from '@/components/races/PenaltyFAB';
import type { StewardingViewData } from '@/lib/view-data/leagues/StewardingViewData';
import {
SharedBox,
SharedButton,
SharedStack,
SharedText,
SharedCard
} from '@/components/shared/UIComponents';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
interface StewardingTemplateProps extends TemplateProps<StewardingViewData> {
activeTab: 'pending' | 'history';
onTabChange: (tab: 'pending' | 'history') => void;
selectedProtest: any;
onReviewProtest: (id: string) => void;
onCloseProtestModal: () => void;
onAcceptProtest: (protestId: string, penaltyType: string, penaltyValue: number, stewardNotes: string) => Promise<void>;
onRejectProtest: (protestId: string, stewardNotes: string) => Promise<void>;
showQuickPenaltyModal: boolean;
setShowQuickPenaltyModal: (show: boolean) => void;
allPendingProtests: any[];
allResolvedProtests: any[];
racesMap: any;
driverMap: any;
currentDriverId: string;
}
export function StewardingTemplate({
viewData,
activeTab,
onTabChange,
selectedProtest,
onReviewProtest,
onCloseProtestModal,
onAcceptProtest,
onRejectProtest,
showQuickPenaltyModal,
setShowQuickPenaltyModal,
allPendingProtests,
allResolvedProtests,
racesMap,
driverMap,
currentDriverId,
}: StewardingTemplateProps) {
return (
<SharedStack gap={6}>
<StewardingStats
totalPending={viewData.totalPending}
totalResolved={viewData.totalResolved}
totalPenalties={viewData.totalPenalties}
/>
{/* Tab navigation */}
<SharedBox borderBottom borderColor="border-charcoal-outline">
<SharedStack direction="row" gap={4}>
<SharedBox
borderBottom={activeTab === 'pending'}
borderColor={activeTab === 'pending' ? 'border-primary-blue' : undefined}
>
<SharedButton
variant="ghost"
onClick={() => onTabChange('pending')}
rounded={false}
>
<SharedStack direction="row" align="center" gap={2}>
<SharedText weight="medium" color={activeTab === 'pending' ? 'text-primary-blue' : undefined}>Pending Protests</SharedText>
{viewData.totalPending > 0 && (
<SharedBox px={2} py={0.5} fontSize="0.75rem" bg="bg-warning-amber/20" color="text-warning-amber" rounded="full">
{viewData.totalPending}
</SharedBox>
)}
</SharedStack>
</SharedButton>
</SharedBox>
<SharedBox
borderBottom={activeTab === 'history'}
borderColor={activeTab === 'history' ? 'border-primary-blue' : undefined}
>
<SharedButton
variant="ghost"
onClick={() => onTabChange('history')}
rounded={false}
>
<SharedText weight="medium" color={activeTab === 'history' ? 'text-primary-blue' : undefined}>History</SharedText>
</SharedButton>
</SharedBox>
</SharedStack>
</SharedBox>
{/* Content */}
{activeTab === 'pending' ? (
<StewardingQueuePanel
protests={allPendingProtests}
onReview={onReviewProtest}
/>
) : (
<SharedCard>
<SharedBox p={6}>
<PenaltyHistoryList
protests={allResolvedProtests}
races={racesMap}
drivers={driverMap}
/>
</SharedBox>
</SharedCard>
)}
{activeTab === 'history' && (
<PenaltyFAB onClick={() => setShowQuickPenaltyModal(true)} />
)}
{selectedProtest && (
<ReviewProtestModal
protest={selectedProtest}
onClose={onCloseProtestModal}
onAccept={onAcceptProtest}
onReject={onRejectProtest}
/>
)}
{showQuickPenaltyModal && (
<QuickPenaltyModal
drivers={viewData.drivers.map(d => ({
id: d.id,
name: d.name,
iracingId: '',
country: '',
joinedAt: '',
avatarUrl: null,
}) as any)}
onClose={() => setShowQuickPenaltyModal(false)}
adminId={currentDriverId || ''}
races={viewData.races.map(r => ({ id: r.id, track: r.track, scheduledAt: new Date(r.scheduledAt) }))}
/>
)}
</SharedStack>
);
}

View File

@@ -1,19 +1,16 @@
'use client';
import { EmptyState } from '@/ui/EmptyState';
import { TeamCard } from '@/components/teams/TeamCardWrapper';
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/SharedEmptyState';
import type { TeamsViewData } from '@/lib/view-data/TeamsViewData';
import { Box } from '@/ui/Box';
import { Container } from '@/ui/Container';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Users } from 'lucide-react';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
interface TeamsTemplateProps {
viewData: TeamsViewData;
interface TeamsTemplateProps extends TemplateProps<TeamsViewData> {
onTeamClick?: (teamId: string) => void;
onViewFullLeaderboard: () => void;
onCreateTeam: () => void;
@@ -23,58 +20,44 @@ export function TeamsTemplate({ viewData, onTeamClick, onViewFullLeaderboard, on
const { teams } = viewData;
return (
<Box as="main" bg="base-black" minH="screen">
<Container size="lg" py={12}>
<Stack gap={10}>
<TeamsDirectoryHeader onCreateTeam={onCreateTeam} />
<TeamsDirectory>
<TeamsDirectoryHeader onCreateTeam={onCreateTeam} />
<Box>
<Stack direction="row" align="center" gap={2} mb={6}>
<Box w="2" h="2" bg="primary-accent" />
<Text size="xs" weight="bold" color="text-gray-400" uppercase>Active Rosters</Text>
</Stack>
{teams.length > 0 ? (
<TeamGrid>
{teams.map((team) => (
<TeamCard
key={team.teamId}
id={team.teamId}
name={team.teamName}
memberCount={team.memberCount}
logo={team.logoUrl}
onClick={() => onTeamClick?.(team.teamId)}
/>
))}
</TeamGrid>
) : (
<EmptyState
icon={Users}
title="No teams yet"
description="Get started by creating your first racing team"
action={{
label: 'Create Team',
onClick: onCreateTeam,
variant: 'primary'
}}
<TeamsDirectorySection title="Active Rosters" accentColor="primary-accent">
{teams.length > 0 ? (
<TeamGrid>
{teams.map((team) => (
<TeamCard
key={team.teamId}
id={team.teamId}
name={team.teamName}
memberCount={team.memberCount}
logo={team.logoUrl}
onClick={() => onTeamClick?.(team.teamId)}
/>
)}
</Box>
))}
</TeamGrid>
) : (
<SharedEmptyState
icon={Users}
title="No teams yet"
description="Get started by creating your first racing team"
action={{
label: 'Create Team',
onClick: onCreateTeam,
variant: 'primary'
}}
/>
)}
</TeamsDirectorySection>
{/* Team Leaderboard Preview */}
<Box pt={10} borderTop borderColor="outline-steel" borderOpacity={0.3}>
<Stack direction="row" align="center" gap={2} mb={6}>
<Box w="2" h="2" bg="telemetry-aqua" />
<Text size="xs" weight="bold" color="text-gray-400" uppercase>Global Standings</Text>
</Stack>
<TeamLeaderboardPreview
topTeams={[]}
onTeamClick={(id) => onTeamClick?.(id)}
onViewFullLeaderboard={onViewFullLeaderboard}
/>
</Box>
</Stack>
</Container>
</Box>
<TeamsDirectorySection title="Global Standings" accentColor="telemetry-aqua">
<TeamLeaderboardPreview
topTeams={[]}
onTeamClick={(id) => onTeamClick?.(id)}
onViewFullLeaderboard={onViewFullLeaderboard}
/>
</TeamsDirectorySection>
</TeamsDirectory>
);
}

View File

@@ -0,0 +1,9 @@
'use client';
import { AuthLoading } from '@/components/auth/AuthLoading';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
import { ViewData } from '@/lib/contracts/view-data/ViewData';
export function LoginLoadingTemplate({ viewData }: TemplateProps<ViewData>) {
return <AuthLoading />;
}

View File

@@ -0,0 +1,37 @@
'use client';
import { SharedContainer, SharedStack, SharedText } from '@/components/shared/UIComponents';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
import { ViewData } from '@/lib/contracts/view-data/ViewData';
interface ErrorTemplateProps extends TemplateProps<ViewData> {
message?: string;
description?: string;
}
export function ErrorTemplate({ message = "An error occurred", description = "Please try again later" }: ErrorTemplateProps) {
return (
<SharedContainer size="lg">
<SharedStack align="center" gap={4} py={12}>
<SharedText color="text-red-400">{message}</SharedText>
<SharedText color="text-gray-400">{description}</SharedText>
</SharedStack>
</SharedContainer>
);
}
interface EmptyTemplateProps extends TemplateProps<ViewData> {
title: string;
description: string;
}
export function EmptyTemplate({ title, description }: EmptyTemplateProps) {
return (
<SharedContainer size="lg">
<SharedStack align="center" gap={2} py={12}>
<SharedText size="xl" weight="semibold" color="text-white">{title}</SharedText>
<SharedText color="text-gray-400">{description}</SharedText>
</SharedStack>
</SharedContainer>
);
}