235 lines
7.8 KiB
TypeScript
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're looking for doesn'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>
|
|
);
|
|
}
|