Files
gridpilot.gg/apps/website/templates/RaceStewardingTemplate.tsx
2026-01-15 18:52:03 +01:00

235 lines
7.8 KiB
TypeScript

'use client';
import React from 'react';
import { Breadcrumbs } from '@/ui/Breadcrumbs';
import { RaceStewardingStats } from '@/ui/RaceStewardingStats';
import { StewardingTabs } from '@/ui/StewardingTabs';
import { ProtestCard } from '@/ui/ProtestCardWrapper';
import { RacePenaltyRow } from '@/ui/RacePenaltyRowWrapper';
import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Button } from '@/ui/Button';
import { Container } from '@/ui/Container';
import { Icon } from '@/ui/Icon';
import { Surface } from '@/ui/Surface';
import {
AlertTriangle,
ArrowLeft,
CheckCircle,
Flag,
Gavel,
Scale,
} from 'lucide-react';
import type { RaceStewardingViewData } from '@/lib/view-data/races/RaceStewardingViewData';
export type StewardingTab = 'pending' | 'resolved' | 'penalties';
interface RaceStewardingTemplateProps {
viewData: RaceStewardingViewData;
isLoading: boolean;
error?: Error | null;
// Actions
onBack: () => void;
onReviewProtest: (protestId: string) => void;
// User state
isAdmin: boolean;
// UI State
activeTab: StewardingTab;
setActiveTab: (tab: StewardingTab) => void;
}
export function RaceStewardingTemplate({
viewData,
isLoading,
onBack,
onReviewProtest,
isAdmin,
activeTab,
setActiveTab,
}: RaceStewardingTemplateProps) {
const formatDate = (date: string) => {
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
};
if (isLoading) {
return (
<Container size="lg" py={12}>
<Stack align="center">
<Text color="text-gray-400">Loading stewarding data...</Text>
</Stack>
</Container>
);
}
if (!viewData?.race) {
return (
<Container size="md" py={12}>
<Card>
<Stack align="center" py={12} gap={4}>
<Surface variant="muted" rounded="full" padding={4}>
<Icon icon={AlertTriangle} size={8} color="#f59e0b" />
</Surface>
<Box textAlign="center">
<Text weight="medium" color="text-white" block mb={1}>Race not found</Text>
<Text size="sm" color="text-gray-500">The race you&apos;re looking for doesn&apos;t exist.</Text>
</Box>
<Button variant="secondary" onClick={onBack}>
Back to Races
</Button>
</Stack>
</Card>
</Container>
);
}
const breadcrumbItems = [
{ label: 'Races', href: '/races' },
{ label: viewData.race.track, href: `/races/${viewData.race.id}` },
{ label: 'Stewarding' },
];
return (
<Container size="lg" py={8}>
<Stack gap={6}>
{/* Navigation */}
<Stack direction="row" align="center" justify="between">
<Breadcrumbs items={breadcrumbItems} />
<Button
variant="secondary"
onClick={onBack}
icon={<Icon icon={ArrowLeft} size={4} />}
>
Back to Race
</Button>
</Stack>
{/* Header */}
<Surface variant="muted" rounded="xl" border padding={6} bg="bg-gradient-to-r from-neutral-800/50 to-neutral-800/30" borderColor="border-neutral-800">
<Stack direction="row" align="center" gap={4} mb={6}>
<Surface variant="muted" rounded="xl" padding={3} bg="bg-blue-500/20">
<Icon icon={Scale} size={6} color="#3b82f6" />
</Surface>
<Box>
<Heading level={1}>Stewarding</Heading>
<Text size="sm" color="text-gray-400" block mt={1}>
{viewData.race.track} {formatDate(viewData.race.scheduledAt)}
</Text>
</Box>
</Stack>
{/* Stats */}
<RaceStewardingStats
pendingCount={viewData.pendingCount}
resolvedCount={viewData.resolvedCount}
penaltiesCount={viewData.penaltiesCount}
/>
</Surface>
{/* Tab Navigation */}
<StewardingTabs
activeTab={activeTab}
onTabChange={setActiveTab}
pendingCount={viewData.pendingProtests.length}
/>
{/* Content */}
{activeTab === 'pending' && (
<Stack gap={4}>
{viewData.pendingProtests.length === 0 ? (
<Card>
<Stack align="center" py={12} gap={4}>
<Surface variant="muted" rounded="full" padding={4}>
<Icon icon={Flag} size={8} color="#10b981" />
</Surface>
<Box textAlign="center">
<Text weight="semibold" size="lg" color="text-white" block mb={1}>All Clear!</Text>
<Text size="sm" color="text-gray-400">No pending protests to review</Text>
</Box>
</Stack>
</Card>
) : (
viewData.pendingProtests.map((protest) => (
<ProtestCard
key={protest.id}
protest={protest}
protester={viewData.driverMap[protest.protestingDriverId]}
accused={viewData.driverMap[protest.accusedDriverId]}
isAdmin={isAdmin}
onReview={onReviewProtest}
formatDate={formatDate}
/>
))
)}
</Stack>
)}
{activeTab === 'resolved' && (
<Stack gap={4}>
{viewData.resolvedProtests.length === 0 ? (
<Card>
<Stack align="center" py={12} gap={4}>
<Surface variant="muted" rounded="full" padding={4}>
<Icon icon={CheckCircle} size={8} color="#525252" />
</Surface>
<Box textAlign="center">
<Text weight="semibold" size="lg" color="text-white" block mb={1}>No Resolved Protests</Text>
<Text size="sm" color="text-gray-400">Resolved protests will appear here</Text>
</Box>
</Stack>
</Card>
) : (
viewData.resolvedProtests.map((protest) => (
<ProtestCard
key={protest.id}
protest={protest}
protester={viewData.driverMap[protest.protestingDriverId]}
accused={viewData.driverMap[protest.accusedDriverId]}
isAdmin={isAdmin}
onReview={onReviewProtest}
formatDate={formatDate}
/>
))
)}
</Stack>
)}
{activeTab === 'penalties' && (
<Stack gap={4}>
{viewData.penalties.length === 0 ? (
<Card>
<Stack align="center" py={12} gap={4}>
<Surface variant="muted" rounded="full" padding={4}>
<Icon icon={Gavel} size={8} color="#525252" />
</Surface>
<Box textAlign="center">
<Text weight="semibold" size="lg" color="text-white" block mb={1}>No Penalties</Text>
<Text size="sm" color="text-gray-400">Penalties issued for this race will appear here</Text>
</Box>
</Stack>
</Card>
) : (
viewData.penalties.map((penalty) => (
<RacePenaltyRow
key={penalty.id}
penalty={{
...penalty,
driverName: viewData.driverMap[penalty.driverId]?.name || 'Unknown',
type: penalty.type as 'time_penalty' | 'grid_penalty' | 'points_deduction' | 'disqualification' | 'warning' | 'license_points'
}}
/>
))
)}
</Stack>
)}
</Stack>
</Container>
);
}