wip
This commit is contained in:
57
apps/website/components/leagues/PenaltyCardMenu.tsx
Normal file
57
apps/website/components/leagues/PenaltyCardMenu.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { MoreVertical, Edit, Trash2 } from 'lucide-react';
|
||||
import Button from '../ui/Button';
|
||||
|
||||
interface PenaltyCardMenuProps {
|
||||
onEdit: () => void;
|
||||
onVoid: () => void;
|
||||
}
|
||||
|
||||
export default function PenaltyCardMenu({ onEdit, onVoid }: PenaltyCardMenuProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="p-2 w-8 h-8"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<MoreVertical className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
{isOpen && (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 z-10"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
<div className="absolute right-0 mt-1 w-32 bg-deep-graphite border border-charcoal-outline rounded-lg shadow-lg z-20">
|
||||
<button
|
||||
onClick={() => {
|
||||
onEdit();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className="w-full px-3 py-2 text-left text-sm text-white hover:bg-iron-gray/50 flex items-center gap-2"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
onVoid();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className="w-full px-3 py-2 text-left text-sm text-red-400 hover:bg-iron-gray/50 flex items-center gap-2"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
Void
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
apps/website/components/leagues/PenaltyFAB.tsx
Normal file
23
apps/website/components/leagues/PenaltyFAB.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { Plus } from 'lucide-react';
|
||||
import Button from '../ui/Button';
|
||||
|
||||
interface PenaltyFABProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export default function PenaltyFAB({ onClick }: PenaltyFABProps) {
|
||||
return (
|
||||
<div className="fixed bottom-6 right-6 z-50">
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-14 h-14 rounded-full shadow-lg"
|
||||
onClick={onClick}
|
||||
title="Add Penalty"
|
||||
>
|
||||
<Plus className="w-6 h-6" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,14 +3,21 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { getQuickPenaltyUseCase } from '@/lib/di-container';
|
||||
import type { Driver } from '@gridpilot/racing/application';
|
||||
import Button from '@/components/ui/Button';
|
||||
import { AlertTriangle, Clock, Flag, Zap } from 'lucide-react';
|
||||
|
||||
interface DriverOption {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface QuickPenaltyModalProps {
|
||||
raceId: string;
|
||||
drivers: Driver[];
|
||||
raceId?: string;
|
||||
drivers: DriverOption[];
|
||||
onClose: () => void;
|
||||
preSelectedDriver?: DriverOption;
|
||||
adminId: string;
|
||||
races?: { id: string; track: string; scheduledAt: Date }[];
|
||||
}
|
||||
|
||||
const INFRACTION_TYPES = [
|
||||
@@ -28,8 +35,9 @@ const SEVERITY_LEVELS = [
|
||||
{ value: 'severe', label: 'Severe', description: 'Heavy penalty' },
|
||||
] as const;
|
||||
|
||||
export default function QuickPenaltyModal({ raceId, drivers, onClose }: QuickPenaltyModalProps) {
|
||||
const [selectedDriver, setSelectedDriver] = useState<string>('');
|
||||
export default function QuickPenaltyModal({ raceId, drivers, onClose, preSelectedDriver, adminId, races }: QuickPenaltyModalProps) {
|
||||
const [selectedRaceId, setSelectedRaceId] = useState<string>(raceId || '');
|
||||
const [selectedDriver, setSelectedDriver] = useState<string>(preSelectedDriver?.id || '');
|
||||
const [infractionType, setInfractionType] = useState<string>('');
|
||||
const [severity, setSeverity] = useState<string>('');
|
||||
const [notes, setNotes] = useState<string>('');
|
||||
@@ -39,21 +47,24 @@ export default function QuickPenaltyModal({ raceId, drivers, onClose }: QuickPen
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!selectedDriver || !infractionType || !severity) return;
|
||||
if (!selectedRaceId || !selectedDriver || !infractionType || !severity) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const useCase = getQuickPenaltyUseCase();
|
||||
await useCase.execute({
|
||||
raceId,
|
||||
const command: any = {
|
||||
raceId: selectedRaceId,
|
||||
driverId: selectedDriver,
|
||||
adminId: 'driver-1', // TODO: Get from current user context
|
||||
adminId,
|
||||
infractionType: infractionType as any,
|
||||
severity: severity as any,
|
||||
notes: notes.trim() || undefined,
|
||||
});
|
||||
};
|
||||
if (notes.trim()) {
|
||||
command.notes = notes.trim();
|
||||
}
|
||||
await useCase.execute(command);
|
||||
|
||||
// Refresh the page to show updated results
|
||||
router.refresh();
|
||||
@@ -72,24 +83,52 @@ export default function QuickPenaltyModal({ raceId, drivers, onClose }: QuickPen
|
||||
<h2 className="text-xl font-bold text-white mb-4">Quick Penalty</h2>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Race Selection */}
|
||||
{races && !raceId && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Race
|
||||
</label>
|
||||
<select
|
||||
value={selectedRaceId}
|
||||
onChange={(e) => setSelectedRaceId(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white focus:border-primary-blue focus:outline-none"
|
||||
required
|
||||
>
|
||||
<option value="">Select race...</option>
|
||||
{races.map((race) => (
|
||||
<option key={race.id} value={race.id}>
|
||||
{race.track} ({race.scheduledAt.toLocaleDateString()})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Driver Selection */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Driver
|
||||
</label>
|
||||
<select
|
||||
value={selectedDriver}
|
||||
onChange={(e) => setSelectedDriver(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white focus:border-primary-blue focus:outline-none"
|
||||
required
|
||||
>
|
||||
<option value="">Select driver...</option>
|
||||
{drivers.map((driver) => (
|
||||
<option key={driver.id} value={driver.id}>
|
||||
{driver.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{preSelectedDriver ? (
|
||||
<div className="w-full px-3 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white">
|
||||
{preSelectedDriver.name}
|
||||
</div>
|
||||
) : (
|
||||
<select
|
||||
value={selectedDriver}
|
||||
onChange={(e) => setSelectedDriver(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white focus:border-primary-blue focus:outline-none"
|
||||
required
|
||||
>
|
||||
<option value="">Select driver...</option>
|
||||
{drivers.map((driver) => (
|
||||
<option key={driver.id} value={driver.id}>
|
||||
{driver.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Infraction Type */}
|
||||
@@ -175,7 +214,7 @@ export default function QuickPenaltyModal({ raceId, drivers, onClose }: QuickPen
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="flex-1"
|
||||
disabled={loading || !selectedDriver || !infractionType || !severity}
|
||||
disabled={loading || !selectedRaceId || !selectedDriver || !infractionType || !severity}
|
||||
>
|
||||
{loading ? 'Applying...' : 'Apply Penalty'}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user