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>
|
||||
|
||||
41
apps/website/components/races/InlinePenaltyButton.tsx
Normal file
41
apps/website/components/races/InlinePenaltyButton.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
import Button from '../ui/Button';
|
||||
|
||||
interface DriverDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface InlinePenaltyButtonProps {
|
||||
driver: DriverDTO;
|
||||
onPenaltyClick?: (driver: DriverDTO) => void;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
export default function InlinePenaltyButton({
|
||||
driver,
|
||||
onPenaltyClick,
|
||||
isAdmin,
|
||||
}: InlinePenaltyButtonProps) {
|
||||
if (!isAdmin) return null;
|
||||
|
||||
const handleButtonClick = () => {
|
||||
if (onPenaltyClick) {
|
||||
onPenaltyClick(driver);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="danger"
|
||||
className="p-1.5 min-h-[32px] w-8 h-8 rounded-full flex items-center justify-center"
|
||||
onClick={handleButtonClick}
|
||||
title={`Issue penalty to ${driver.name}`}
|
||||
>
|
||||
<AlertTriangle className="w-4 h-4 flex-shrink-0" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
import { AlertTriangle, ExternalLink } from 'lucide-react';
|
||||
import InlinePenaltyButton from './InlinePenaltyButton';
|
||||
|
||||
type PenaltyTypeDTO =
|
||||
| 'time_penalty'
|
||||
@@ -41,6 +42,8 @@ interface ResultsTableProps {
|
||||
fastestLapTime?: number | undefined;
|
||||
penalties?: PenaltyData[];
|
||||
currentDriverId?: string | undefined;
|
||||
isAdmin?: boolean;
|
||||
onPenaltyClick?: (driver: DriverDTO) => void;
|
||||
}
|
||||
|
||||
export default function ResultsTable({
|
||||
@@ -50,6 +53,8 @@ export default function ResultsTable({
|
||||
fastestLapTime,
|
||||
penalties = [],
|
||||
currentDriverId,
|
||||
isAdmin = false,
|
||||
onPenaltyClick,
|
||||
}: ResultsTableProps) {
|
||||
const getDriver = (driverId: string): DriverDTO | undefined => {
|
||||
return drivers.find((d) => d.id === driverId);
|
||||
@@ -118,6 +123,7 @@ export default function ResultsTable({
|
||||
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-400">Points</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-400">+/-</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-400">Penalties</th>
|
||||
{isAdmin && <th className="text-left py-3 px-4 text-sm font-semibold text-gray-400">Actions</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -246,6 +252,17 @@ export default function ResultsTable({
|
||||
<span className="text-gray-500">—</span>
|
||||
)}
|
||||
</td>
|
||||
{isAdmin && (
|
||||
<td className="py-3 px-4">
|
||||
{driver && onPenaltyClick && (
|
||||
<InlinePenaltyButton
|
||||
driver={driver}
|
||||
onPenaltyClick={onPenaltyClick}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user