wip
This commit is contained in:
@@ -1,13 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Button from '../ui/Button';
|
||||
import { Result } from '@gridpilot/racing/domain/entities/Result';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import Button from '../ui/Button';
|
||||
|
||||
interface ImportResultRowDTO {
|
||||
id: string;
|
||||
raceId: string;
|
||||
driverId: string;
|
||||
position: number;
|
||||
fastestLap: number;
|
||||
incidents: number;
|
||||
startPosition: number;
|
||||
}
|
||||
|
||||
interface ImportResultsFormProps {
|
||||
raceId: string;
|
||||
onSuccess: (results: Result[]) => void;
|
||||
onSuccess: (results: ImportResultRowDTO[]) => void;
|
||||
onError: (error: string) => void;
|
||||
}
|
||||
|
||||
@@ -25,36 +34,35 @@ export default function ImportResultsForm({ raceId, onSuccess, onError }: Import
|
||||
|
||||
const parseCSV = (content: string): CSVRow[] => {
|
||||
const lines = content.trim().split('\n');
|
||||
|
||||
|
||||
if (lines.length < 2) {
|
||||
throw new Error('CSV file is empty or invalid');
|
||||
}
|
||||
|
||||
// Parse header
|
||||
const header = lines[0].toLowerCase().split(',').map(h => h.trim());
|
||||
const header = lines[0].toLowerCase().split(',').map((h) => h.trim());
|
||||
const requiredFields = ['driverid', 'position', 'fastestlap', 'incidents', 'startposition'];
|
||||
|
||||
|
||||
for (const field of requiredFields) {
|
||||
if (!header.includes(field)) {
|
||||
throw new Error(`Missing required field: ${field}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse rows
|
||||
const rows: CSVRow[] = [];
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const values = lines[i].split(',').map(v => v.trim());
|
||||
|
||||
const values = lines[i].split(',').map((v) => v.trim());
|
||||
|
||||
if (values.length !== header.length) {
|
||||
throw new Error(`Invalid row ${i}: expected ${header.length} columns, got ${values.length}`);
|
||||
throw new Error(
|
||||
`Invalid row ${i}: expected ${header.length} columns, got ${values.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
const row: any = {};
|
||||
const row: Record<string, string> = {};
|
||||
header.forEach((field, index) => {
|
||||
row[field] = values[index];
|
||||
row[field] = values[index] ?? '';
|
||||
});
|
||||
|
||||
// Validate and convert types
|
||||
const driverId = row.driverid;
|
||||
const position = parseInt(row.position, 10);
|
||||
const fastestLap = parseFloat(row.fastestlap);
|
||||
@@ -65,34 +73,32 @@ export default function ImportResultsForm({ raceId, onSuccess, onError }: Import
|
||||
throw new Error(`Row ${i}: driverId is required`);
|
||||
}
|
||||
|
||||
if (isNaN(position) || position < 1) {
|
||||
if (Number.isNaN(position) || position < 1) {
|
||||
throw new Error(`Row ${i}: position must be a positive integer`);
|
||||
}
|
||||
|
||||
if (isNaN(fastestLap) || fastestLap < 0) {
|
||||
if (Number.isNaN(fastestLap) || fastestLap < 0) {
|
||||
throw new Error(`Row ${i}: fastestLap must be a non-negative number`);
|
||||
}
|
||||
|
||||
if (isNaN(incidents) || incidents < 0) {
|
||||
if (Number.isNaN(incidents) || incidents < 0) {
|
||||
throw new Error(`Row ${i}: incidents must be a non-negative integer`);
|
||||
}
|
||||
|
||||
if (isNaN(startPosition) || startPosition < 1) {
|
||||
if (Number.isNaN(startPosition) || startPosition < 1) {
|
||||
throw new Error(`Row ${i}: startPosition must be a positive integer`);
|
||||
}
|
||||
|
||||
rows.push({ driverId, position, fastestLap, incidents, startPosition });
|
||||
}
|
||||
|
||||
// Validate no duplicate positions
|
||||
const positions = rows.map(r => r.position);
|
||||
const positions = rows.map((r) => r.position);
|
||||
const uniquePositions = new Set(positions);
|
||||
if (positions.length !== uniquePositions.size) {
|
||||
throw new Error('Duplicate positions found in CSV');
|
||||
}
|
||||
|
||||
// Validate no duplicate drivers
|
||||
const driverIds = rows.map(r => r.driverId);
|
||||
const driverIds = rows.map((r) => r.driverId);
|
||||
const uniqueDrivers = new Set(driverIds);
|
||||
if (driverIds.length !== uniqueDrivers.size) {
|
||||
throw new Error('Duplicate driver IDs found in CSV');
|
||||
@@ -109,33 +115,27 @@ export default function ImportResultsForm({ raceId, onSuccess, onError }: Import
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Read file
|
||||
const content = await file.text();
|
||||
|
||||
// Parse CSV
|
||||
const rows = parseCSV(content);
|
||||
|
||||
// Create Result entities
|
||||
const results = rows.map(row =>
|
||||
Result.create({
|
||||
id: uuidv4(),
|
||||
raceId,
|
||||
driverId: row.driverId,
|
||||
position: row.position,
|
||||
fastestLap: row.fastestLap,
|
||||
incidents: row.incidents,
|
||||
startPosition: row.startPosition,
|
||||
})
|
||||
);
|
||||
const results: ImportResultRowDTO[] = rows.map((row) => ({
|
||||
id: uuidv4(),
|
||||
raceId,
|
||||
driverId: row.driverId,
|
||||
position: row.position,
|
||||
fastestLap: row.fastestLap,
|
||||
incidents: row.incidents,
|
||||
startPosition: row.startPosition,
|
||||
}));
|
||||
|
||||
onSuccess(results);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to parse CSV file';
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : 'Failed to parse CSV file';
|
||||
setError(errorMessage);
|
||||
onError(errorMessage);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
// Reset file input
|
||||
event.target.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,32 +1,58 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { Result } from '@gridpilot/racing/domain/entities/Result';
|
||||
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
|
||||
import type { PenaltyType } from '@gridpilot/racing/domain/entities/Penalty';
|
||||
import { AlertTriangle, ExternalLink } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* Penalty data for display (can be domain Penalty or RacePenaltyDTO)
|
||||
*/
|
||||
type PenaltyTypeDTO =
|
||||
| 'time_penalty'
|
||||
| 'grid_penalty'
|
||||
| 'points_deduction'
|
||||
| 'disqualification'
|
||||
| 'warning'
|
||||
| 'license_points'
|
||||
| string;
|
||||
|
||||
interface ResultDTO {
|
||||
id: string;
|
||||
raceId: string;
|
||||
driverId: string;
|
||||
position: number;
|
||||
fastestLap: number;
|
||||
incidents: number;
|
||||
startPosition: number;
|
||||
getPositionChange(): number;
|
||||
}
|
||||
|
||||
interface DriverDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface PenaltyData {
|
||||
driverId: string;
|
||||
type: PenaltyType;
|
||||
type: PenaltyTypeDTO;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
interface ResultsTableProps {
|
||||
results: Result[];
|
||||
drivers: Driver[];
|
||||
results: ResultDTO[];
|
||||
drivers: DriverDTO[];
|
||||
pointsSystem: Record<number, number>;
|
||||
fastestLapTime?: number;
|
||||
penalties?: PenaltyData[];
|
||||
currentDriverId?: string;
|
||||
}
|
||||
|
||||
export default function ResultsTable({ results, drivers, pointsSystem, fastestLapTime, penalties = [], currentDriverId }: ResultsTableProps) {
|
||||
const getDriver = (driverId: string): Driver | undefined => {
|
||||
return drivers.find(d => d.id === driverId);
|
||||
export default function ResultsTable({
|
||||
results,
|
||||
drivers,
|
||||
pointsSystem,
|
||||
fastestLapTime,
|
||||
penalties = [],
|
||||
currentDriverId,
|
||||
}: ResultsTableProps) {
|
||||
const getDriver = (driverId: string): DriverDTO | undefined => {
|
||||
return drivers.find((d) => d.id === driverId);
|
||||
};
|
||||
|
||||
const getDriverName = (driverId: string): string => {
|
||||
@@ -35,7 +61,7 @@ export default function ResultsTable({ results, drivers, pointsSystem, fastestLa
|
||||
};
|
||||
|
||||
const getDriverPenalties = (driverId: string): PenaltyData[] => {
|
||||
return penalties.filter(p => p.driverId === driverId);
|
||||
return penalties.filter((p) => p.driverId === driverId);
|
||||
};
|
||||
|
||||
const getPenaltyDescription = (penalty: PenaltyData): string => {
|
||||
@@ -97,30 +123,39 @@ export default function ResultsTable({ results, drivers, pointsSystem, fastestLa
|
||||
<tbody>
|
||||
{results.map((result) => {
|
||||
const positionChange = result.getPositionChange();
|
||||
const isFastestLap = fastestLapTime && result.fastestLap === fastestLapTime;
|
||||
const isFastestLap =
|
||||
typeof fastestLapTime === 'number' && result.fastestLap === fastestLapTime;
|
||||
const driverPenalties = getDriverPenalties(result.driverId);
|
||||
const driver = getDriver(result.driverId);
|
||||
const isCurrentUser = currentDriverId === result.driverId;
|
||||
const isPodium = result.position <= 3;
|
||||
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={result.id}
|
||||
className={`
|
||||
border-b border-charcoal-outline/50 transition-colors
|
||||
${isCurrentUser
|
||||
? 'bg-gradient-to-r from-primary-blue/20 via-primary-blue/10 to-transparent hover:from-primary-blue/30'
|
||||
: 'hover:bg-iron-gray/20'}
|
||||
${
|
||||
isCurrentUser
|
||||
? 'bg-gradient-to-r from-primary-blue/20 via-primary-blue/10 to-transparent hover:from-primary-blue/30'
|
||||
: 'hover:bg-iron-gray/20'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<td className="py-3 px-4">
|
||||
<div className={`
|
||||
<div
|
||||
className={`
|
||||
inline-flex items-center justify-center w-8 h-8 rounded-lg font-bold text-sm
|
||||
${result.position === 1 ? 'bg-yellow-500/20 text-yellow-400' :
|
||||
result.position === 2 ? 'bg-gray-400/20 text-gray-300' :
|
||||
result.position === 3 ? 'bg-amber-600/20 text-amber-500' :
|
||||
'text-white'}
|
||||
`}>
|
||||
${
|
||||
result.position === 1
|
||||
? 'bg-yellow-500/20 text-yellow-400'
|
||||
: result.position === 2
|
||||
? 'bg-gray-400/20 text-gray-300'
|
||||
: result.position === 3
|
||||
? 'bg-amber-600/20 text-amber-500'
|
||||
: 'text-white'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{result.position}
|
||||
</div>
|
||||
</td>
|
||||
@@ -128,17 +163,27 @@ export default function ResultsTable({ results, drivers, pointsSystem, fastestLa
|
||||
<div className="flex items-center gap-3">
|
||||
{driver ? (
|
||||
<>
|
||||
<div className={`
|
||||
<div
|
||||
className={`
|
||||
w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold flex-shrink-0
|
||||
${isCurrentUser ? 'bg-primary-blue/30 text-primary-blue ring-2 ring-primary-blue/50' : 'bg-iron-gray text-gray-400'}
|
||||
`}>
|
||||
${
|
||||
isCurrentUser
|
||||
? 'bg-primary-blue/30 text-primary-blue ring-2 ring-primary-blue/50'
|
||||
: 'bg-iron-gray text-gray-400'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{driver.name.charAt(0)}
|
||||
</div>
|
||||
<Link
|
||||
href={`/drivers/${driver.id}`}
|
||||
className={`
|
||||
flex items-center gap-1.5 group
|
||||
${isCurrentUser ? 'text-primary-blue font-semibold' : 'text-white hover:text-primary-blue'}
|
||||
${
|
||||
isCurrentUser
|
||||
? 'text-primary-blue font-semibold'
|
||||
: 'text-white hover:text-primary-blue'
|
||||
}
|
||||
transition-colors
|
||||
`}
|
||||
>
|
||||
@@ -157,20 +202,30 @@ export default function ResultsTable({ results, drivers, pointsSystem, fastestLa
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className={isFastestLap ? 'text-performance-green font-medium' : 'text-white'}>
|
||||
<span
|
||||
className={
|
||||
isFastestLap ? 'text-performance-green font-medium' : 'text-white'
|
||||
}
|
||||
>
|
||||
{formatLapTime(result.fastestLap)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className={result.incidents > 0 ? 'text-warning-amber' : 'text-white'}>
|
||||
<span
|
||||
className={result.incidents > 0 ? 'text-warning-amber' : 'text-white'}
|
||||
>
|
||||
{result.incidents}×
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className="text-white font-medium">{getPoints(result.position)}</span>
|
||||
<span className="text-white font-medium">
|
||||
{getPoints(result.position)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className={`font-medium ${getPositionChangeColor(positionChange)}`}>
|
||||
<span
|
||||
className={`font-medium ${getPositionChangeColor(positionChange)}`}
|
||||
>
|
||||
{getPositionChangeText(positionChange)}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
Reference in New Issue
Block a user