di usage in website

This commit is contained in:
2026-01-06 19:36:03 +01:00
parent 589b55a87e
commit e589c30bf8
191 changed files with 6367 additions and 4253 deletions

View File

@@ -5,7 +5,7 @@ import Modal from '@/components/ui/Modal';
import Button from '@/components/ui/Button';
import type { FileProtestCommandDTO } from '@/lib/types/generated/FileProtestCommandDTO';
import type { ProtestIncidentDTO } from '@/lib/types/generated/ProtestIncidentDTO';
import { useServices } from '@/lib/services/ServiceProvider';
import { useFileProtest } from '@/hooks/race/useFileProtest';
import {
AlertTriangle,
Video,
@@ -39,8 +39,7 @@ export default function FileProtestModal({
protestingDriverId,
participants,
}: FileProtestModalProps) {
const { raceService } = useServices();
const [step, setStep] = useState<'form' | 'submitting' | 'success' | 'error'>('form');
const fileProtestMutation = useFileProtest();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
// Form state
@@ -68,37 +67,41 @@ export default function FileProtestModal({
return;
}
setStep('submitting');
setErrorMessage(null);
try {
const incident: ProtestIncidentDTO = {
lap: parseInt(lap, 10),
description: description.trim(),
...(timeInRace ? { timeInRace: parseInt(timeInRace, 10) } : {}),
};
const incident: ProtestIncidentDTO = {
lap: parseInt(lap, 10),
description: description.trim(),
...(timeInRace ? { timeInRace: parseInt(timeInRace, 10) } : {}),
};
const command = {
raceId,
protestingDriverId,
accusedDriverId,
incident,
...(comment.trim() ? { comment: comment.trim() } : {}),
...(proofVideoUrl.trim() ? { proofVideoUrl: proofVideoUrl.trim() } : {}),
} satisfies FileProtestCommandDTO;
const command = {
raceId,
protestingDriverId,
accusedDriverId,
incident,
...(comment.trim() ? { comment: comment.trim() } : {}),
...(proofVideoUrl.trim() ? { proofVideoUrl: proofVideoUrl.trim() } : {}),
} satisfies FileProtestCommandDTO;
await raceService.fileProtest(command);
setStep('success');
} catch (err) {
setStep('error');
setErrorMessage(err instanceof Error ? err.message : 'Failed to file protest');
}
fileProtestMutation.mutate(command, {
onSuccess: () => {
// Reset form state on success
setAccusedDriverId('');
setLap('');
setTimeInRace('');
setDescription('');
setComment('');
setProofVideoUrl('');
},
onError: (error) => {
setErrorMessage(error.message || 'Failed to file protest');
},
});
};
const handleClose = () => {
// Reset form state
setStep('form');
setErrorMessage(null);
setAccusedDriverId('');
setLap('');
@@ -106,10 +109,12 @@ export default function FileProtestModal({
setDescription('');
setComment('');
setProofVideoUrl('');
fileProtestMutation.reset();
onClose();
};
if (step === 'success') {
// Show success state when mutation is successful
if (fileProtestMutation.isSuccess) {
return (
<Modal
isOpen={isOpen}
@@ -122,7 +127,7 @@ export default function FileProtestModal({
</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.
The stewards will review your protest and make a decision.
You'll be notified of the outcome.
</p>
<Button variant="primary" onClick={handleClose}>
@@ -157,7 +162,7 @@ export default function FileProtestModal({
<select
value={accusedDriverId}
onChange={(e) => setAccusedDriverId(e.target.value)}
disabled={step === 'submitting'}
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>
@@ -181,7 +186,7 @@ export default function FileProtestModal({
min="0"
value={lap}
onChange={(e) => setLap(e.target.value)}
disabled={step === 'submitting'}
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"
/>
@@ -196,13 +201,13 @@ export default function FileProtestModal({
min="0"
value={timeInRace}
onChange={(e) => setTimeInRace(e.target.value)}
disabled={step === 'submitting'}
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">
@@ -212,13 +217,13 @@ export default function FileProtestModal({
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
disabled={step === 'submitting'}
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">
@@ -228,13 +233,13 @@ export default function FileProtestModal({
<textarea
value={comment}
onChange={(e) => setComment(e.target.value)}
disabled={step === 'submitting'}
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">
@@ -245,7 +250,7 @@ export default function FileProtestModal({
type="url"
value={proofVideoUrl}
onChange={(e) => setProofVideoUrl(e.target.value)}
disabled={step === 'submitting'}
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"
/>
@@ -253,22 +258,22 @@ export default function FileProtestModal({
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
<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={step === 'submitting'}
disabled={fileProtestMutation.isPending}
className="flex-1"
>
Cancel
@@ -276,10 +281,10 @@ export default function FileProtestModal({
<Button
variant="primary"
onClick={handleSubmit}
disabled={step === 'submitting'}
disabled={fileProtestMutation.isPending}
className="flex-1"
>
{step === 'submitting' ? 'Submitting...' : 'Submit Protest'}
{fileProtestMutation.isPending ? 'Submitting...' : 'Submit Protest'}
</Button>
</div>
</div>