194 lines
5.7 KiB
TypeScript
194 lines
5.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
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: ImportResultRowDTO[]) => void;
|
|
onError: (error: string) => void;
|
|
}
|
|
|
|
interface CSVRow {
|
|
driverId: string;
|
|
position: number;
|
|
fastestLap: number;
|
|
incidents: number;
|
|
startPosition: number;
|
|
}
|
|
|
|
export default function ImportResultsForm({ raceId, onSuccess, onError }: ImportResultsFormProps) {
|
|
const [uploading, setUploading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const parseCSV = (content: string): CSVRow[] => {
|
|
const lines = content.trim().split('\n');
|
|
|
|
if (lines.length < 2) {
|
|
throw new Error('CSV file is empty or invalid');
|
|
}
|
|
|
|
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}`);
|
|
}
|
|
}
|
|
|
|
const rows: CSVRow[] = [];
|
|
for (let i = 1; i < lines.length; i++) {
|
|
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}`,
|
|
);
|
|
}
|
|
|
|
const row: Record<string, string> = {};
|
|
header.forEach((field, index) => {
|
|
row[field] = values[index] ?? '';
|
|
});
|
|
|
|
const driverId = row.driverid;
|
|
const position = parseInt(row.position, 10);
|
|
const fastestLap = parseFloat(row.fastestlap);
|
|
const incidents = parseInt(row.incidents, 10);
|
|
const startPosition = parseInt(row.startposition, 10);
|
|
|
|
if (!driverId || driverId.length === 0) {
|
|
throw new Error(`Row ${i}: driverId is required`);
|
|
}
|
|
|
|
if (Number.isNaN(position) || position < 1) {
|
|
throw new Error(`Row ${i}: position must be a positive integer`);
|
|
}
|
|
|
|
if (Number.isNaN(fastestLap) || fastestLap < 0) {
|
|
throw new Error(`Row ${i}: fastestLap must be a non-negative number`);
|
|
}
|
|
|
|
if (Number.isNaN(incidents) || incidents < 0) {
|
|
throw new Error(`Row ${i}: incidents must be a non-negative integer`);
|
|
}
|
|
|
|
if (Number.isNaN(startPosition) || startPosition < 1) {
|
|
throw new Error(`Row ${i}: startPosition must be a positive integer`);
|
|
}
|
|
|
|
rows.push({ driverId, position, fastestLap, incidents, startPosition });
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
return rows;
|
|
};
|
|
|
|
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = event.target.files?.[0];
|
|
if (!file) return;
|
|
|
|
setUploading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const content = await file.text();
|
|
const rows = parseCSV(content);
|
|
|
|
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';
|
|
setError(errorMessage);
|
|
onError(errorMessage);
|
|
} finally {
|
|
setUploading(false);
|
|
event.target.value = '';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-400 mb-2">
|
|
Upload Results CSV
|
|
</label>
|
|
<p className="text-xs text-gray-500 mb-3">
|
|
CSV format: driverId, position, fastestLap, incidents, startPosition
|
|
</p>
|
|
|
|
<input
|
|
type="file"
|
|
accept=".csv"
|
|
onChange={handleFileChange}
|
|
disabled={uploading}
|
|
className="block w-full text-sm text-gray-400
|
|
file:mr-4 file:py-2 file:px-4
|
|
file:rounded file:border-0
|
|
file:text-sm file:font-semibold
|
|
file:bg-primary-blue file:text-white
|
|
file:cursor-pointer file:transition-colors
|
|
hover:file:bg-primary-blue/80
|
|
disabled:file:opacity-50 disabled:file:cursor-not-allowed"
|
|
/>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="p-4 bg-warning-amber/10 border border-warning-amber/30 rounded text-warning-amber text-sm">
|
|
<strong>Error:</strong> {error}
|
|
</div>
|
|
)}
|
|
|
|
{uploading && (
|
|
<div className="text-center text-gray-400 text-sm">
|
|
Parsing CSV and importing results...
|
|
</div>
|
|
)}
|
|
|
|
<div className="p-4 bg-iron-gray/20 rounded text-xs text-gray-500">
|
|
<p className="font-semibold mb-2">CSV Example:</p>
|
|
<pre className="text-gray-400">
|
|
{`driverId,position,fastestLap,incidents,startPosition
|
|
550e8400-e29b-41d4-a716-446655440001,1,92.456,0,3
|
|
550e8400-e29b-41d4-a716-446655440002,2,92.789,1,1
|
|
550e8400-e29b-41d4-a716-446655440003,3,93.012,2,2`}
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
} |