website refactor

This commit is contained in:
2026-01-19 18:34:01 +01:00
parent 61b5cf3b64
commit 41e21e6595
24 changed files with 643 additions and 717 deletions

View File

@@ -6,7 +6,7 @@ import { AdminUsersTemplate } from '@/templates/AdminUsersTemplate';
import { AdminUsersViewData } from '@/lib/view-data/AdminUsersViewData';
import { updateUserStatus, deleteUser } from '@/app/actions/adminActions';
import { routes } from '@/lib/routing/RouteConfig';
import { SharedConfirmDialog } from '@/components/shared/UIComponents';
import { ConfirmDialog } from '@/components/shared/ConfirmDialog';
import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts';
export function AdminUsersWrapper({ viewData }: ClientWrapperProps<AdminUsersViewData>) {
@@ -148,7 +148,7 @@ export function AdminUsersWrapper({ viewData }: ClientWrapperProps<AdminUsersVie
onSelectAll={handleSelectAll}
onClearSelection={handleClearSelection}
/>
<SharedConfirmDialog
<ConfirmDialog
isOpen={!!userToDelete}
onClose={() => setUserToDelete(null)}
onConfirm={confirmDeleteUser}

View File

@@ -8,7 +8,12 @@ import {
updateRaceAction
} from '@/app/actions/leagueScheduleActions';
import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper';
import { SharedConfirmDialog, SharedStack, SharedCard, SharedBox, SharedText, SharedHeading } from '@/components/shared/UIComponents';
import { ConfirmDialog } from '@/components/shared/ConfirmDialog';
import { Stack } from '@/ui/Stack';
import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import {
useLeagueAdminSchedule,
useLeagueAdminStatus,
@@ -172,16 +177,16 @@ export function LeagueAdminSchedulePageClient() {
// Render admin access required if not admin
if (!isLoading && !isAdmin) {
return (
<SharedStack gap={6}>
<SharedCard>
<SharedBox p={6} textAlign="center">
<SharedHeading level={3}>Admin Access Required</SharedHeading>
<SharedBox mt={2}>
<SharedText size="sm" color="text-gray-400">Only league admins can manage the schedule.</SharedText>
</SharedBox>
</SharedBox>
</SharedCard>
</SharedStack>
<Stack gap={6}>
<Card>
<Box p={6} textAlign="center">
<Heading level={3}>Admin Access Required</Heading>
<Box mt={2}>
<Text size="sm" color="text-gray-400">Only league admins can manage the schedule.</Text>
</Box>
</Box>
</Card>
</Stack>
);
}
@@ -221,7 +226,7 @@ export function LeagueAdminSchedulePageClient() {
setForm(new RaceScheduleCommandModel(form.toCommand()));
}}
/>
<SharedConfirmDialog
<ConfirmDialog
isOpen={!!raceToDelete}
onClose={() => setRaceToDelete(null)}
onConfirm={confirmDelete}

View File

@@ -3,13 +3,11 @@
import type { Result } from '@/lib/contracts/Result';
import type { ProfileViewData } from '@/lib/view-data/ProfileViewData';
import { ProfileSettingsTemplate } from '@/templates/ProfileSettingsTemplate';
import {
SharedBox,
SharedStack,
SharedText,
SharedIcon,
SharedProgressLine
} from '@/components/shared/UIComponents';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
import { ProgressLine } from '@/components/shared/ProgressLine';
import { ShieldAlert } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -46,19 +44,19 @@ export function ProfileSettingsPageClient({ viewData, onSave }: ProfileSettingsP
return (
<>
<SharedProgressLine isLoading={isSaving} />
<ProgressLine isLoading={isSaving} />
{error && (
<SharedBox position="fixed" top={4} right={4} zIndex={50} maxWidth="md">
<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">Update Failed</SharedText>
<SharedText size="sm" color="text-error-red/80">{error}</SharedText>
</SharedBox>
</SharedStack>
</SharedBox>
</SharedBox>
<Box position="fixed" top={4} right={4} zIndex={50} maxWidth="md">
<Box bg="var(--ui-color-bg-surface)" p={4} rounded="md" border borderColor="var(--ui-color-intent-critical)">
<Stack direction="row" align="center" gap={3}>
<Icon icon={ShieldAlert} size={5} color="var(--ui-color-intent-critical)" />
<Box>
<Text weight="bold" variant="critical">Update Failed</Text>
<Text size="sm" variant="low">{error}</Text>
</Box>
</Stack>
</Box>
</Box>
)}
<ProfileSettingsTemplate
viewData={viewData}

View File

@@ -1,12 +1,10 @@
'use client';
import {
SharedBox,
SharedStack,
SharedText,
SharedIcon,
SharedProgressLine
} from '@/components/shared/UIComponents';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
import { ProgressLine } from '@/components/shared/ProgressLine';
import { ShieldAlert } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -53,19 +51,19 @@ export function SponsorshipRequestsClient({ viewData, onAccept, onReject }: Spon
return (
<>
<SharedProgressLine isLoading={!!isProcessing} />
<ProgressLine isLoading={!!isProcessing} />
{error && (
<SharedBox position="fixed" top={4} right={4} zIndex={50} maxWidth="md">
<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">Action Failed</SharedText>
<SharedText size="sm" color="text-error-red/80">{error}</SharedText>
</SharedBox>
</SharedStack>
</SharedBox>
</SharedBox>
<Box position="fixed" top={4} right={4} zIndex={50} maxWidth="md">
<Box bg="bg-error-red/10" p={4} rounded="md" border borderColor="border-error-red/20">
<Stack direction="row" align="center" gap={3}>
<Icon icon={ShieldAlert} size={5} color="text-error-red" />
<Box>
<Text weight="bold" color="text-error-red">Action Failed</Text>
<Text size="sm" color="text-error-red/80">{error}</Text>
</Box>
</Stack>
</Box>
</Box>
)}
<SponsorshipRequestsTemplate
viewData={viewData}

View File

@@ -24,7 +24,7 @@ export function AppFooter({ children }: AppFooterProps) {
variant="muted"
paddingY={8}
marginTop="auto"
style={{ borderTop: '1px solid var(--ui-color-border-default)' }}
borderTop={true}
>
<Container size="xl">
{children ? (

View File

@@ -1,7 +1,9 @@
'use client';
import { ReactNode } from 'react';
import { SharedBox, SharedStack, SharedContainer } from '@/components/shared/UIComponents';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Container } from '@/ui/Container';
interface CreateLeagueWizardLayoutProps {
children: ReactNode;
@@ -13,12 +15,12 @@ interface CreateLeagueWizardLayoutProps {
export function CreateLeagueWizardLayout({ children, header, progress, navigation, footer }: CreateLeagueWizardLayoutProps) {
return (
<SharedBox as="main" maxWidth="4xl" mx="auto" pb={8}>
<Box as="main" maxWidth="4xl" mx="auto" pb={8}>
{header}
{progress}
{children}
{navigation}
{footer}
</SharedBox>
</Box>
);
}

View File

@@ -1,26 +0,0 @@
'use client';
import { LucideIcon } from 'lucide-react';
import { EmptyState } from '@/ui/EmptyState';
interface SharedEmptyStateProps {
icon: LucideIcon;
title: string;
description?: string;
action?: {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'race-final' | 'discord';
};
}
export function SharedEmptyState({ icon, title, description, action }: SharedEmptyStateProps) {
return (
<EmptyState
icon={icon}
title={title}
description={description}
action={action}
/>
);
}

View File

@@ -1,44 +0,0 @@
import { Pagination } from '@/ui/Pagination';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Container } from '@/ui/Container';
import { ConfirmDialog } from '@/components/shared/ConfirmDialog';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { Grid } from '@/ui/Grid';
import { GridItem } from '@/ui/GridItem';
import { Surface } from '@/ui/Surface';
import { Input } from '@/ui/Input';
import { Link } from '@/ui/Link';
import { Skeleton } from '@/ui/Skeleton';
import { LoadingSpinner } from '@/ui/LoadingSpinner';
import { Badge } from '@/ui/Badge';
import { ProgressLine } from '@/components/shared/ProgressLine';
import { SharedEmptyState } from './SharedEmptyState';
export {
Pagination as SharedPagination,
Text as SharedText,
Box as SharedBox,
Stack as SharedStack,
Container as SharedContainer,
ConfirmDialog as SharedConfirmDialog,
Button as SharedButton,
Icon as SharedIcon,
Card as SharedCard,
Heading as SharedHeading,
Grid as SharedGrid,
GridItem as SharedGridItem,
Surface as SharedSurface,
Input as SharedInput,
Link as SharedLink,
Skeleton as SharedSkeleton,
LoadingSpinner as SharedLoadingSpinner,
Badge as SharedBadge,
ProgressLine as SharedProgressLine,
SharedEmptyState
};

View File

@@ -8,7 +8,13 @@ import { AdminUsersTable } from '@/components/admin/AdminUsersTable';
import { BulkActionBar } from '@/components/admin/BulkActionBar';
import { UserFilters } from '@/components/admin/UserFilters';
import { AdminUsersViewData } from '@/lib/view-data/AdminUsersViewData';
import { SharedButton, SharedContainer, SharedIcon, SharedStack, SharedBox, SharedText } from '@/components/shared/UIComponents';
import { Button } from '@/ui/Button';
import { Container } from '@/ui/Container';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { ErrorBanner } from '@/ui/ErrorBanner';
import { RefreshCw, ShieldAlert, Users } from 'lucide-react';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
@@ -95,37 +101,29 @@ export function AdminUsersTemplate({
];
return (
<SharedContainer size="lg">
<SharedBox paddingY={8}>
<SharedStack gap={8}>
<Container size="lg">
<Box paddingY={8}>
<Stack gap={8}>
<AdminHeaderPanel
title="User Management"
description="Monitor and control system access"
isLoading={loading}
actions={
<SharedButton
<Button
onClick={onRefresh}
disabled={loading}
variant="secondary"
size="sm"
icon={<SharedIcon icon={RefreshCw} size={3} animate={loading ? 'spin' : 'none'} />}
icon={<Icon icon={RefreshCw} size={3} animate={loading ? 'spin' : 'none'} />}
>
Refresh Data
</SharedButton>
</Button>
}
/>
{/* 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>
<ErrorBanner title="Operation Failed" message={error} />
)}
<AdminStatsPanel stats={stats} />
@@ -147,9 +145,9 @@ export function AdminUsersTemplate({
title="No users found"
description="Try adjusting your filters or search query"
action={
<SharedButton variant="secondary" size="sm" onClick={onClearFilters}>
<Button variant="secondary" size="sm" onClick={onClearFilters}>
Clear All Filters
</SharedButton>
</Button>
}
/>
) : (
@@ -170,8 +168,8 @@ export function AdminUsersTemplate({
actions={bulkActions}
onClearSelection={onClearSelection}
/>
</SharedStack>
</SharedBox>
</SharedContainer>
</Stack>
</Box>
</Container>
);
}

View File

@@ -67,14 +67,14 @@ export function DashboardTemplate({
<TelemetryPanel title="Active Session">
<Box display="flex" alignItems="center" justifyContent="between">
<Box>
<Text size="xs" color="text-gray-500" mb={1} block>Next Event</Text>
<Text size="xs" variant="low" mb={1} block>Next Event</Text>
<Text size="lg" weight="bold" block>{nextRace.track}</Text>
<Text size="xs" color="primary-accent" font="mono" block>{nextRace.car}</Text>
<Text size="xs" variant="primary" font="mono" block>{nextRace.car}</Text>
</Box>
<Box textAlign="right">
<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>
<Text size="xs" variant="low" mb={1} block>Starts In</Text>
<Text size="xl" font="mono" weight="bold" variant="warning" block>{nextRace.timeUntil}</Text>
<Text size="xs" variant="low" block>{nextRace.formattedDate} @ {nextRace.formattedTime}</Text>
</Box>
</Box>
</TelemetryPanel>
@@ -85,7 +85,7 @@ export function DashboardTemplate({
<RecentActivityTable items={activityItems} />
) : (
<Box py={8} textAlign="center">
<Text italic color="text-gray-500">No recent activity recorded.</Text>
<Text italic variant="low">No recent activity recorded.</Text>
</Box>
)}
</TelemetryPanel>
@@ -99,18 +99,18 @@ export function DashboardTemplate({
{hasLeagueStandings ? (
<Stack direction="col" gap={3}>
{leagueStandings.map((standing) => (
<Box key={standing.leagueId} display="flex" alignItems="center" justifyContent="between" borderBottom borderColor="rgba(255, 255, 255, 0.1)" pb={2}>
<Box key={standing.leagueId} display="flex" alignItems="center" justifyContent="between" borderBottom borderColor="var(--ui-color-border-muted)" pb={2}>
<Box>
<Text size="xs" weight="bold" truncate block maxWidth="180px">{standing.leagueName}</Text>
<Text size="xs" color="text-gray-500" block>Pos: {standing.position} / {standing.totalDrivers}</Text>
<Text size="xs" variant="low" block>Pos: {standing.position} / {standing.totalDrivers}</Text>
</Box>
<Text size="sm" font="mono" weight="bold" color="primary-accent">{standing.points} PTS</Text>
<Text size="sm" font="mono" weight="bold" variant="primary">{standing.points} PTS</Text>
</Box>
))}
</Stack>
) : (
<Box py={4} textAlign="center">
<Text italic color="text-gray-500">No active championships.</Text>
<Text italic variant="low">No active championships.</Text>
</Box>
)}
</TelemetryPanel>
@@ -121,11 +121,11 @@ export function DashboardTemplate({
<Box key={race.id} cursor="pointer">
<Box display="flex" justifyContent="between" alignItems="start" mb={1}>
<Text size="xs" weight="bold">{race.track}</Text>
<Text size="xs" font="mono" color="text-gray-500">{race.timeUntil}</Text>
<Text size="xs" font="mono" variant="low">{race.timeUntil}</Text>
</Box>
<Box display="flex" justifyContent="between">
<Text size="xs" color="text-gray-500">{race.car}</Text>
<Text size="xs" color="text-gray-500">{race.formattedDate}</Text>
<Text size="xs" variant="low">{race.car}</Text>
<Text size="xs" variant="low">{race.formattedDate}</Text>
</Box>
</Box>
))}

View File

@@ -3,35 +3,33 @@
import { LeagueCard } from '@/components/leagues/LeagueCardWrapper';
import { routes } from '@/lib/routing/RouteConfig';
import type { LeagueDetailViewData } from '@/lib/view-data/LeagueDetailViewData';
import {
SharedBox,
SharedLink,
SharedText,
SharedStack,
SharedContainer,
SharedIcon
} from '@/components/shared/UIComponents';
import { Box } from '@/ui/Box';
import { Link } from '@/ui/Link';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Container } from '@/ui/Container';
import { Icon } from '@/ui/Icon';
import { ChevronRight } from 'lucide-react';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
export function LeagueDetailTemplate({ viewData, children, tabs }: TemplateProps<LeagueDetailViewData> & { children?: React.ReactNode, tabs?: any[] }) {
return (
<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>
<Container size="lg">
<Box paddingY={8}>
<Stack gap={8}>
<Box>
<Stack direction="row" align="center" gap={2}>
<Link href={routes.public.leagues}>
<Text size="sm" color="text-gray-400">Leagues</Text>
</Link>
<Icon icon={ChevronRight} size={3} color="text-gray-500" />
<Text size="sm" color="text-white">{viewData.name}</Text>
</Stack>
</Box>
{children}
{/* ... rest of the template ... */}
</SharedStack>
</SharedBox>
</SharedContainer>
</Stack>
</Box>
</Container>
);
}

View File

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

View File

@@ -2,14 +2,12 @@
import { UploadDropzone } from '@/components/shared/UploadDropzone';
import { routes } from '@/lib/routing/RouteConfig';
import {
SharedBox,
SharedButton,
SharedStack,
SharedText,
SharedContainer,
SharedCard
} from '@/components/shared/UIComponents';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Container } from '@/ui/Container';
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { MediaMetaPanel, mapMediaMetadata } from '@/ui/MediaMetaPanel';
import { MediaPreviewCard } from '@/ui/MediaPreviewCard';
@@ -33,18 +31,18 @@ export function ProfileLiveryUploadTemplate({
onUpload,
}: ProfileLiveryUploadTemplateProps) {
return (
<SharedContainer size="md">
<SharedBox paddingY={8}>
<SharedBox mb={6}>
<Container size="md">
<Box paddingY={8}>
<Box mb={6}>
<Heading level={1}>Upload livery</Heading>
<SharedText color="text-gray-500">
<Text color="text-gray-500">
Upload your custom car livery. Supported formats: .png, .jpg, .tga
</SharedText>
</SharedBox>
</Text>
</Box>
<SharedBox display="grid" responsiveGridCols={{ base: 1, md: 2 }} gap={6}>
<SharedBox>
<SharedCard>
<Box display="grid" responsiveGridCols={{ base: 1, md: 2 }} gap={6}>
<Box>
<Card>
<UploadDropzone
onFilesSelected={onFilesSelected}
accept=".png,.jpg,.jpeg,.tga"
@@ -52,25 +50,25 @@ export function ProfileLiveryUploadTemplate({
isLoading={isUploading}
/>
<SharedBox mt={6} display="flex" justifyContent="end" gap={3}>
<Box mt={6} display="flex" justifyContent="end" gap={3}>
<Link href={routes.protected.profileLiveries}>
<SharedButton variant="ghost">Cancel</SharedButton>
<Button variant="ghost">Cancel</Button>
</Link>
<SharedButton
<Button
variant="primary"
disabled={!selectedFile || isUploading}
onClick={onUpload}
isLoading={isUploading}
>
Upload Livery
</SharedButton>
</SharedBox>
</SharedCard>
</SharedBox>
</Button>
</Box>
</Card>
</Box>
<SharedBox>
<Box>
{previewUrl ? (
<SharedBox display="flex" flexDirection="col" gap={6}>
<Box display="flex" flexDirection="col" gap={6}>
<MediaPreviewCard
type="image"
src={previewUrl}
@@ -88,17 +86,17 @@ export function ProfileLiveryUploadTemplate({
createdAt: new Date(),
})}
/>
</SharedBox>
</Box>
) : (
<SharedCard center p={12}>
<SharedText color="text-gray-500" align="center">
<Card center p={12}>
<Text color="text-gray-500" align="center">
Select a file to see preview and details
</SharedText>
</SharedCard>
</Text>
</Card>
)}
</SharedBox>
</SharedBox>
</SharedBox>
</SharedContainer>
</Box>
</Box>
</Box>
</Container>
);
}

View File

@@ -39,11 +39,11 @@ 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="var(--color-primary)" />
<Icon icon={User} size={8} color="var(--ui-color-intent-primary)" />
</Surface>
<Box>
<Heading level={1}>Create Your Driver Profile</Heading>
<Text color="text-gray-400">Join the GridPilot community and start your racing journey</Text>
<Text variant="low">Join the GridPilot community and start your racing journey</Text>
</Box>
<Box maxWidth="42rem" mx="auto" width="100%">
@@ -51,7 +51,7 @@ export function ProfileTemplate({
<Stack gap={6}>
<Box>
<Heading level={2}>Get Started</Heading>
<Text size="sm" color="text-gray-400">
<Text size="sm" variant="low">
Create your driver profile to join leagues, compete in races, and connect with other drivers.
</Text>
</Box>
@@ -110,7 +110,7 @@ export function ProfileTemplate({
<Stack gap={4}>
<Stack direction="row" justify="between" align="center">
<Heading level={3} id="achievements-heading">Achievements</Heading>
<Text size="sm" color="text-gray-500">{viewData.extendedProfile.achievements.length} earned</Text>
<Text size="sm" variant="low">{viewData.extendedProfile.achievements.length} earned</Text>
</Stack>
<AchievementGrid
achievements={viewData.extendedProfile.achievements.map(a => ({

View File

@@ -1,14 +1,12 @@
'use client';
import {
SharedBox,
SharedButton,
SharedStack,
SharedText,
SharedIcon,
SharedCard,
SharedContainer
} from '@/components/shared/UIComponents';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
import { Card } from '@/ui/Card';
import { Container } from '@/ui/Container';
import { Heading } from '@/ui/Heading';
import { Link as UILink } from '@/ui/Link';
import { Grid } from '@/ui/Grid';
@@ -99,114 +97,114 @@ export function ProtestDetailTemplate({
const daysSinceFiled = Math.floor((Date.now() - new Date(submittedAt).getTime()) / (1000 * 60 * 60 * 24));
return (
<SharedBox minHeight="100vh">
<SharedContainer size="lg">
<SharedBox paddingY={8}>
<Box minHeight="100vh">
<Container size="lg">
<Box paddingY={8}>
{/* Compact Header */}
<SharedBox mb={6}>
<SharedStack direction="row" align="center" gap={3} mb={4}>
<Box mb={6}>
<Stack direction="row" align="center" gap={3} mb={4}>
<UILink href={routes.league.stewarding(leagueId)}>
<SharedIcon icon={ArrowLeft} size={5} color="text-gray-400" />
<Icon icon={ArrowLeft} size={5} color="text-gray-400" />
</UILink>
<SharedStack direction="row" align="center" gap={3} flexGrow={1}>
<Stack 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>
<Box 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}>
<Icon icon={StatusIcon} size={3} />
<Text>{statusConfig.label}</Text>
</Box>
{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>
<Box 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">
<Icon icon={AlertTriangle} size={3} />
<Text>{daysSinceFiled}d old</Text>
</Box>
)}
</SharedStack>
</SharedStack>
</SharedBox>
</Stack>
</Stack>
</Box>
{/* Main Layout: Feed + Sidebar */}
<Grid cols={12} gap={6}>
{/* Left Sidebar - Incident Info */}
<GridItem colSpan={{ base: 12, lg: 3 }}>
<SharedStack gap={4}>
<Stack gap={4}>
{/* Drivers Involved */}
<SharedCard>
<SharedBox p={4}>
<Card>
<Box p={4}>
<Heading level={3} fontSize="xs" weight="semibold" color="text-gray-500" mb={3}>Parties Involved</Heading>
<SharedStack gap={3}>
<Stack 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>
<Box 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">
<Box w={10} h={10} rounded="full" bg="bg-blue-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
<Icon icon={User} size={5} color="text-blue-400" />
</Box>
<Box flexGrow={1} minWidth="0">
<Text size="xs" color="text-blue-400" weight="medium" block>Protesting</Text>
<Text size="sm" weight="semibold" color="text-white" truncate block>{protestingDriver?.name || 'Unknown'}</Text>
</Box>
<Icon icon={ExternalLink} size={3} color="text-gray-500" />
</Box>
</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>
<Box 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">
<Box w={10} h={10} rounded="full" bg="bg-orange-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
<Icon icon={User} size={5} color="text-orange-400" />
</Box>
<Box flexGrow={1} minWidth="0">
<Text size="xs" color="text-orange-400" weight="medium" block>Accused</Text>
<Text size="sm" weight="semibold" color="text-white" truncate block>{accusedDriver?.name || 'Unknown'}</Text>
</Box>
<Icon icon={ExternalLink} size={3} color="text-gray-500" />
</Box>
</UILink>
</SharedStack>
</SharedBox>
</SharedCard>
</Stack>
</Box>
</Card>
{/* Race Info */}
<SharedCard>
<SharedBox p={4}>
<Card>
<Box p={4}>
<Heading level={3} fontSize="xs" weight="semibold" color="text-gray-500" mb={3}>Race Details</Heading>
<SharedBox marginBottom={3}>
<Box 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>
<Box p={3} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline" hoverBorderColor="border-primary-blue/50" hoverBg="bg-primary-blue/5" transition>
<Box display="flex" alignItems="center" justifyContent="between">
<Text size="sm" weight="medium" color="text-white">{race?.name || 'Unknown Race'}</Text>
<Icon icon={ExternalLink} size={3} color="text-gray-500" />
</Box>
</Box>
</UILink>
</SharedBox>
</Box>
<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>
<Stack gap={2}>
<Box display="flex" alignItems="center" gap={2}>
<Icon icon={MapPin} size={4} color="text-gray-500" />
<Text size="sm" color="text-gray-300">{race?.name || 'Unknown Track'}</Text>
</Box>
<Box display="flex" alignItems="center" gap={2}>
<Icon icon={Calendar} size={4} color="text-gray-500" />
<Text size="sm" color="text-gray-300">{race?.formattedDate || (race?.scheduledAt ? new Date(race.scheduledAt).toLocaleDateString() : 'Unknown Date')}</Text>
</Box>
{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>
<Box display="flex" alignItems="center" gap={2}>
<Icon icon={Flag} size={4} color="text-gray-500" />
<Text size="sm" color="text-gray-300">Lap {protest.incident.lap}</Text>
</Box>
)}
</SharedStack>
</SharedBox>
</SharedCard>
</Stack>
</Box>
</Card>
{protest.proofVideoUrl && (
<SharedCard>
<SharedBox p={4}>
<Card>
<Box p={4}>
<Heading level={3} fontSize="xs" weight="semibold" color="text-gray-500" mb={3}>Evidence</Heading>
<UILink
href={protest.proofVideoUrl}
@@ -214,133 +212,133 @@ export function ProtestDetailTemplate({
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>
<Box 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>
<Icon icon={Video} size={4} />
<Text size="sm" weight="medium" flexGrow={1}>Watch Video</Text>
<Icon icon={ExternalLink} size={3} />
</Box>
</UILink>
</SharedBox>
</SharedCard>
</Box>
</Card>
)}
{/* Quick Stats */}
<SharedCard>
<SharedBox p={4}>
<Card>
<Box 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>
<Stack gap={2}>
<Box display="flex" justifyContent="between">
<Text size="sm" color="text-gray-500">Filed</Text>
<Text size="sm" color="text-gray-300">{new Date(submittedAt).toLocaleDateString()}</Text>
</Box>
<Box display="flex" justifyContent="between">
<Text size="sm" color="text-gray-500">Age</Text>
<Text size="sm" color={daysSinceFiled > 2 ? 'text-red-400' : 'text-gray-300'}>{daysSinceFiled} days</Text>
</Box>
{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>
<Box display="flex" justifyContent="between">
<Text size="sm" color="text-gray-500">Resolved</Text>
<Text size="sm" color="text-gray-300">{new Date(protest.reviewedAt).toLocaleDateString()}</Text>
</Box>
)}
</SharedStack>
</SharedBox>
</SharedCard>
</SharedStack>
</Stack>
</Box>
</Card>
</Stack>
</GridItem>
{/* Center - Discussion Feed */}
<GridItem colSpan={12} lgSpan={6}>
<SharedStack gap={4}>
<Stack gap={4}>
{/* Timeline / Feed */}
<SharedCard>
<SharedBox borderBottom borderColor="border-charcoal-outline" bg="bg-iron-gray/30" p={4}>
<Card>
<Box borderBottom borderColor="border-charcoal-outline" bg="bg-iron-gray/30" p={4}>
<Heading level={2}>Discussion</Heading>
</SharedBox>
</Box>
<SharedStack gap={0}>
<Stack 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>
<Box p={4}>
<Box display="flex" gap={3}>
<Box w={10} h={10} rounded="full" bg="bg-blue-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
<Icon icon={AlertCircle} size={5} color="text-blue-400" />
</Box>
<Box flexGrow={1} minWidth="0">
<Box display="flex" alignItems="center" gap={2} mb={1}>
<Text weight="semibold" color="text-white" size="sm">{protestingDriver?.name || 'Unknown'}</Text>
<Text size="xs" color="text-blue-400" weight="medium">filed protest</Text>
<Text size="xs" color="text-gray-500"></Text>
<Text size="xs" color="text-gray-500">{new Date(submittedAt).toLocaleString()}</Text>
</Box>
<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>
<Box bg="bg-deep-graphite" rounded="lg" p={4} border borderColor="border-charcoal-outline">
<Text size="sm" color="text-gray-300" block mb={3}>{protest.description || protestDetail.incident?.description}</Text>
{(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>
<Box mt={3} pt={3} borderTop borderColor="border-charcoal-outline/50">
<Text size="xs" color="text-gray-500" block mb={1}>Additional details:</Text>
<Text size="sm" color="text-gray-400">{protest.comment || protestDetail.comment}</Text>
</Box>
)}
</SharedBox>
</SharedBox>
</SharedBox>
</SharedBox>
</Box>
</Box>
</Box>
</Box>
{/* 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>
<Box p={4} bg="bg-purple-500/5">
<Box display="flex" gap={3}>
<Box w={10} h={10} rounded="full" bg="bg-purple-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
<Icon icon={MessageCircle} size={5} color="text-purple-400" />
</Box>
<Box flexGrow={1}>
<Text size="sm" color="text-purple-400" weight="medium" block mb={1}>Defense Requested</Text>
<Text size="sm" color="text-gray-400">Waiting for {accusedDriver?.name || 'the accused driver'} to submit their defense...</Text>
</Box>
</Box>
</Box>
)}
{/* 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'}>
<Box p={4} bg={protest.status === 'upheld' ? 'bg-red-500/5' : 'bg-gray-500/5'}>
<Box display="flex" gap={3}>
<Box 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'}>
<Icon icon={Gavel} size={5} color={protest.status === 'upheld' ? 'text-red-400' : 'text-gray-400'} />
</Box>
<Box flexGrow={1} minWidth="0">
<Box display="flex" alignItems="center" gap={2} mb={1}>
<Text weight="semibold" color="text-white" size="sm">Steward Decision</Text>
<Text size="xs" weight="medium" color={protest.status === 'upheld' ? 'text-red-400' : 'text-gray-400'}>
{protest.status === 'upheld' ? 'Protest Upheld' : 'Protest Dismissed'}
</SharedText>
</Text>
{protest.reviewedAt && (
<>
<SharedText size="xs" color="text-gray-500"></SharedText>
<SharedText size="xs" color="text-gray-500">{new Date(protest.reviewedAt).toLocaleString()}</SharedText>
<Text size="xs" color="text-gray-500"></Text>
<Text size="xs" color="text-gray-500">{new Date(protest.reviewedAt).toLocaleString()}</Text>
</>
)}
</SharedBox>
</Box>
<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>
<Box 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'}>
<Text size="sm" color="text-gray-300">{protest.decisionNotes}</Text>
</Box>
</Box>
</Box>
</Box>
)}
</SharedStack>
</Stack>
{/* 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"
<Box p={4} borderTop borderColor="border-charcoal-outline" bg="bg-iron-gray/20">
<Box display="flex" gap={3}>
<Box w={10} h={10} rounded="full" bg="bg-iron-gray" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
<Icon icon={User} size={5} color="text-gray-500" />
</Box>
<Box flexGrow={1}>
<Box as="textarea"
value={newComment}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setNewComment(e.target.value)}
placeholder="Add a comment or request more information..."
@@ -355,102 +353,102 @@ export function ProtestDetailTemplate({
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' }} />
<Box display="flex" justifyContent="end" mt={2}>
<Button variant="secondary" disabled={!newComment.trim()}>
<Icon icon={Send} size={3} style={{ marginRight: '0.25rem' }} />
Comment
</SharedButton>
</SharedBox>
</SharedBox>
</SharedBox>
</SharedBox>
</Button>
</Box>
</Box>
</Box>
</Box>
)}
</SharedCard>
</SharedStack>
</Card>
</Stack>
</GridItem>
{/* Right Sidebar - Actions */}
<GridItem colSpan={12} lgSpan={3}>
<SharedStack gap={4}>
<Stack gap={4}>
{isPending && (
<>
{/* Quick Actions */}
<SharedCard>
<SharedBox p={4}>
<Card>
<Box p={4}>
<Heading level={3} fontSize="xs" weight="semibold" color="text-gray-500" mb={3}>Actions</Heading>
<SharedStack gap={2}>
<SharedButton
<Stack gap={2}>
<Button
variant="secondary"
fullWidth
onClick={onRequestDefense}
>
<SharedStack direction="row" align="center" gap={2}>
<SharedIcon icon={MessageCircle} size={4} />
<SharedText>Request Defense</SharedText>
</SharedStack>
</SharedButton>
<Stack direction="row" align="center" gap={2}>
<Icon icon={MessageCircle} size={4} />
<Text>Request Defense</Text>
</Stack>
</Button>
<SharedButton
<Button
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>
<Stack direction="row" align="center" gap={2} fullWidth>
<Icon icon={Gavel} size={4} />
<Text>Make Decision</Text>
<Box ml="auto" transition style={{ transform: showDecisionPanel ? 'rotate(180deg)' : 'none' }}>
<Icon icon={ChevronDown} size={4} />
</Box>
</Stack>
</Button>
</Stack>
</Box>
</Card>
{/* Decision Panel */}
{showDecisionPanel && (
<SharedCard>
<SharedBox p={4}>
<Card>
<Box 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
<Box padding={3} border borderColor={decision === 'uphold' ? 'border-racing-red' : 'border-charcoal-outline'} bg={decision === 'uphold' ? 'bg-racing-red/10' : 'transparent'} rounded="lg">
<Button
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
<Stack align="center" gap={1}>
<Icon icon={CheckCircle} size={5} color={decision === 'uphold' ? 'text-red-400' : 'text-gray-500'} />
<Text size="xs" weight="medium" color={decision === 'uphold' ? 'text-red-400' : 'text-gray-400'}>Uphold</Text>
</Stack>
</Button>
</Box>
<Box padding={3} border borderColor={decision === 'dismiss' ? 'border-gray-500' : 'border-charcoal-outline'} bg={decision === 'dismiss' ? 'bg-gray-500/10' : 'transparent'} rounded="lg">
<Button
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>
<Stack align="center" gap={1}>
<Icon icon={XCircle} size={5} color={decision === 'dismiss' ? 'text-gray-300' : 'text-gray-500'} />
<Text size="xs" weight="medium" color={decision === 'dismiss' ? 'text-gray-300' : 'text-gray-400'}>Dismiss</Text>
</Stack>
</Button>
</Box>
</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>
<Box mb={4}>
<Text as="label" size="xs" weight="medium" color="text-gray-400" block mb={2}>Penalty Type</Text>
{penaltyTypes.length === 0 ? (
<SharedText size="xs" color="text-gray-500">
<Text size="xs" color="text-gray-500">
Loading penalty types...
</SharedText>
</Text>
) : (
<>
<Grid cols={2} gap={2}>
@@ -458,8 +456,8 @@ export function ProtestDetailTemplate({
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
<Box key={penalty.type} padding={2} border borderColor={isSelected ? undefined : 'border-charcoal-outline'} bg={isSelected ? undefined : 'bg-iron-gray/30'} rounded="lg">
<Button
variant="ghost"
onClick={() => {
setPenaltyType(penalty.type);
@@ -468,24 +466,24 @@ export function ProtestDetailTemplate({
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'}>
<Stack align="start" gap={0.5}>
<Icon icon={Icon} size={3.5} color={isSelected ? penalty.color : 'text-gray-500'} />
<Text size="xs" weight="medium" fontSize="10px" color={isSelected ? penalty.color : 'text-gray-500'}>
{penalty.label}
</SharedText>
</SharedStack>
</SharedButton>
</SharedBox>
</Text>
</Stack>
</Button>
</Box>
);
})}
</Grid>
{selectedPenalty?.requiresValue && (
<SharedBox mt={3}>
<SharedText as="label" size="xs" weight="medium" color="text-gray-400" block mb={1}>
<Box mt={3}>
<Text as="label" size="xs" weight="medium" color="text-gray-400" block mb={1}>
Value ({selectedPenalty.valueLabel})
</SharedText>
<SharedBox as="input"
</Text>
<Box as="input"
type="number"
value={penaltyValue}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPenaltyValue(Number(e.target.value))}
@@ -500,17 +498,17 @@ export function ProtestDetailTemplate({
color="text-white"
fontSize="sm"
/>
</SharedBox>
</Box>
)}
</>
)}
</SharedBox>
</Box>
)}
{/* 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"
<Box mb={4}>
<Text as="label" size="xs" weight="medium" color="text-gray-400" block mb={1}>Decision Reasoning *</Text>
<Box as="textarea"
value={stewardNotes}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setStewardNotes(e.target.value)}
placeholder="Explain your decision..."
@@ -525,42 +523,42 @@ export function ProtestDetailTemplate({
color="text-white"
fontSize="sm"
/>
</SharedBox>
</Box>
{/* Submit */}
<SharedButton
<Button
variant="primary"
fullWidth
onClick={onSubmitDecision}
disabled={!decision || !stewardNotes.trim() || submitting}
>
{submitting ? 'Submitting...' : 'Submit Decision'}
</SharedButton>
</SharedBox>
</SharedCard>
</Button>
</Box>
</Card>
)}
</>
)}
{/* 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}>
<Card>
<Box p={4} textAlign="center">
<Box py={4} color={protest.status === 'upheld' ? 'text-red-400' : 'text-gray-400'}>
<Icon icon={Gavel} size={8} style={{ margin: '0 auto 0.5rem auto' }} />
<Text weight="semibold" block>Case Closed</Text>
<Text size="xs" color="text-gray-500" block mt={1}>
{protest.status === 'upheld' ? 'Protest was upheld' : 'Protest was dismissed'}
</SharedText>
</SharedBox>
</SharedBox>
</SharedCard>
</Text>
</Box>
</Box>
</Card>
)}
</SharedStack>
</Stack>
</GridItem>
</Grid>
</SharedBox>
</SharedContainer>
</SharedBox>
</Box>
</Container>
</Box>
);
}

View File

@@ -7,7 +7,9 @@ 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 { SharedPagination, SharedText, SharedBox } from '@/components/shared/UIComponents';
import { Pagination } from '@/ui/Pagination';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
export type StatusFilter = 'scheduled' | 'running' | 'completed' | 'cancelled' | 'all';
@@ -76,7 +78,7 @@ export function RacesAllTemplate({
/>
}
pagination={
<SharedPagination
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={onPageChange}
@@ -85,9 +87,9 @@ export function RacesAllTemplate({
>
<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>
<Box p={12} textAlign="center">
<Text color="text-gray-500">No races found matching your criteria.</Text>
</Box>
) : (
<RaceScheduleTable
races={races.map(race => ({

View File

@@ -7,15 +7,13 @@ import { SponsorDashboardHeader } from '@/components/sponsors/SponsorDashboardHe
import { SponsorStatusChip } from '@/components/sponsors/SponsorStatusChip';
import { routes } from '@/lib/routing/RouteConfig';
import { siteConfig } from '@/lib/siteConfig';
import {
SharedBox,
SharedButton,
SharedStack,
SharedText,
SharedIcon,
SharedCard,
SharedContainer
} from '@/components/shared/UIComponents';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
import { Card } from '@/ui/Card';
import { Container } from '@/ui/Container';
import { Heading } from '@/ui/Heading';
import { Link } from '@/ui/Link';
import { Grid } from '@/ui/Grid';
@@ -187,23 +185,23 @@ export function SponsorLeagueDetailTemplate({
];
return (
<SharedContainer size="lg">
<SharedBox paddingY={8}>
<SharedStack gap={8}>
<Container size="lg">
<Box paddingY={8}>
<Stack gap={8}>
{/* Breadcrumb */}
<SharedBox>
<SharedStack direction="row" align="center" gap={2}>
<Box>
<Stack direction="row" align="center" gap={2}>
<Link href={routes.sponsor.dashboard}>
<SharedText size="sm" color="text-gray-400">Dashboard</SharedText>
<Text size="sm" color="text-gray-400">Dashboard</Text>
</Link>
<SharedText size="sm" color="text-gray-500">/</SharedText>
<Text size="sm" color="text-gray-500">/</Text>
<Link href={routes.sponsor.leagues}>
<SharedText size="sm" color="text-gray-400">Leagues</SharedText>
<Text size="sm" color="text-gray-400">Leagues</Text>
</Link>
<SharedText size="sm" color="text-gray-500">/</SharedText>
<SharedText size="sm" color="text-white">{league.name}</SharedText>
</SharedStack>
</SharedBox>
<Text size="sm" color="text-gray-500">/</Text>
<Text size="sm" color="text-white">{league.name}</Text>
</Stack>
</Box>
{/* Header */}
<SponsorDashboardHeader
@@ -215,10 +213,10 @@ export function SponsorLeagueDetailTemplate({
<BillingSummaryPanel stats={billingStats} />
{/* Tabs */}
<SharedBox borderBottom borderColor="border-neutral-800">
<SharedStack direction="row" gap={6}>
<Box borderBottom borderColor="border-neutral-800">
<Stack direction="row" gap={6}>
{(['overview', 'drivers', 'races', 'sponsor'] as const).map((tab) => (
<SharedBox
<Box
key={tab}
onClick={() => setActiveTab(tab)}
style={{ paddingBottom: '0.75rem' }}
@@ -227,148 +225,148 @@ export function SponsorLeagueDetailTemplate({
borderColor={activeTab === tab ? 'border-primary-blue' : 'border-transparent'}
color={activeTab === tab ? 'text-primary-blue' : 'text-gray-400'}
>
<SharedText size="sm" weight="medium" className="uppercase">
<Text size="sm" weight="medium" className="uppercase">
{tab === 'sponsor' ? '🎯 Become a Sponsor' : tab}
</SharedText>
</SharedBox>
</Text>
</Box>
))}
</SharedStack>
</SharedBox>
</Stack>
</Box>
{/* 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" />
<Card>
<Box mb={4}>
<Stack direction="row" align="center" gap={3}>
<Icon icon={Trophy} size={5} color="#3b82f6" />
<Heading level={2}>
League Information
</Heading>
</SharedStack>
</SharedBox>
<SharedStack gap={3}>
</Stack>
</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.formattedDrivers} />
<InfoRow label="Races" value={league.formattedRaces} last />
</SharedStack>
</SharedCard>
</Stack>
</Card>
<SharedCard>
<SharedBox mb={4}>
<Heading level={2} icon={<SharedIcon icon={TrendingUp} size={5} color="#10b981" />}>
<Card>
<Box mb={4}>
<Heading level={2} icon={<Icon icon={TrendingUp} size={5} color="#10b981" />}>
Sponsorship Value
</Heading>
</SharedBox>
<SharedStack gap={3}>
</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.formattedEngagement} />
<InfoRow label="League Rating" value={league.formattedRating} last />
</SharedStack>
</SharedCard>
</Stack>
</Card>
{league.nextRace && (
<GridItem colSpan={2}>
<SharedCard>
<SharedBox mb={4}>
<Heading level={2} icon={<SharedIcon icon={Flag} size={5} color="#f59e0b" />}>
<Card>
<Box mb={4}>
<Heading level={2} icon={<Icon icon={Flag} size={5} color="#f59e0b" />}>
Next Race
</Heading>
</SharedBox>
</Box>
<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}>
<Stack direction="row" align="center" justify="between">
<Stack 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" />
<Icon 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.formattedDate}</SharedText>
</SharedBox>
</SharedStack>
<SharedButton variant="secondary">
<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.formattedDate}</Text>
</Box>
</Stack>
<Button variant="secondary">
View Schedule
</SharedButton>
</SharedStack>
</Button>
</Stack>
</Surface>
</SharedCard>
</Card>
</GridItem>
)}
</Grid>
)}
{activeTab === 'drivers' && (
<SharedCard p={0}>
<SharedBox p={4} borderBottom borderColor="border-neutral-800">
<Card p={0}>
<Box 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}>
<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) => (
<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}>
<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} style={{ width: '2.5rem', height: '2.5rem', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#262626' }}>
<SharedText weight="bold" color="text-white">{driver.positionLabel}</SharedText>
<Text weight="bold" color="text-white">{driver.positionLabel}</Text>
</Surface>
<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.formattedRaces}</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>
<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.formattedRaces}</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>
))}
</SharedStack>
</SharedCard>
</Stack>
</Card>
)}
{activeTab === 'races' && (
<SharedCard p={0}>
<SharedBox p={4} borderBottom borderColor="border-neutral-800">
<Card p={0}>
<Box 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}>
<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) => (
<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>
<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' ? (
<SharedBox textAlign="right">
<SharedText weight="semibold" color="text-white" block>{race.formattedViews}</SharedText>
<SharedText size="xs" color="text-gray-500">views</SharedText>
</SharedBox>
<Box textAlign="right">
<Text weight="semibold" color="text-white" block>{race.formattedViews}</Text>
<Text size="xs" color="text-gray-500">views</Text>
</Box>
) : (
<SponsorStatusChip status="pending" label="Upcoming" />
)}
</SharedBox>
</SharedStack>
</SharedBox>
</Box>
</Stack>
</Box>
))}
</SharedStack>
</SharedCard>
</Stack>
</Card>
)}
{activeTab === 'sponsor' && (
@@ -383,62 +381,62 @@ export function SponsorLeagueDetailTemplate({
</GridItem>
<GridItem colSpan={{ base: 12, lg: 4 }}>
<SharedStack gap={6}>
<Stack gap={6}>
<SponsorBrandingPreview
name="Your Brand"
/>
<SharedCard>
<SharedBox mb={4}>
<Heading level={2} icon={<SharedIcon icon={CreditCard} size={5} color="#3b82f6" />}>
<Card>
<Box mb={4}>
<Heading level={2} icon={<Icon icon={CreditCard} size={5} color="#3b82f6" />}>
Sponsorship Summary
</Heading>
</SharedBox>
</Box>
<SharedStack gap={3} mb={6}>
<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.priceLabel : league.sponsorSlots.secondary.priceLabel} />
<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">
<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)}
</SharedText>
</SharedStack>
</SharedBox>
</SharedStack>
</Text>
</Stack>
</Box>
</Stack>
<SharedText size="xs" color="text-gray-500" block mb={4}>
<Text size="xs" color="text-gray-500" block mb={4}>
{siteConfig.vat.notice}
</SharedText>
</Text>
<SharedStack direction="row" gap={3}>
<SharedButton variant="primary" fullWidth icon={<SharedIcon icon={Megaphone} size={4} />}>
<Stack direction="row" gap={3}>
<Button variant="primary" fullWidth icon={<Icon icon={Megaphone} size={4} />}>
Request Sponsorship
</SharedButton>
<SharedButton variant="secondary" icon={<SharedIcon icon={FileText} size={4} />}>
</Button>
<Button variant="secondary" icon={<Icon icon={FileText} size={4} />}>
Download Info Pack
</SharedButton>
</SharedStack>
</SharedCard>
</SharedStack>
</Button>
</Stack>
</Card>
</Stack>
</GridItem>
</Grid>
)}
</SharedStack>
</SharedBox>
</SharedContainer>
</Stack>
</Box>
</Container>
);
}
function InfoRow({ label, value, color = 'text-white', last }: { label: string, value: string | number, color?: string, last?: boolean }) {
return (
<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>
<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>
);
}

View File

@@ -7,13 +7,11 @@ 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 { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Card } from '@/ui/Card';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
interface StewardingTemplateProps extends TemplateProps<StewardingViewData> {
@@ -51,7 +49,7 @@ export function StewardingTemplate({
currentDriverId,
}: StewardingTemplateProps) {
return (
<SharedStack gap={6}>
<Stack gap={6}>
<StewardingStats
totalPending={viewData.totalPending}
totalResolved={viewData.totalResolved}
@@ -59,41 +57,41 @@ export function StewardingTemplate({
/>
{/* Tab navigation */}
<SharedBox borderBottom borderColor="border-charcoal-outline">
<SharedStack direction="row" gap={4}>
<SharedBox
<Box borderBottom borderColor="border-charcoal-outline">
<Stack direction="row" gap={4}>
<Box
borderBottom={activeTab === 'pending'}
borderColor={activeTab === 'pending' ? 'border-primary-blue' : undefined}
>
<SharedButton
<Button
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>
<Stack direction="row" align="center" gap={2}>
<Text weight="medium" color={activeTab === 'pending' ? 'text-primary-blue' : undefined}>Pending Protests</Text>
{viewData.totalPending > 0 && (
<SharedBox px={2} py={0.5} fontSize="0.75rem" bg="bg-warning-amber/20" color="text-warning-amber" rounded="full">
<Box px={2} py={0.5} fontSize="0.75rem" bg="bg-warning-amber/20" color="text-warning-amber" rounded="full">
{viewData.totalPending}
</SharedBox>
</Box>
)}
</SharedStack>
</SharedButton>
</SharedBox>
<SharedBox
</Stack>
</Button>
</Box>
<Box
borderBottom={activeTab === 'history'}
borderColor={activeTab === 'history' ? 'border-primary-blue' : undefined}
>
<SharedButton
<Button
variant="ghost"
onClick={() => onTabChange('history')}
rounded={false}
>
<SharedText weight="medium" color={activeTab === 'history' ? 'text-primary-blue' : undefined}>History</SharedText>
</SharedButton>
</SharedBox>
</SharedStack>
</SharedBox>
<Text weight="medium" color={activeTab === 'history' ? 'text-primary-blue' : undefined}>History</Text>
</Button>
</Box>
</Stack>
</Box>
{/* Content */}
{activeTab === 'pending' ? (
@@ -102,15 +100,15 @@ export function StewardingTemplate({
onReview={onReviewProtest}
/>
) : (
<SharedCard>
<SharedBox p={6}>
<Card>
<Box p={6}>
<PenaltyHistoryList
protests={allResolvedProtests}
races={racesMap}
drivers={driverMap}
/>
</SharedBox>
</SharedCard>
</Box>
</Card>
)}
{activeTab === 'history' && (
@@ -141,6 +139,6 @@ export function StewardingTemplate({
races={viewData.races.map(r => ({ id: r.id, track: r.track, scheduledAt: new Date(r.scheduledAt) }))}
/>
)}
</SharedStack>
</Stack>
);
}

View File

@@ -74,7 +74,7 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
/>
<Group justify="end" fullWidth>
<Link href={routes.auth.forgotPassword}>
<Text size="xs" color="text-primary-accent">
<Text size="xs" variant="primary">
Forgot password?
</Text>
</Link>
@@ -101,10 +101,10 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
{viewData.hasInsufficientPermissions && (
<Group direction="row" align="start" gap={3} fullWidth>
<Icon icon={AlertCircle} size={5} color="var(--color-warning)" />
<Icon icon={AlertCircle} size={5} color="var(--ui-color-intent-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>
<Text weight="bold" variant="warning" block size="sm">Insufficient Permissions</Text>
<Text size="xs" variant="low" block>
Please log in with an account that has the required role.
</Text>
</Group>
@@ -133,17 +133,17 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
</AuthForm>
<AuthFooterLinks>
<Text size="sm" color="text-gray-400">
<Text size="sm" variant="low">
Don&apos;t have an account?{' '}
<Link
href={viewData.returnTo && viewData.returnTo !== '/dashboard' ? `/auth/signup?returnTo=${encodeURIComponent(viewData.returnTo)}` : '/auth/signup'}
>
<Text as="span" color="text-primary-accent" weight="bold">Create one</Text>
<Text as="span" variant="primary" weight="bold">Create one</Text>
</Link>
</Text>
<Group direction="column" gap={1} align="center" fullWidth>
<Text size="xs" color="text-gray-600">
<Text size="xs" variant="low">
By signing in, you agree to our{' '}
<Link href="/terms">Terms</Link>
{' '}and{' '}

View File

@@ -62,7 +62,7 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
<AuthForm onSubmit={formActions.handleSubmit}>
<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>
<Text size="xs" weight="bold" variant="low" uppercase letterSpacing="wide" block>Personal Information</Text>
<Grid cols={{ base: 1, md: 2 }} gap={4}>
<Input
label="First Name"
@@ -91,9 +91,9 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
</Grid>
<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.
<Icon icon={AlertCircle} size={3.5} color="var(--ui-color-intent-warning)" />
<Text size="xs" variant="low">
<Text weight="bold" variant="warning">Note:</Text> Your name cannot be changed after signup.
</Text>
</Group>
@@ -113,7 +113,7 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
</Group>
<Group direction="column" gap={4} fullWidth>
<Text size="xs" weight="bold" color="text-low" uppercase letterSpacing="wide" block>Security</Text>
<Text size="xs" weight="bold" variant="low" uppercase letterSpacing="wide" block>Security</Text>
<PasswordField
label="Password"
id="password"
@@ -138,15 +138,15 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
size="sm"
/>
</Group>
<Text size="xs" weight="bold" color="text-low" uppercase>
<Text size="xs" weight="bold" variant="low" uppercase>
{passwordStrength.label}
</Text>
</Group>
<Grid cols={2} gap={2}>
{passwordRequirements.map((req, index) => (
<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'}>
<Icon icon={req.met ? Check : X} size={3} color={req.met ? 'var(--ui-color-intent-success)' : 'var(--ui-color-text-low)'} />
<Text size="xs" variant={req.met ? 'med' : 'low'}>
{req.label}
</Text>
</Group>
@@ -173,8 +173,8 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
{mutationState.error && (
<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>
<Icon icon={AlertCircle} size={4.5} color="var(--ui-color-intent-critical)" />
<Text size="sm" variant="critical">{mutationState.error}</Text>
</Group>
)}
@@ -190,17 +190,17 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
</AuthForm>
<AuthFooterLinks>
<Text size="sm" color="text-gray-400">
<Text size="sm" variant="low">
Already have an account?{' '}
<Link
href={viewData.returnTo && viewData.returnTo !== '/onboarding' ? `/auth/login?returnTo=${encodeURIComponent(viewData.returnTo)}` : '/auth/login'}
>
<Text color="text-primary-accent" weight="bold">Sign in</Text>
<Text variant="primary" weight="bold">Sign in</Text>
</Link>
</Text>
<Group direction="column" gap={1} align="center" fullWidth>
<Text size="xs" color="text-gray-600">
<Text size="xs" variant="low">
By creating an account, you agree to our{' '}
<Link href="/terms">Terms</Link>
{' '}and{' '}

View File

@@ -15,8 +15,8 @@ export function GlobalFooterTemplate(_props: GlobalFooterViewData) {
<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" />
<Box display={{ base: 'none', sm: 'flex' }} alignItems="center" gap={2} borderLeft borderColor="var(--ui-color-border-default)" pl={4}>
<Box w="4px" h="4px" rounded="full" bg="var(--ui-color-intent-primary)" animate="pulse" />
<Text size="xs" variant="low" weight="bold" font="mono" letterSpacing="0.1em">
INFRASTRUCTURE
</Text>

View File

@@ -21,7 +21,7 @@ export function GlobalSidebarTemplate(_props: GlobalSidebarViewData) {
<DashboardRail>
<Box py={6} fullWidth>
<Box px={6} mb={8}>
<Text size="xs" color="text-gray-500" weight="bold" font="mono" letterSpacing="0.2em">
<Text size="xs" variant="low" weight="bold" font="mono" letterSpacing="0.2em">
NAVIGATION
</Text>
</Box>

View File

@@ -20,9 +20,9 @@ export function HeaderContentTemplate(_props: HeaderContentViewData) {
<>
<Stack direction="row" align="center" gap={6}>
<BrandMark href={homeHref} priority />
<Box display={{ base: 'none', sm: 'flex' }} alignItems="center" gap={2} borderLeft borderColor="[#23272B]" pl={6}>
<Box w="6px" h="6px" rounded="full" bg="primary-accent" animate="pulse" />
<Text size="xs" color="text-gray-500" weight="bold" font="mono" letterSpacing="0.2em">
<Box display={{ base: 'none', sm: 'flex' }} alignItems="center" gap={2} borderLeft borderColor="var(--ui-color-border-default)" pl={6}>
<Box w="6px" h="6px" rounded="full" bg="var(--ui-color-intent-primary)" animate="pulse" />
<Text size="xs" variant="low" weight="bold" font="mono" letterSpacing="0.2em">
MOTORSPORT INFRASTRUCTURE
</Text>
</Box>
@@ -35,9 +35,9 @@ export function HeaderContentTemplate(_props: HeaderContentViewData) {
)}
<Box display="flex" alignItems="center" gap={4}>
<Stack direction="row" display={{ base: 'none', md: 'flex' }} align="center" gap={1} px={3} py={1} border borderColor="[#23272B]" bg="[#141619]/20">
<Text size="xs" color="text-gray-600" weight="bold" font="mono">STATUS:</Text>
<Text size="xs" color="text-success-green" weight="bold" font="mono">OPERATIONAL</Text>
<Stack direction="row" display={{ base: 'none', md: 'flex' }} align="center" gap={1} px={3} py={1} border borderColor="var(--ui-color-border-default)" bg="var(--ui-color-bg-surface-muted)">
<Text size="xs" variant="low" weight="bold" font="mono">STATUS:</Text>
<Text size="xs" variant="success" weight="bold" font="mono">OPERATIONAL</Text>
</Stack>
<HeaderActions isAuthenticated={isAuthenticated} />
</Box>

View File

@@ -1,6 +1,8 @@
'use client';
import { SharedContainer, SharedStack, SharedText } from '@/components/shared/UIComponents';
import { Container } from '@/ui/Container';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
import { ViewData } from '@/lib/contracts/view-data/ViewData';
@@ -11,12 +13,12 @@ interface ErrorTemplateProps extends TemplateProps<ViewData> {
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>
<Container size="lg">
<Stack align="center" gap={4} py={12}>
<Text color="text-red-400">{message}</Text>
<Text color="text-gray-400">{description}</Text>
</Stack>
</Container>
);
}
@@ -27,11 +29,11 @@ interface EmptyTemplateProps extends TemplateProps<ViewData> {
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>
<Container size="lg">
<Stack align="center" gap={2} py={12}>
<Text size="xl" weight="semibold" color="text-white">{title}</Text>
<Text color="text-gray-400">{description}</Text>
</Stack>
</Container>
);
}