migrate leaderboard to driver ID-based wire format
This commit is contained in:
@@ -38,19 +38,20 @@ export function LeaderboardManager({ serverId, initialData }: LeaderboardManager
|
||||
const dirty = isDirty(draft, saved);
|
||||
|
||||
const handleDriversChange = (newDrivers: LeaderboardDriver[]) => {
|
||||
const oldDrivers = draft.drivers;
|
||||
const oldDriverIds = new Set(draft.drivers.map((d) => d.id));
|
||||
|
||||
// Build index map by matching drivers by initials (their stable identifier)
|
||||
// For each position in newDrivers, find where that driver was in oldDrivers
|
||||
const indexMap = newDrivers.map((d) => {
|
||||
const oldIndex = oldDrivers.findIndex((old) => old.initials === d.initials);
|
||||
return oldIndex; // -1 if new driver, otherwise the old index
|
||||
});
|
||||
|
||||
// If a driver was added (findIndex returns -1), it's new, start with DNS
|
||||
// Remap tracks: keep results for drivers that still exist, remove results for deleted drivers
|
||||
const newTracks = draft.tracks.map((t) => ({
|
||||
...t,
|
||||
results: indexMap.map((i) => (i === -1 ? 'DNS' : (t.results[i] ?? 'DNS')))
|
||||
// Filter existing results to only include drivers still in newDrivers, add DNS for new drivers
|
||||
results: [
|
||||
// Keep existing results for drivers still in the list
|
||||
...t.results.filter((r) => newDrivers.some((d) => d.id === r.driverId)),
|
||||
// Add DNS for new drivers
|
||||
...newDrivers
|
||||
.filter((d) => !oldDriverIds.has(d.id))
|
||||
.map((d) => ({ driverId: d.id, score: 'DNS' as const }))
|
||||
]
|
||||
}));
|
||||
|
||||
setDraft({ ...draft, drivers: newDrivers, tracks: newTracks });
|
||||
|
||||
@@ -10,15 +10,17 @@ interface LeaderboardStandingsProps {
|
||||
export function LeaderboardStandings({ leaderboard }: LeaderboardStandingsProps) {
|
||||
const standings = useMemo<StandingsRow[]>(() => {
|
||||
return leaderboard.drivers
|
||||
.map((driver, idx) => {
|
||||
const trackResults = leaderboard.tracks.map((t) => t.results[idx] ?? 0);
|
||||
.map((driver) => {
|
||||
const trackResults = leaderboard.tracks.map(
|
||||
(t) => t.results.find((r) => r.driverId === driver.id)?.score ?? 'DNS'
|
||||
);
|
||||
const fastestLapCount = leaderboard.tracks.filter(
|
||||
(t) => t.fastestLapInitials === driver.initials
|
||||
(t) => t.fastestLapDriverId === driver.id
|
||||
).length;
|
||||
const totalPoints =
|
||||
trackResults.reduce((sum: number, r) => sum + (typeof r === 'number' ? r : 0), 0) +
|
||||
fastestLapCount * leaderboard.flPoints.points;
|
||||
return { driver, driverIndex: idx, totalPoints, trackResults, fastestLapCount };
|
||||
return { driver, driverId: driver.id, totalPoints, trackResults, fastestLapCount };
|
||||
})
|
||||
.sort((a, b) => b.totalPoints - a.totalPoints);
|
||||
}, [leaderboard]);
|
||||
@@ -54,7 +56,7 @@ export function LeaderboardStandings({ leaderboard }: LeaderboardStandingsProps)
|
||||
</thead>
|
||||
<tbody>
|
||||
{standings.map((row, pos) => (
|
||||
<tr key={row.driverIndex} className="border-b border-gray-700/50">
|
||||
<tr key={row.driver.id} className="border-b border-gray-700/50">
|
||||
<td className="py-2 pr-4 text-gray-400">{pos + 1}</td>
|
||||
<td className="py-2 pr-4">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -68,7 +70,7 @@ export function LeaderboardStandings({ leaderboard }: LeaderboardStandingsProps)
|
||||
</td>
|
||||
{row.trackResults.map((result, ti) => {
|
||||
const track = leaderboard.tracks[ti];
|
||||
const isFl = track.fastestLapInitials === row.driver.initials;
|
||||
const isFl = track.fastestLapDriverId === row.driver.id;
|
||||
return (
|
||||
<td key={ti} className="px-2 py-2 text-center">
|
||||
<span
|
||||
|
||||
@@ -36,13 +36,13 @@ export function DriversEditor({ drivers, onChange }: DriversEditorProps) {
|
||||
};
|
||||
|
||||
const add = () => {
|
||||
onChange([...drivers, { name: 'New Driver', initials: 'ND', color: randomColor() }]);
|
||||
onChange([...drivers, { id: crypto.randomUUID(), name: 'New Driver', initials: 'ND', color: randomColor() }]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{drivers.map((driver, i) => (
|
||||
<div key={i} className="flex items-center gap-3 rounded-lg bg-gray-700 p-3">
|
||||
<div key={driver.id} className="flex items-center gap-3 rounded-lg bg-gray-700 p-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<button
|
||||
onClick={() => moveUp(i)}
|
||||
|
||||
@@ -53,9 +53,14 @@ export function TracksEditor({ tracks, drivers, pointsTable, onChange }: TracksE
|
||||
onChange(tracks.map((t, i) => (i === index ? { ...t, ...patch } : t)));
|
||||
};
|
||||
|
||||
const updateResult = (trackIndex: number, driverIndex: number, value: ResultValue) => {
|
||||
const updateResult = (trackIndex: number, driverId: string, value: ResultValue) => {
|
||||
const newResults = [...tracks[trackIndex].results];
|
||||
newResults[driverIndex] = value;
|
||||
const existingIndex = newResults.findIndex((r) => r.driverId === driverId);
|
||||
if (existingIndex >= 0) {
|
||||
newResults[existingIndex] = { driverId, score: value };
|
||||
} else {
|
||||
newResults.push({ driverId, score: value });
|
||||
}
|
||||
updateTrack(trackIndex, { results: newResults });
|
||||
};
|
||||
|
||||
@@ -68,8 +73,8 @@ export function TracksEditor({ tracks, drivers, pointsTable, onChange }: TracksE
|
||||
...tracks,
|
||||
{
|
||||
name: `Race ${tracks.length + 1}`,
|
||||
results: drivers.map(() => 'DNS' as const),
|
||||
fastestLapInitials: ''
|
||||
results: drivers.map((d) => ({ driverId: d.id, score: 'DNS' as const })),
|
||||
fastestLapDriverId: ''
|
||||
}
|
||||
]);
|
||||
};
|
||||
@@ -89,13 +94,13 @@ export function TracksEditor({ tracks, drivers, pointsTable, onChange }: TracksE
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-xs text-gray-400">FL Driver:</label>
|
||||
<select
|
||||
value={track.fastestLapInitials}
|
||||
onChange={(e) => updateTrack(ti, { fastestLapInitials: e.target.value })}
|
||||
value={track.fastestLapDriverId}
|
||||
onChange={(e) => updateTrack(ti, { fastestLapDriverId: e.target.value })}
|
||||
className="rounded bg-gray-600 px-2 py-1 text-sm text-white"
|
||||
>
|
||||
<option value="">—</option>
|
||||
{drivers.map((d) => (
|
||||
<option key={d.initials} value={d.initials}>
|
||||
<option key={d.id} value={d.id}>
|
||||
{d.initials} ({d.name})
|
||||
</option>
|
||||
))}
|
||||
@@ -114,8 +119,8 @@ export function TracksEditor({ tracks, drivers, pointsTable, onChange }: TracksE
|
||||
<p className="text-xs text-gray-500">Add drivers first to enter results.</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{drivers.map((driver, di) => (
|
||||
<div key={di} className="flex items-center gap-3">
|
||||
{drivers.map((driver) => (
|
||||
<div key={driver.id} className="flex items-center gap-3">
|
||||
<div className="flex w-40 items-center gap-2">
|
||||
<div
|
||||
className="h-3 w-3 shrink-0 rounded-full"
|
||||
@@ -125,9 +130,9 @@ export function TracksEditor({ tracks, drivers, pointsTable, onChange }: TracksE
|
||||
</div>
|
||||
<div className="w-24">
|
||||
<ResultInput
|
||||
value={track.results[di] ?? 0}
|
||||
value={track.results.find((r) => r.driverId === driver.id)?.score ?? 'DNS'}
|
||||
pointsTable={pointsTable}
|
||||
onChange={(v) => updateResult(ti, di, v)}
|
||||
onChange={(v) => updateResult(ti, driver.id, v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user