page wrapper

This commit is contained in:
2026-01-07 12:40:52 +01:00
parent e589c30bf8
commit 0db80fa98d
128 changed files with 7386 additions and 8096 deletions

View File

@@ -0,0 +1,359 @@
'use client';
import PenaltyFAB from '@/components/leagues/PenaltyFAB';
import QuickPenaltyModal from '@/components/leagues/QuickPenaltyModal';
import { ReviewProtestModal } from '@/components/leagues/ReviewProtestModal';
import StewardingStats from '@/components/leagues/StewardingStats';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import { useLeagueStewardingMutations } from '@/hooks/league/useLeagueStewardingMutations';
import {
AlertCircle,
AlertTriangle,
Calendar,
ChevronRight,
Flag,
Gavel,
MapPin,
Video
} from 'lucide-react';
import Link from 'next/link';
import { useMemo, useState } from 'react';
interface StewardingData {
totalPending: number;
totalResolved: number;
totalPenalties: number;
racesWithData: Array<{
race: { id: string; track: string; scheduledAt: Date };
pendingProtests: any[];
resolvedProtests: any[];
penalties: any[];
}>;
allDrivers: any[];
driverMap: Record<string, any>;
}
interface StewardingTemplateProps {
data: StewardingData;
leagueId: string;
currentDriverId: string;
onRefetch: () => void;
}
export function StewardingTemplate({ data, leagueId, currentDriverId, onRefetch }: StewardingTemplateProps) {
const [activeTab, setActiveTab] = useState<'pending' | 'history'>('pending');
const [selectedProtest, setSelectedProtest] = useState<any | null>(null);
const [expandedRaces, setExpandedRaces] = useState<Set<string>>(new Set());
const [showQuickPenaltyModal, setShowQuickPenaltyModal] = useState(false);
// Mutations using domain hook
const { acceptProtestMutation, rejectProtestMutation } = useLeagueStewardingMutations(onRefetch);
// Filter races based on active tab
const filteredRaces = useMemo(() => {
return activeTab === 'pending' ? data.racesWithData.filter(r => r.pendingProtests.length > 0) : data.racesWithData.filter(r => r.resolvedProtests.length > 0 || r.penalties.length > 0);
}, [data, activeTab]);
const handleAcceptProtest = async (
protestId: string,
penaltyType: string,
penaltyValue: number,
stewardNotes: string
) => {
// Find the protest to get details for penalty
let foundProtest: any | undefined;
data.racesWithData.forEach(raceData => {
const p = raceData.pendingProtests.find(pr => pr.id === protestId) ||
raceData.resolvedProtests.find(pr => pr.id === protestId);
if (p) foundProtest = { ...p, raceId: raceData.race.id };
});
if (foundProtest) {
acceptProtestMutation.mutate({
protestId,
penaltyType,
penaltyValue,
stewardNotes,
raceId: foundProtest.raceId,
accusedDriverId: foundProtest.accusedDriverId,
reason: foundProtest.incident.description,
});
}
};
const handleRejectProtest = async (protestId: string, stewardNotes: string) => {
rejectProtestMutation.mutate({
protestId,
stewardNotes,
});
};
const toggleRaceExpanded = (raceId: string) => {
setExpandedRaces(prev => {
const next = new Set(prev);
if (next.has(raceId)) {
next.delete(raceId);
} else {
next.add(raceId);
}
return next;
});
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'pending':
case 'under_review':
return <span className="px-2 py-0.5 text-xs font-medium bg-warning-amber/20 text-warning-amber rounded-full">Pending</span>;
case 'upheld':
return <span className="px-2 py-0.5 text-xs font-medium bg-red-500/20 text-red-400 rounded-full">Upheld</span>;
case 'dismissed':
return <span className="px-2 py-0.5 text-xs font-medium bg-gray-500/20 text-gray-400 rounded-full">Dismissed</span>;
case 'withdrawn':
return <span className="px-2 py-0.5 text-xs font-medium bg-blue-500/20 text-blue-400 rounded-full">Withdrawn</span>;
default:
return null;
}
};
return (
<div className="space-y-6">
<Card>
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-xl font-semibold text-white">Stewarding</h2>
<p className="text-sm text-gray-400 mt-1">
Quick overview of protests and penalties across all races
</p>
</div>
</div>
{/* Stats summary */}
<StewardingStats
totalPending={data.totalPending}
totalResolved={data.totalResolved}
totalPenalties={data.totalPenalties}
/>
{/* Tab navigation */}
<div className="border-b border-charcoal-outline mb-6">
<div className="flex gap-4">
<button
onClick={() => setActiveTab('pending')}
className={`pb-3 px-1 font-medium transition-colors ${
activeTab === 'pending'
? 'text-primary-blue border-b-2 border-primary-blue'
: 'text-gray-400 hover:text-white'
}`}
>
Pending Protests
{data.totalPending > 0 && (
<span className="ml-2 px-2 py-0.5 text-xs bg-warning-amber/20 text-warning-amber rounded-full">
{data.totalPending}
</span>
)}
</button>
<button
onClick={() => setActiveTab('history')}
className={`pb-3 px-1 font-medium transition-colors ${
activeTab === 'history'
? 'text-primary-blue border-b-2 border-primary-blue'
: 'text-gray-400 hover:text-white'
}`}
>
History
</button>
</div>
</div>
{/* Content */}
{filteredRaces.length === 0 ? (
<div className="text-center py-12">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-performance-green/10 flex items-center justify-center">
<Flag className="w-8 h-8 text-performance-green" />
</div>
<p className="font-semibold text-lg text-white mb-2">
{activeTab === 'pending' ? 'All Clear!' : 'No History Yet'}
</p>
<p className="text-sm text-gray-400">
{activeTab === 'pending'
? 'No pending protests to review'
: 'No resolved protests or penalties'}
</p>
</div>
) : (
<div className="space-y-4">
{filteredRaces.map(({ race, pendingProtests, resolvedProtests, penalties }) => {
const isExpanded = expandedRaces.has(race.id);
const displayProtests = activeTab === 'pending' ? pendingProtests : resolvedProtests;
return (
<div key={race.id} className="rounded-lg border border-charcoal-outline overflow-hidden">
{/* Race Header */}
<button
onClick={() => toggleRaceExpanded(race.id)}
className="w-full px-4 py-3 bg-iron-gray/30 hover:bg-iron-gray/50 transition-colors flex items-center justify-between"
>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4 text-gray-400" />
<span className="font-medium text-white">{race.track}</span>
</div>
<div className="flex items-center gap-2 text-gray-400 text-sm">
<Calendar className="w-4 h-4" />
<span>{race.scheduledAt.toLocaleDateString()}</span>
</div>
{activeTab === 'pending' && pendingProtests.length > 0 && (
<span className="px-2 py-0.5 text-xs font-medium bg-warning-amber/20 text-warning-amber rounded-full">
{pendingProtests.length} pending
</span>
)}
{activeTab === 'history' && (
<span className="px-2 py-0.5 text-xs font-medium bg-gray-500/20 text-gray-400 rounded-full">
{resolvedProtests.length} protests, {penalties.length} penalties
</span>
)}
</div>
<ChevronRight className={`w-5 h-5 text-gray-400 transition-transform ${isExpanded ? 'rotate-90' : ''}`} />
</button>
{/* Expanded Content */}
{isExpanded && (
<div className="p-4 space-y-3 bg-deep-graphite/50">
{displayProtests.length === 0 && penalties.length === 0 ? (
<p className="text-sm text-gray-400 text-center py-4">No items to display</p>
) : (
<>
{displayProtests.map((protest) => {
const protester = data.driverMap[protest.protestingDriverId];
const accused = data.driverMap[protest.accusedDriverId];
const daysSinceFiled = Math.floor((Date.now() - new Date(protest.filedAt).getTime()) / (1000 * 60 * 60 * 24));
const isUrgent = daysSinceFiled > 2 && (protest.status === 'pending' || protest.status === 'under_review');
return (
<div
key={protest.id}
className={`rounded-lg border border-charcoal-outline bg-iron-gray/30 p-4 ${isUrgent ? 'border-l-4 border-l-red-500' : ''}`}
>
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
<AlertCircle className="w-4 h-4 text-warning-amber flex-shrink-0" />
<span className="font-medium text-white">
{protester?.name || 'Unknown'} vs {accused?.name || 'Unknown'}
</span>
{getStatusBadge(protest.status)}
{isUrgent && (
<span className="px-2 py-0.5 text-xs font-medium bg-red-500/20 text-red-400 rounded-full flex items-center gap-1">
<AlertTriangle className="w-3 h-3" />
{daysSinceFiled}d old
</span>
)}
</div>
<div className="flex items-center gap-4 text-sm text-gray-400 mb-2">
<span>Lap {protest.incident.lap}</span>
<span></span>
<span>Filed {new Date(protest.filedAt).toLocaleDateString()}</span>
{protest.proofVideoUrl && (
<>
<span></span>
<span className="flex items-center gap-1 text-primary-blue">
<Video className="w-3 h-3" />
Video
</span>
</>
)}
</div>
<p className="text-sm text-gray-300 line-clamp-2">
{protest.incident.description}
</p>
{protest.decisionNotes && (
<div className="mt-2 p-2 rounded bg-iron-gray/50 border border-charcoal-outline/50">
<p className="text-xs text-gray-400">
<span className="font-medium">Steward:</span> {protest.decisionNotes}
</p>
</div>
)}
</div>
{(protest.status === 'pending' || protest.status === 'under_review') && (
<Link href={`/leagues/${leagueId}/stewarding/protests/${protest.id}`}>
<Button variant="primary">
Review
</Button>
</Link>
)}
</div>
</div>
);
})}
{activeTab === 'history' && penalties.map((penalty) => {
const driver = data.driverMap[penalty.driverId];
return (
<div
key={penalty.id}
className="rounded-lg border border-charcoal-outline bg-iron-gray/30 p-4"
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-red-500/20 flex items-center justify-center flex-shrink-0">
<Gavel className="w-4 h-4 text-red-400" />
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium text-white">{driver?.name || 'Unknown'}</span>
<span className="px-2 py-0.5 text-xs font-medium bg-red-500/20 text-red-400 rounded-full">
{penalty.type.replace('_', ' ')}
</span>
</div>
<p className="text-sm text-gray-400">{penalty.reason}</p>
</div>
<div className="text-right">
<span className="text-lg font-bold text-red-400">
{penalty.type === 'time_penalty' && `+${penalty.value}s`}
{penalty.type === 'grid_penalty' && `+${penalty.value} grid`}
{penalty.type === 'points_deduction' && `-${penalty.value} pts`}
{penalty.type === 'disqualification' && 'DSQ'}
{penalty.type === 'warning' && 'Warning'}
{penalty.type === 'license_points' && `${penalty.value} LP`}
</span>
</div>
</div>
</div>
);
})}
</>
)}
</div>
)}
</div>
);
})}
</div>
)}
</Card>
{activeTab === 'history' && (
<PenaltyFAB onClick={() => setShowQuickPenaltyModal(true)} />
)}
{selectedProtest && (
<ReviewProtestModal
protest={selectedProtest}
onClose={() => setSelectedProtest(null)}
onAccept={handleAcceptProtest}
onReject={handleRejectProtest}
/>
)}
{showQuickPenaltyModal && (
<QuickPenaltyModal
drivers={data.allDrivers}
onClose={() => setShowQuickPenaltyModal(false)}
adminId={currentDriverId || ''}
races={data.racesWithData.map(r => ({ id: r.race.id, track: r.race.track, scheduledAt: r.race.scheduledAt }))}
/>
)}
</div>
);
}

View File

@@ -1,134 +1,24 @@
'use client';
import PenaltyFAB from '@/components/leagues/PenaltyFAB';
import QuickPenaltyModal from '@/components/leagues/QuickPenaltyModal';
import { ReviewProtestModal } from '@/components/leagues/ReviewProtestModal';
import StewardingStats from '@/components/leagues/StewardingStats';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import { useCurrentDriver } from '@/hooks/driver/useCurrentDriver';
import { useLeagueAdminStatus } from '@/hooks/league/useLeagueAdminStatus';
import { useLeagueStewardingData } from '@/hooks/league/useLeagueStewardingData';
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
import {
AlertCircle,
AlertTriangle,
Calendar,
ChevronRight,
Flag,
Gavel,
MapPin,
Video
} from 'lucide-react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { useMemo, useState } from 'react';
// Shared state components
import { StateContainer } from '@/components/shared/state/StateContainer';
import { useLeagueStewardingMutations } from '@/hooks/league/useLeagueStewardingMutations';
import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper';
import { StewardingTemplate } from './StewardingTemplate';
import { LoadingWrapper } from '@/components/shared/state/LoadingWrapper';
import Card from '@/components/ui/Card';
import { AlertTriangle } from 'lucide-react';
import { useParams } from 'next/navigation';
export default function LeagueStewardingPage() {
const params = useParams();
const leagueId = params.id as string;
const { data: currentDriver } = useCurrentDriver();
const currentDriverId = currentDriver?.id;
const [activeTab, setActiveTab] = useState<'pending' | 'history'>('pending');
const [selectedProtest, setSelectedProtest] = useState<any | null>(null);
const [expandedRaces, setExpandedRaces] = useState<Set<string>>(new Set());
const [showQuickPenaltyModal, setShowQuickPenaltyModal] = useState(false);
const currentDriverId = currentDriver?.id || '';
// Check admin status
const { data: isAdmin, isLoading: adminLoading } = useLeagueAdminStatus(leagueId, currentDriverId || '');
// Load stewarding data (only if admin)
const { data: stewardingData, isLoading: dataLoading, error, retry } = useLeagueStewardingData(leagueId);
// Filter races based on active tab
const filteredRaces = useMemo(() => {
return activeTab === 'pending' ? stewardingData?.pendingRaces ?? [] : stewardingData?.historyRaces ?? [];
}, [stewardingData, activeTab]);
const handleAcceptProtest = async (
protestId: string,
penaltyType: string,
penaltyValue: number,
stewardNotes: string
) => {
// Find the protest to get details for penalty
let foundProtest: any | undefined;
stewardingData?.racesWithData.forEach(raceData => {
const p = raceData.pendingProtests.find(pr => pr.id === protestId) ||
raceData.resolvedProtests.find(pr => pr.id === protestId);
if (p) foundProtest = { ...p, raceId: raceData.race.id };
});
if (foundProtest) {
// TODO: Implement protest review and penalty application
// await leagueStewardingService.reviewProtest({
// protestId,
// stewardId: currentDriverId,
// decision: 'uphold',
// decisionNotes: stewardNotes,
// });
// await leagueStewardingService.applyPenalty({
// raceId: foundProtest.raceId,
// driverId: foundProtest.accusedDriverId,
// stewardId: currentDriverId,
// type: penaltyType,
// value: penaltyValue,
// reason: foundProtest.incident.description,
// protestId,
// notes: stewardNotes,
// });
}
// Retry to refresh data
await retry();
};
const handleRejectProtest = async (protestId: string, stewardNotes: string) => {
// TODO: Implement protest rejection
// await leagueStewardingService.reviewProtest({
// protestId,
// stewardId: currentDriverId,
// decision: 'dismiss',
// decisionNotes: stewardNotes,
// });
// Retry to refresh data
await retry();
};
const toggleRaceExpanded = (raceId: string) => {
setExpandedRaces(prev => {
const next = new Set(prev);
if (next.has(raceId)) {
next.delete(raceId);
} else {
next.add(raceId);
}
return next;
});
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'pending':
case 'under_review':
return <span className="px-2 py-0.5 text-xs font-medium bg-warning-amber/20 text-warning-amber rounded-full">Pending</span>;
case 'upheld':
return <span className="px-2 py-0.5 text-xs font-medium bg-red-500/20 text-red-400 rounded-full">Upheld</span>;
case 'dismissed':
return <span className="px-2 py-0.5 text-xs font-medium bg-gray-500/20 text-gray-400 rounded-full">Dismissed</span>;
case 'withdrawn':
return <span className="px-2 py-0.5 text-xs font-medium bg-blue-500/20 text-blue-400 rounded-full">Withdrawn</span>;
default:
return null;
}
};
const { data: isAdmin, isLoading: adminLoading } = useLeagueAdminStatus(leagueId, currentDriverId);
// Show loading for admin check
if (adminLoading) {
@@ -152,265 +42,30 @@ export default function LeagueStewardingPage() {
);
}
// Load stewarding data using domain hook
const { data, isLoading, error, refetch } = useLeagueStewardingData(leagueId);
return (
<StateContainer
data={stewardingData}
isLoading={dataLoading}
<StatefulPageWrapper
data={data}
isLoading={isLoading}
error={error}
retry={retry}
config={{
loading: { variant: 'spinner', message: 'Loading stewarding data...' },
error: { variant: 'full-screen' },
empty: {
icon: Flag,
title: 'No stewarding data',
description: 'There are no protests or penalties to review.',
}
retry={refetch}
Template={({ data }) => (
<StewardingTemplate
data={data}
leagueId={leagueId}
currentDriverId={currentDriverId}
onRefetch={refetch}
/>
)}
loading={{ variant: 'skeleton', message: 'Loading stewarding data...' }}
errorConfig={{ variant: 'full-screen' }}
empty={{
icon: require('lucide-react').Flag,
title: 'No stewarding data',
description: 'There are no protests or penalties to review.',
}}
>
{(data) => {
if (!data) return null;
return (
<div className="space-y-6">
<Card>
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-xl font-semibold text-white">Stewarding</h2>
<p className="text-sm text-gray-400 mt-1">
Quick overview of protests and penalties across all races
</p>
</div>
</div>
{/* Stats summary */}
<StewardingStats
totalPending={data.totalPending}
totalResolved={data.totalResolved}
totalPenalties={data.totalPenalties}
/>
{/* Tab navigation */}
<div className="border-b border-charcoal-outline mb-6">
<div className="flex gap-4">
<button
onClick={() => setActiveTab('pending')}
className={`pb-3 px-1 font-medium transition-colors ${
activeTab === 'pending'
? 'text-primary-blue border-b-2 border-primary-blue'
: 'text-gray-400 hover:text-white'
}`}
>
Pending Protests
{data.totalPending > 0 && (
<span className="ml-2 px-2 py-0.5 text-xs bg-warning-amber/20 text-warning-amber rounded-full">
{data.totalPending}
</span>
)}
</button>
<button
onClick={() => setActiveTab('history')}
className={`pb-3 px-1 font-medium transition-colors ${
activeTab === 'history'
? 'text-primary-blue border-b-2 border-primary-blue'
: 'text-gray-400 hover:text-white'
}`}
>
History
</button>
</div>
</div>
{/* Content */}
{filteredRaces.length === 0 ? (
<div className="text-center py-12">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-performance-green/10 flex items-center justify-center">
<Flag className="w-8 h-8 text-performance-green" />
</div>
<p className="font-semibold text-lg text-white mb-2">
{activeTab === 'pending' ? 'All Clear!' : 'No History Yet'}
</p>
<p className="text-sm text-gray-400">
{activeTab === 'pending'
? 'No pending protests to review'
: 'No resolved protests or penalties'}
</p>
</div>
) : (
<div className="space-y-4">
{filteredRaces.map(({ race, pendingProtests, resolvedProtests, penalties }) => {
const isExpanded = expandedRaces.has(race.id);
const displayProtests = activeTab === 'pending' ? pendingProtests : resolvedProtests;
return (
<div key={race.id} className="rounded-lg border border-charcoal-outline overflow-hidden">
{/* Race Header */}
<button
onClick={() => toggleRaceExpanded(race.id)}
className="w-full px-4 py-3 bg-iron-gray/30 hover:bg-iron-gray/50 transition-colors flex items-center justify-between"
>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4 text-gray-400" />
<span className="font-medium text-white">{race.track}</span>
</div>
<div className="flex items-center gap-2 text-gray-400 text-sm">
<Calendar className="w-4 h-4" />
<span>{race.scheduledAt.toLocaleDateString()}</span>
</div>
{activeTab === 'pending' && pendingProtests.length > 0 && (
<span className="px-2 py-0.5 text-xs font-medium bg-warning-amber/20 text-warning-amber rounded-full">
{pendingProtests.length} pending
</span>
)}
{activeTab === 'history' && (
<span className="px-2 py-0.5 text-xs font-medium bg-gray-500/20 text-gray-400 rounded-full">
{resolvedProtests.length} protests, {penalties.length} penalties
</span>
)}
</div>
<ChevronRight className={`w-5 h-5 text-gray-400 transition-transform ${isExpanded ? 'rotate-90' : ''}`} />
</button>
{/* Expanded Content */}
{isExpanded && (
<div className="p-4 space-y-3 bg-deep-graphite/50">
{displayProtests.length === 0 && penalties.length === 0 ? (
<p className="text-sm text-gray-400 text-center py-4">No items to display</p>
) : (
<>
{displayProtests.map((protest) => {
const protester = data.driverMap[protest.protestingDriverId];
const accused = data.driverMap[protest.accusedDriverId];
const daysSinceFiled = Math.floor((Date.now() - new Date(protest.filedAt).getTime()) / (1000 * 60 * 60 * 24));
const isUrgent = daysSinceFiled > 2 && (protest.status === 'pending' || protest.status === 'under_review');
return (
<div
key={protest.id}
className={`rounded-lg border border-charcoal-outline bg-iron-gray/30 p-4 ${isUrgent ? 'border-l-4 border-l-red-500' : ''}`}
>
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
<AlertCircle className="w-4 h-4 text-warning-amber flex-shrink-0" />
<span className="font-medium text-white">
{protester?.name || 'Unknown'} vs {accused?.name || 'Unknown'}
</span>
{getStatusBadge(protest.status)}
{isUrgent && (
<span className="px-2 py-0.5 text-xs font-medium bg-red-500/20 text-red-400 rounded-full flex items-center gap-1">
<AlertTriangle className="w-3 h-3" />
{daysSinceFiled}d old
</span>
)}
</div>
<div className="flex items-center gap-4 text-sm text-gray-400 mb-2">
<span>Lap {protest.incident.lap}</span>
<span></span>
<span>Filed {new Date(protest.filedAt).toLocaleDateString()}</span>
{protest.proofVideoUrl && (
<>
<span></span>
<span className="flex items-center gap-1 text-primary-blue">
<Video className="w-3 h-3" />
Video
</span>
</>
)}
</div>
<p className="text-sm text-gray-300 line-clamp-2">
{protest.incident.description}
</p>
{protest.decisionNotes && (
<div className="mt-2 p-2 rounded bg-iron-gray/50 border border-charcoal-outline/50">
<p className="text-xs text-gray-400">
<span className="font-medium">Steward:</span> {protest.decisionNotes}
</p>
</div>
)}
</div>
{(protest.status === 'pending' || protest.status === 'under_review') && (
<Link href={`/leagues/${leagueId}/stewarding/protests/${protest.id}`}>
<Button variant="primary">
Review
</Button>
</Link>
)}
</div>
</div>
);
})}
{activeTab === 'history' && penalties.map((penalty) => {
const driver = data.driverMap[penalty.driverId];
return (
<div
key={penalty.id}
className="rounded-lg border border-charcoal-outline bg-iron-gray/30 p-4"
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-red-500/20 flex items-center justify-center flex-shrink-0">
<Gavel className="w-4 h-4 text-red-400" />
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium text-white">{driver?.name || 'Unknown'}</span>
<span className="px-2 py-0.5 text-xs font-medium bg-red-500/20 text-red-400 rounded-full">
{penalty.type.replace('_', ' ')}
</span>
</div>
<p className="text-sm text-gray-400">{penalty.reason}</p>
</div>
<div className="text-right">
<span className="text-lg font-bold text-red-400">
{penalty.type === 'time_penalty' && `+${penalty.value}s`}
{penalty.type === 'grid_penalty' && `+${penalty.value} grid`}
{penalty.type === 'points_deduction' && `-${penalty.value} pts`}
{penalty.type === 'disqualification' && 'DSQ'}
{penalty.type === 'warning' && 'Warning'}
{penalty.type === 'license_points' && `${penalty.value} LP`}
</span>
</div>
</div>
</div>
);
})}
</>
)}
</div>
)}
</div>
);
})}
</div>
)}
</Card>
{activeTab === 'history' && (
<PenaltyFAB onClick={() => setShowQuickPenaltyModal(true)} />
)}
{selectedProtest && (
<ReviewProtestModal
protest={selectedProtest}
onClose={() => setSelectedProtest(null)}
onAccept={handleAcceptProtest}
onReject={handleRejectProtest}
/>
)}
{showQuickPenaltyModal && stewardingData && (
<QuickPenaltyModal
drivers={stewardingData.allDrivers}
onClose={() => setShowQuickPenaltyModal(false)}
adminId={currentDriverId || ''}
races={stewardingData.racesWithData.map(r => ({ id: r.race.id, track: r.race.track, scheduledAt: r.race.scheduledAt }))}
/>
)}
</div>
);
}}
</StateContainer>
/>
);
}

View File

@@ -156,7 +156,7 @@ export default function ProtestReviewPage() {
}, [penaltyTypes, penaltyType]);
const handleSubmitDecision = async () => {
if (!decision || !stewardNotes.trim() || !detail) return;
if (!decision || !stewardNotes.trim() || !detail || !currentDriverId) return;
setSubmitting(true);
try {
@@ -241,7 +241,7 @@ export default function ProtestReviewPage() {
};
const handleRequestDefense = async () => {
if (!detail) return;
if (!detail || !currentDriverId) return;
try {
// Request defense
@@ -734,4 +734,4 @@ export default function ProtestReviewPage() {
}}
</StateContainer>
);
}
}