Files
gridpilot.gg/apps/website/components/races/FileProtestModal.tsx
2026-01-14 23:46:04 +01:00

281 lines
10 KiB
TypeScript

'use client';
import { useState } from 'react';
import Modal from '@/ui/Modal';
import Button from '@/ui/Button';
import type { FileProtestCommandDTO } from '@/lib/types/generated/FileProtestCommandDTO';
import { useFileProtest } from "@/lib/hooks/race/useFileProtest";
import {
AlertTriangle,
Video,
MessageSquare,
Hash,
Clock,
User,
FileText,
CheckCircle2,
} from 'lucide-react';
import { useInject } from '@/lib/di/hooks/useInject';
import { PROTEST_SERVICE_TOKEN } from '@/lib/di/tokens';
import type { ProtestParticipant } from '@/lib/services/protests/ProtestService';
interface FileProtestModalProps {
isOpen: boolean;
onClose: () => void;
raceId: string;
leagueId?: string;
protestingDriverId: string;
participants: ProtestParticipant[];
}
export default function FileProtestModal({
isOpen,
onClose,
raceId,
leagueId,
protestingDriverId,
participants,
}: FileProtestModalProps) {
const fileProtestMutation = useFileProtest();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const protestService = useInject(PROTEST_SERVICE_TOKEN);
// 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 () => {
try {
// Use ProtestService for validation and command construction
const command = protestService.constructFileProtestCommand({
raceId,
leagueId,
protestingDriverId,
accusedDriverId,
lap,
timeInRace,
description,
comment,
proofVideoUrl,
});
setErrorMessage(null);
// Use existing hook for the actual API call
fileProtestMutation.mutate(command, {
onSuccess: () => {
// Reset form state on success
setAccusedDriverId('');
setLap('');
setTimeInRace('');
setDescription('');
setComment('');
setProofVideoUrl('');
},
onError: (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"
>
<div className="flex flex-col items-center py-6 text-center">
<div className="p-4 bg-performance-green/20 rounded-full mb-4">
<CheckCircle2 className="w-8 h-8 text-performance-green" />
</div>
<p className="text-white font-medium mb-2">Your protest has been submitted</p>
<p className="text-sm text-gray-400 mb-6">
The stewards will review your protest and make a decision.
You'll be notified of the outcome.
</p>
<Button variant="primary" onClick={handleClose}>
Close
</Button>
</div>
</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."
>
<div className="space-y-5">
{errorMessage && (
<div className="flex items-start gap-3 p-3 bg-warning-amber/10 border border-warning-amber/30 rounded-lg">
<AlertTriangle className="w-5 h-5 text-warning-amber flex-shrink-0 mt-0.5" />
<p className="text-sm text-warning-amber">{errorMessage}</p>
</div>
)}
{/* Driver Selection */}
<div>
<label className="flex items-center gap-2 text-sm font-medium text-gray-300 mb-2">
<User className="w-4 h-4 text-primary-blue" />
Driver involved *
</label>
<select
value={accusedDriverId}
onChange={(e) => setAccusedDriverId(e.target.value)}
disabled={fileProtestMutation.isPending}
className="w-full px-3 py-2.5 bg-deep-graphite border border-charcoal-outline rounded-lg text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-blue/50 focus:border-primary-blue disabled:opacity-50"
>
<option value="">Select driver...</option>
{otherParticipants.map((driver) => (
<option key={driver.id} value={driver.id}>
{driver.name}
</option>
))}
</select>
</div>
{/* Lap and Time */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="flex items-center gap-2 text-sm font-medium text-gray-300 mb-2">
<Hash className="w-4 h-4 text-primary-blue" />
Lap number *
</label>
<input
type="number"
min="0"
value={lap}
onChange={(e) => setLap(e.target.value)}
disabled={fileProtestMutation.isPending}
placeholder="e.g. 5"
className="w-full px-3 py-2.5 bg-deep-graphite border border-charcoal-outline rounded-lg text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-blue/50 focus:border-primary-blue disabled:opacity-50"
/>
</div>
<div>
<label className="flex items-center gap-2 text-sm font-medium text-gray-300 mb-2">
<Clock className="w-4 h-4 text-primary-blue" />
Time (seconds)
</label>
<input
type="number"
min="0"
value={timeInRace}
onChange={(e) => setTimeInRace(e.target.value)}
disabled={fileProtestMutation.isPending}
placeholder="Optional"
className="w-full px-3 py-2.5 bg-deep-graphite border border-charcoal-outline rounded-lg text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-blue/50 focus:border-primary-blue disabled:opacity-50"
/>
</div>
</div>
{/* Incident Description */}
<div>
<label className="flex items-center gap-2 text-sm font-medium text-gray-300 mb-2">
<FileText className="w-4 h-4 text-primary-blue" />
What happened? *
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
disabled={fileProtestMutation.isPending}
placeholder="Describe the incident clearly and objectively..."
rows={3}
className="w-full px-3 py-2.5 bg-deep-graphite border border-charcoal-outline rounded-lg text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-blue/50 focus:border-primary-blue disabled:opacity-50 resize-none"
/>
</div>
{/* Additional Comment */}
<div>
<label className="flex items-center gap-2 text-sm font-medium text-gray-300 mb-2">
<MessageSquare className="w-4 h-4 text-primary-blue" />
Additional comment
</label>
<textarea
value={comment}
onChange={(e) => setComment(e.target.value)}
disabled={fileProtestMutation.isPending}
placeholder="Any additional context for the stewards..."
rows={2}
className="w-full px-3 py-2.5 bg-deep-graphite border border-charcoal-outline rounded-lg text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-blue/50 focus:border-primary-blue disabled:opacity-50 resize-none"
/>
</div>
{/* Video Proof */}
<div>
<label className="flex items-center gap-2 text-sm font-medium text-gray-300 mb-2">
<Video className="w-4 h-4 text-primary-blue" />
Video proof URL
</label>
<input
type="url"
value={proofVideoUrl}
onChange={(e) => setProofVideoUrl(e.target.value)}
disabled={fileProtestMutation.isPending}
placeholder="https://youtube.com/... or https://streamable.com/..."
className="w-full px-3 py-2.5 bg-deep-graphite border border-charcoal-outline rounded-lg text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-blue/50 focus:border-primary-blue disabled:opacity-50"
/>
<p className="mt-1.5 text-xs text-gray-500">
Providing video evidence significantly helps the stewards review your protest.
</p>
</div>
{/* Info Box */}
<div className="p-3 bg-iron-gray rounded-lg border border-charcoal-outline">
<p className="text-xs text-gray-400">
<strong className="text-gray-300">Note:</strong> 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.
</p>
</div>
{/* Actions */}
<div className="flex gap-3 pt-2">
<Button
variant="secondary"
onClick={handleClose}
disabled={fileProtestMutation.isPending}
className="flex-1"
>
Cancel
</Button>
<Button
variant="primary"
onClick={handleSubmit}
disabled={fileProtestMutation.isPending}
className="flex-1"
>
{fileProtestMutation.isPending ? 'Submitting...' : 'Submit Protest'}
</Button>
</div>
</div>
</Modal>
);
}