Files
gridpilot.gg/apps/website/components/races/FileProtestModal.tsx
2026-01-18 16:43:32 +01:00

251 lines
7.5 KiB
TypeScript

'use client';
import { useFileProtest } from "@/hooks/race/useFileProtest";
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { InfoBox } from '@/ui/InfoBox';
import { Input } from '@/ui/Input';
import { Modal } from '@/ui/Modal';
import { Stack } from '@/ui/primitives/Stack';
import { Select } from '@/ui/Select';
import { Text } from '@/ui/Text';
import { TextArea } from '@/ui/TextArea';
import {
AlertTriangle,
CheckCircle2,
} from 'lucide-react';
import { useState } from 'react';
export interface ProtestParticipant {
id: string;
name: string;
}
interface FileProtestModalProps {
isOpen: boolean;
onClose: () => void;
raceId: string;
leagueId?: string;
protestingDriverId: string;
participants: ProtestParticipant[];
}
export function FileProtestModal({
isOpen,
onClose,
raceId,
protestingDriverId,
participants,
}: FileProtestModalProps) {
const fileProtestMutation = useFileProtest();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
// Form state
const [accusedDriverId, setAccusedDriverId] = useState<string>('');
const [lap, setLap] = useState<string>('');
const [timeInRace, setTimeInRace] = useState<string>('');
const [description, setDescription] = useState<string>('');
const [comment, setComment] = useState<string>('');
const [proofVideoUrl, setProofVideoUrl] = useState<string>('');
const otherParticipants = participants.filter(p => p.id !== protestingDriverId);
const handleSubmit = async () => {
if (!accusedDriverId || !lap || !description) {
setErrorMessage('Please fill in all required fields');
return;
}
try {
setErrorMessage(null);
fileProtestMutation.mutate({
raceId,
protestingDriverId,
accusedDriverId,
incident: {
lap: parseInt(lap, 10),
description,
timeInRace: timeInRace ? parseInt(timeInRace, 10) : undefined,
},
comment,
proofVideoUrl,
}, {
onSuccess: () => {
// Reset form state on success
setAccusedDriverId('');
setLap('');
setTimeInRace('');
setDescription('');
setComment('');
setProofVideoUrl('');
},
onError: (error: Error) => {
setErrorMessage(error.message || 'Failed to file protest');
},
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to validate protest input';
setErrorMessage(errorMessage);
}
};
const handleClose = () => {
// Reset form state
setErrorMessage(null);
setAccusedDriverId('');
setLap('');
setTimeInRace('');
setDescription('');
setComment('');
setProofVideoUrl('');
fileProtestMutation.reset();
onClose();
};
// Show success state when mutation is successful
if (fileProtestMutation.isSuccess) {
return (
<Modal
isOpen={isOpen}
onOpenChange={handleClose}
title="Protest Filed Successfully"
>
<Stack display="flex" flexDirection="col" alignItems="center" py={6} textAlign="center">
<Stack p={4} bg="bg-performance-green/20" rounded="full" mb={4}>
<Icon icon={CheckCircle2} size={8} color="rgb(16, 185, 129)" />
</Stack>
<Text color="text-white" weight="medium" mb={2} block>Your protest has been submitted</Text>
<Text size="sm" color="text-gray-400" mb={6} block>
The stewards will review your protest and make a decision.
You&apos;ll be notified of the outcome.
</Text>
<Button variant="primary" onClick={handleClose}>
Close
</Button>
</Stack>
</Modal>
);
}
return (
<Modal
isOpen={isOpen}
onOpenChange={handleClose}
title="File a Protest"
description="Report an incident to the stewards for review. Please provide as much detail as possible."
>
<Stack gap={5}>
{errorMessage && (
<InfoBox
variant="warning"
icon={AlertTriangle}
title="Error"
description={errorMessage}
/>
)}
{/* Driver Selection */}
<Stack>
<Select
label="Driver involved *"
options={[
{ value: '', label: 'Select driver...' },
...otherParticipants.map(driver => ({ value: driver.id, label: driver.name }))
]}
value={accusedDriverId}
onChange={(e) => setAccusedDriverId(e.target.value)}
disabled={fileProtestMutation.isPending}
/>
</Stack>
{/* Lap and Time */}
<Stack display="grid" gridCols={2} gap={4}>
<Input
label="Lap number *"
type="number"
min="0"
value={lap}
onChange={(e) => setLap(e.target.value)}
disabled={fileProtestMutation.isPending}
placeholder="e.g. 5"
/>
<Input
label="Time (seconds)"
type="number"
min="0"
value={timeInRace}
onChange={(e) => setTimeInRace(e.target.value)}
disabled={fileProtestMutation.isPending}
placeholder="Optional"
/>
</Stack>
{/* Incident Description */}
<TextArea
label="What happened? *"
value={description}
onChange={(e) => setDescription(e.target.value)}
disabled={fileProtestMutation.isPending}
placeholder="Describe the incident clearly and objectively..."
rows={3}
/>
{/* Additional Comment */}
<TextArea
label="Additional comment"
value={comment}
onChange={(e) => setComment(e.target.value)}
disabled={fileProtestMutation.isPending}
placeholder="Any additional context for the stewards..."
rows={2}
/>
{/* Video Proof */}
<Stack>
<Input
label="Video proof URL"
type="url"
value={proofVideoUrl}
onChange={(e) => setProofVideoUrl(e.target.value)}
disabled={fileProtestMutation.isPending}
placeholder="https://youtube.com/... or https://streamable.com/..."
/>
<Text size="xs" color="text-gray-500" mt={1.5} block>
Providing video evidence significantly helps the stewards review your protest.
</Text>
</Stack>
{/* Info Box */}
<Stack p={3} bg="bg-iron-gray" rounded="lg" border borderColor="border-charcoal-outline">
<Text size="xs" color="text-gray-400" block>
<Text as="strong" color="text-gray-300">Note:</Text> Filing a protest does not guarantee action.
The stewards will review the incident and may apply penalties ranging from time penalties
to grid penalties for future races, depending on the severity.
</Text>
</Stack>
{/* Actions */}
<Stack display="flex" gap={3} pt={2}>
<Button
variant="secondary"
onClick={handleClose}
disabled={fileProtestMutation.isPending}
fullWidth
>
Cancel
</Button>
<Button
variant="primary"
onClick={handleSubmit}
disabled={fileProtestMutation.isPending}
fullWidth
>
{fileProtestMutation.isPending ? 'Submitting...' : 'Submit Protest'}
</Button>
</Stack>
</Stack>
</Modal>
);
}