252 lines
7.5 KiB
TypeScript
252 lines
7.5 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { Modal } from '@/ui/Modal';
|
|
import { Button } from '@/ui/Button';
|
|
import { Box } from '@/ui/Box';
|
|
import { Text } from '@/ui/Text';
|
|
import { Stack } from '@/ui/Stack';
|
|
import { Icon } from '@/ui/Icon';
|
|
import { InfoBox } from '@/ui/InfoBox';
|
|
import { Input } from '@/ui/Input';
|
|
import { TextArea } from '@/ui/TextArea';
|
|
import { Select } from '@/ui/Select';
|
|
import { useFileProtest } from "@/hooks/race/useFileProtest";
|
|
import {
|
|
AlertTriangle,
|
|
CheckCircle2,
|
|
} from 'lucide-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"
|
|
>
|
|
<Box display="flex" flexDirection="col" alignItems="center" py={6} textAlign="center">
|
|
<Box p={4} bg="bg-performance-green/20" rounded="full" mb={4}>
|
|
<Icon icon={CheckCircle2} size={8} color="rgb(16, 185, 129)" />
|
|
</Box>
|
|
<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'll be notified of the outcome.
|
|
</Text>
|
|
<Button variant="primary" onClick={handleClose}>
|
|
Close
|
|
</Button>
|
|
</Box>
|
|
</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 */}
|
|
<Box>
|
|
<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}
|
|
/>
|
|
</Box>
|
|
|
|
{/* Lap and Time */}
|
|
<Box 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"
|
|
/>
|
|
</Box>
|
|
|
|
{/* 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 */}
|
|
<Box>
|
|
<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>
|
|
</Box>
|
|
|
|
{/* Info Box */}
|
|
<Box 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>
|
|
</Box>
|
|
|
|
{/* Actions */}
|
|
<Box 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>
|
|
</Box>
|
|
</Stack>
|
|
</Modal>
|
|
);
|
|
}
|