Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e2f86fbcb6 | |||
| adf123f56a | |||
| 3ba68ac973 | |||
| 67a72f9a92 | |||
| 51c45ad4a9 |
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Leaderboard, LeaderboardDriver } from '@/lib/schemas/leaderboard';
|
import { Leaderboard, LeaderboardDriver, FlPoints } from '@/lib/schemas/leaderboard';
|
||||||
import { saveLeaderboardAction } from '@/lib/actions/leaderboard';
|
import { saveLeaderboardAction } from '@/lib/actions/leaderboard';
|
||||||
import { LeaderboardStandings } from './LeaderboardStandings';
|
import { LeaderboardStandings } from './LeaderboardStandings';
|
||||||
import { DriversEditor } from './editors/DriversEditor';
|
import { DriversEditor } from './editors/DriversEditor';
|
||||||
@@ -24,13 +24,38 @@ interface LeaderboardManagerProps {
|
|||||||
initialData: Leaderboard;
|
initialData: Leaderboard;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDirty(a: Leaderboard, b: Leaderboard) {
|
// Extended type for internal UI state with full FL Points object
|
||||||
|
type LeaderboardDraft = Leaderboard & { flPointsData: FlPoints };
|
||||||
|
|
||||||
|
function toApiFormat(draft: LeaderboardDraft): Leaderboard {
|
||||||
|
return {
|
||||||
|
...draft,
|
||||||
|
flPoints: draft.flPointsData.points,
|
||||||
|
flColor: draft.flPointsData.color,
|
||||||
|
flTextColor: draft.flPointsData.textColor
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toUiFormat(api: Leaderboard): LeaderboardDraft {
|
||||||
|
return {
|
||||||
|
...api,
|
||||||
|
flPointsData: {
|
||||||
|
points: api.flPoints,
|
||||||
|
label: 'FL +' + api.flPoints,
|
||||||
|
color: api.flColor,
|
||||||
|
textColor: api.flTextColor,
|
||||||
|
priority: 3
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDirty(a: LeaderboardDraft, b: LeaderboardDraft) {
|
||||||
return JSON.stringify(a) !== JSON.stringify(b);
|
return JSON.stringify(a) !== JSON.stringify(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LeaderboardManager({ serverId, initialData }: LeaderboardManagerProps) {
|
export function LeaderboardManager({ serverId, initialData }: LeaderboardManagerProps) {
|
||||||
const [draft, setDraft] = useState<Leaderboard>(initialData);
|
const [draft, setDraft] = useState<LeaderboardDraft>(toUiFormat(initialData));
|
||||||
const [saved, setSaved] = useState<Leaderboard>(initialData);
|
const [saved, setSaved] = useState<LeaderboardDraft>(toUiFormat(initialData));
|
||||||
const [activeSection, setActiveSection] = useState<Section>('standings');
|
const [activeSection, setActiveSection] = useState<Section>('standings');
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -38,15 +63,20 @@ export function LeaderboardManager({ serverId, initialData }: LeaderboardManager
|
|||||||
const dirty = isDirty(draft, saved);
|
const dirty = isDirty(draft, saved);
|
||||||
|
|
||||||
const handleDriversChange = (newDrivers: LeaderboardDriver[]) => {
|
const handleDriversChange = (newDrivers: LeaderboardDriver[]) => {
|
||||||
const oldDrivers = draft.drivers;
|
const oldDriverIds = new Set(draft.drivers.map((d) => d.id));
|
||||||
|
|
||||||
// Build index map: for each position in newDrivers, what was the old index?
|
// Remap tracks: keep results for drivers that still exist, remove results for deleted drivers
|
||||||
const indexMap = newDrivers.map((d) => oldDrivers.indexOf(d));
|
|
||||||
|
|
||||||
// If a driver was added (indexOf returns -1), it's new
|
|
||||||
const newTracks = draft.tracks.map((t) => ({
|
const newTracks = draft.tracks.map((t) => ({
|
||||||
...t,
|
...t,
|
||||||
results: indexMap.map((i) => (i === -1 ? 0 : (t.results[i] ?? 0)))
|
// 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 });
|
setDraft({ ...draft, drivers: newDrivers, tracks: newTracks });
|
||||||
@@ -56,7 +86,8 @@ export function LeaderboardManager({ serverId, initialData }: LeaderboardManager
|
|||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const result = await saveLeaderboardAction(serverId, draft);
|
const apiData = toApiFormat(draft);
|
||||||
|
const result = await saveLeaderboardAction(serverId, apiData);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setSaved(draft);
|
setSaved(draft);
|
||||||
} else {
|
} else {
|
||||||
@@ -72,7 +103,7 @@ export function LeaderboardManager({ serverId, initialData }: LeaderboardManager
|
|||||||
const renderSection = () => {
|
const renderSection = () => {
|
||||||
switch (activeSection) {
|
switch (activeSection) {
|
||||||
case 'standings':
|
case 'standings':
|
||||||
return <LeaderboardStandings leaderboard={draft} />;
|
return <LeaderboardStandings leaderboard={toApiFormat(draft)} />;
|
||||||
case 'drivers':
|
case 'drivers':
|
||||||
return <DriversEditor drivers={draft.drivers} onChange={handleDriversChange} />;
|
return <DriversEditor drivers={draft.drivers} onChange={handleDriversChange} />;
|
||||||
case 'points':
|
case 'points':
|
||||||
@@ -85,8 +116,8 @@ export function LeaderboardManager({ serverId, initialData }: LeaderboardManager
|
|||||||
case 'fl':
|
case 'fl':
|
||||||
return (
|
return (
|
||||||
<FlPointsEditor
|
<FlPointsEditor
|
||||||
flPoints={draft.flPoints}
|
flPoints={draft.flPointsData}
|
||||||
onChange={(flPoints) => setDraft({ ...draft, flPoints })}
|
onChange={(flPointsData) => setDraft({ ...draft, flPointsData })}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'tracks':
|
case 'tracks':
|
||||||
|
|||||||
@@ -10,15 +10,17 @@ interface LeaderboardStandingsProps {
|
|||||||
export function LeaderboardStandings({ leaderboard }: LeaderboardStandingsProps) {
|
export function LeaderboardStandings({ leaderboard }: LeaderboardStandingsProps) {
|
||||||
const standings = useMemo<StandingsRow[]>(() => {
|
const standings = useMemo<StandingsRow[]>(() => {
|
||||||
return leaderboard.drivers
|
return leaderboard.drivers
|
||||||
.map((driver, idx) => {
|
.map((driver) => {
|
||||||
const trackResults = leaderboard.tracks.map((t) => t.results[idx] ?? 0);
|
const trackResults = leaderboard.tracks.map(
|
||||||
|
(t) => t.results.find((r) => r.driverId === driver.id)?.score ?? 'DNS'
|
||||||
|
);
|
||||||
const fastestLapCount = leaderboard.tracks.filter(
|
const fastestLapCount = leaderboard.tracks.filter(
|
||||||
(t) => t.fastestLapInitials === driver.initials
|
(t) => t.fastestLapDriverId === driver.id
|
||||||
).length;
|
).length;
|
||||||
const totalPoints =
|
const totalPoints =
|
||||||
trackResults.reduce((sum, r) => sum + (typeof r === 'number' ? r : 0), 0) +
|
trackResults.reduce((sum: number, r) => sum + (typeof r === 'number' ? r : 0), 0) +
|
||||||
fastestLapCount * leaderboard.flPoints.points;
|
fastestLapCount * leaderboard.flPoints;
|
||||||
return { driver, driverIndex: idx, totalPoints, trackResults, fastestLapCount };
|
return { driver, driverId: driver.id, totalPoints, trackResults, fastestLapCount };
|
||||||
})
|
})
|
||||||
.sort((a, b) => b.totalPoints - a.totalPoints);
|
.sort((a, b) => b.totalPoints - a.totalPoints);
|
||||||
}, [leaderboard]);
|
}, [leaderboard]);
|
||||||
@@ -54,7 +56,7 @@ export function LeaderboardStandings({ leaderboard }: LeaderboardStandingsProps)
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{standings.map((row, pos) => (
|
{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 text-gray-400">{pos + 1}</td>
|
||||||
<td className="py-2 pr-4">
|
<td className="py-2 pr-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -68,7 +70,7 @@ export function LeaderboardStandings({ leaderboard }: LeaderboardStandingsProps)
|
|||||||
</td>
|
</td>
|
||||||
{row.trackResults.map((result, ti) => {
|
{row.trackResults.map((result, ti) => {
|
||||||
const track = leaderboard.tracks[ti];
|
const track = leaderboard.tracks[ti];
|
||||||
const isFl = track.fastestLapInitials === row.driver.initials;
|
const isFl = track.fastestLapDriverId === row.driver.id;
|
||||||
return (
|
return (
|
||||||
<td key={ti} className="px-2 py-2 text-center">
|
<td key={ti} className="px-2 py-2 text-center">
|
||||||
<span
|
<span
|
||||||
@@ -85,10 +87,10 @@ export function LeaderboardStandings({ leaderboard }: LeaderboardStandingsProps)
|
|||||||
<span
|
<span
|
||||||
className="ml-0.5 rounded px-0.5 text-xs"
|
className="ml-0.5 rounded px-0.5 text-xs"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: leaderboard.flPoints.color,
|
backgroundColor: leaderboard.flColor,
|
||||||
color: leaderboard.flPoints.textColor
|
color: leaderboard.flTextColor
|
||||||
}}
|
}}
|
||||||
title={`Fastest lap: ${leaderboard.flPoints.label}`}
|
title="Fastest lap"
|
||||||
>
|
>
|
||||||
FL
|
FL
|
||||||
</span>
|
</span>
|
||||||
@@ -100,9 +102,7 @@ export function LeaderboardStandings({ leaderboard }: LeaderboardStandingsProps)
|
|||||||
<td className="px-2 py-2 text-center text-xs text-purple-400">
|
<td className="px-2 py-2 text-center text-xs text-purple-400">
|
||||||
{row.fastestLapCount > 0 ? `×${row.fastestLapCount}` : '—'}
|
{row.fastestLapCount > 0 ? `×${row.fastestLapCount}` : '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-2 py-2 text-center font-bold text-yellow-400">
|
<td className="px-2 py-2 text-center font-bold text-yellow-400">{row.totalPoints}</td>
|
||||||
{row.totalPoints}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ export function DriversEditor({ drivers, onChange }: DriversEditorProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const add = () => {
|
const add = () => {
|
||||||
onChange([...drivers, { name: 'New Driver', initials: 'ND', color: randomColor() }]);
|
onChange([...drivers, { id: crypto.randomUUID(), name: 'New Driver', initials: 'ND', color: randomColor() }]);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{drivers.map((driver, i) => (
|
{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">
|
<div className="flex flex-col gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => moveUp(i)}
|
onClick={() => moveUp(i)}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function PointsTableEditor({ pointsTable, onChange }: PointsTableEditorPr
|
|||||||
const pos = pointsTable.length + 1;
|
const pos = pointsTable.length + 1;
|
||||||
onChange([
|
onChange([
|
||||||
...pointsTable,
|
...pointsTable,
|
||||||
{ points: 0, label: `P${pos}`, color: '#374151', textColor: '#ffffff', priority: pos }
|
{ id: crypto.randomUUID(), points: 0, label: `P${pos}`, color: '#374151', textColor: '#ffffff', priority: pos }
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,20 +10,27 @@ interface ResultInputProps {
|
|||||||
|
|
||||||
function ResultInput({ value, pointsTable, onChange }: ResultInputProps) {
|
function ResultInput({ value, pointsTable, onChange }: ResultInputProps) {
|
||||||
const knownPoints = Array.from(new Set(pointsTable.map((e) => e.points))).sort((a, b) => b - a);
|
const knownPoints = Array.from(new Set(pointsTable.map((e) => e.points))).sort((a, b) => b - a);
|
||||||
|
// Fallback 0 to DNS if 0 is not a valid points option
|
||||||
|
const displayValue = value === 0 && !knownPoints.includes(0) ? 'DNS' : value;
|
||||||
|
// Ensure the current numeric value is always included in options
|
||||||
|
const allPoints = typeof displayValue === 'number'
|
||||||
|
? Array.from(new Set([...knownPoints, displayValue])).sort((a, b) => b - a)
|
||||||
|
: knownPoints;
|
||||||
|
|
||||||
const handleChange = (raw: string) => {
|
const handleChange = (raw: string) => {
|
||||||
if (raw === 'DNF') return onChange('DNF');
|
if (raw === 'DNF') return onChange('DNF');
|
||||||
if (raw === 'DNS') return onChange('DNS');
|
if (raw === 'DNS') return onChange('DNS');
|
||||||
onChange(parseInt(raw) || 0);
|
const parsed = parseInt(raw);
|
||||||
|
onChange(isNaN(parsed) ? 0 : parsed);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<select
|
<select
|
||||||
value={String(value)}
|
value={String(displayValue)}
|
||||||
onChange={(e) => handleChange(e.target.value)}
|
onChange={(e) => handleChange(e.target.value)}
|
||||||
className="w-full rounded bg-gray-600 px-1 py-1 text-xs text-white"
|
className="w-full rounded bg-gray-600 px-1 py-1 text-xs text-white"
|
||||||
>
|
>
|
||||||
{knownPoints.map((p) => (
|
{allPoints.map((p) => (
|
||||||
<option key={p} value={String(p)}>
|
<option key={p} value={String(p)}>
|
||||||
{p}
|
{p}
|
||||||
</option>
|
</option>
|
||||||
@@ -46,9 +53,14 @@ export function TracksEditor({ tracks, drivers, pointsTable, onChange }: TracksE
|
|||||||
onChange(tracks.map((t, i) => (i === index ? { ...t, ...patch } : t)));
|
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];
|
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 });
|
updateTrack(trackIndex, { results: newResults });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,9 +72,10 @@ export function TracksEditor({ tracks, drivers, pointsTable, onChange }: TracksE
|
|||||||
onChange([
|
onChange([
|
||||||
...tracks,
|
...tracks,
|
||||||
{
|
{
|
||||||
|
id: crypto.randomUUID(),
|
||||||
name: `Race ${tracks.length + 1}`,
|
name: `Race ${tracks.length + 1}`,
|
||||||
results: drivers.map(() => 'DNS' as const),
|
results: drivers.map((d) => ({ driverId: d.id, score: 'DNS' as const })),
|
||||||
fastestLapInitials: ''
|
fastestLapDriverId: ''
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
@@ -82,13 +95,13 @@ export function TracksEditor({ tracks, drivers, pointsTable, onChange }: TracksE
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<label className="text-xs text-gray-400">FL Driver:</label>
|
<label className="text-xs text-gray-400">FL Driver:</label>
|
||||||
<select
|
<select
|
||||||
value={track.fastestLapInitials}
|
value={track.fastestLapDriverId}
|
||||||
onChange={(e) => updateTrack(ti, { fastestLapInitials: e.target.value })}
|
onChange={(e) => updateTrack(ti, { fastestLapDriverId: e.target.value })}
|
||||||
className="rounded bg-gray-600 px-2 py-1 text-sm text-white"
|
className="rounded bg-gray-600 px-2 py-1 text-sm text-white"
|
||||||
>
|
>
|
||||||
<option value="">—</option>
|
<option value="">—</option>
|
||||||
{drivers.map((d) => (
|
{drivers.map((d) => (
|
||||||
<option key={d.initials} value={d.initials}>
|
<option key={d.id} value={d.id}>
|
||||||
{d.initials} ({d.name})
|
{d.initials} ({d.name})
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
@@ -107,8 +120,8 @@ export function TracksEditor({ tracks, drivers, pointsTable, onChange }: TracksE
|
|||||||
<p className="text-xs text-gray-500">Add drivers first to enter results.</p>
|
<p className="text-xs text-gray-500">Add drivers first to enter results.</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{drivers.map((driver, di) => (
|
{drivers.map((driver) => (
|
||||||
<div key={di} className="flex items-center gap-3">
|
<div key={driver.id} className="flex items-center gap-3">
|
||||||
<div className="flex w-40 items-center gap-2">
|
<div className="flex w-40 items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className="h-3 w-3 shrink-0 rounded-full"
|
className="h-3 w-3 shrink-0 rounded-full"
|
||||||
@@ -118,9 +131,9 @@ export function TracksEditor({ tracks, drivers, pointsTable, onChange }: TracksE
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-24">
|
<div className="w-24">
|
||||||
<ResultInput
|
<ResultInput
|
||||||
value={track.results[di] ?? 0}
|
value={track.results.find((r) => r.driverId === driver.id)?.score ?? 'DNS'}
|
||||||
pointsTable={pointsTable}
|
pointsTable={pointsTable}
|
||||||
onChange={(v) => updateResult(ti, di, v)}
|
onChange={(v) => updateResult(ti, driver.id, v)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export const resultValueSchema = z.union([z.literal('DNF'), z.literal('DNS'), z.
|
|||||||
export type ResultValue = z.infer<typeof resultValueSchema>;
|
export type ResultValue = z.infer<typeof resultValueSchema>;
|
||||||
|
|
||||||
export const leaderboardDriverSchema = z.object({
|
export const leaderboardDriverSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
color: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
color: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
||||||
initials: z.string().min(1).max(10)
|
initials: z.string().min(1).max(10)
|
||||||
@@ -11,6 +12,7 @@ export const leaderboardDriverSchema = z.object({
|
|||||||
export type LeaderboardDriver = z.infer<typeof leaderboardDriverSchema>;
|
export type LeaderboardDriver = z.infer<typeof leaderboardDriverSchema>;
|
||||||
|
|
||||||
const pointsEntryFields = {
|
const pointsEntryFields = {
|
||||||
|
id: z.string(),
|
||||||
points: z.number().int().min(0),
|
points: z.number().int().min(0),
|
||||||
label: z.string().min(1),
|
label: z.string().min(1),
|
||||||
color: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
color: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
||||||
@@ -21,20 +23,31 @@ const pointsEntryFields = {
|
|||||||
export const pointsEntrySchema = z.object(pointsEntryFields);
|
export const pointsEntrySchema = z.object(pointsEntryFields);
|
||||||
export type PointsEntry = z.infer<typeof pointsEntrySchema>;
|
export type PointsEntry = z.infer<typeof pointsEntrySchema>;
|
||||||
|
|
||||||
export const flPointsSchema = z.object(pointsEntryFields);
|
export const flPointsSchema = z.object({
|
||||||
|
points: z.number().int().min(0),
|
||||||
|
label: z.string().min(1),
|
||||||
|
color: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
||||||
|
textColor: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
||||||
|
priority: z.number().int().min(0)
|
||||||
|
});
|
||||||
export type FlPoints = z.infer<typeof flPointsSchema>;
|
export type FlPoints = z.infer<typeof flPointsSchema>;
|
||||||
|
|
||||||
export const leaderboardTrackSchema = z.object({
|
export const leaderboardTrackSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
results: z.array(resultValueSchema),
|
results: z.array(z.object({ driverId: z.string(), score: resultValueSchema })),
|
||||||
fastestLapInitials: z.string()
|
fastestLapDriverId: z.string()
|
||||||
});
|
});
|
||||||
export type LeaderboardTrack = z.infer<typeof leaderboardTrackSchema>;
|
export type LeaderboardTrack = z.infer<typeof leaderboardTrackSchema>;
|
||||||
|
|
||||||
export const leaderboardSchema = z.object({
|
export const leaderboardSchema = z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
serverId: z.string().optional(),
|
||||||
drivers: z.array(leaderboardDriverSchema),
|
drivers: z.array(leaderboardDriverSchema),
|
||||||
pointsTable: z.array(pointsEntrySchema),
|
pointsTable: z.array(pointsEntrySchema),
|
||||||
flPoints: flPointsSchema,
|
flPoints: z.number().int().min(0),
|
||||||
|
flColor: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
||||||
|
flTextColor: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
||||||
tracks: z.array(leaderboardTrackSchema)
|
tracks: z.array(leaderboardTrackSchema)
|
||||||
});
|
});
|
||||||
export type Leaderboard = z.infer<typeof leaderboardSchema>;
|
export type Leaderboard = z.infer<typeof leaderboardSchema>;
|
||||||
@@ -42,14 +55,16 @@ export type Leaderboard = z.infer<typeof leaderboardSchema>;
|
|||||||
export const emptyLeaderboard: Leaderboard = {
|
export const emptyLeaderboard: Leaderboard = {
|
||||||
drivers: [],
|
drivers: [],
|
||||||
pointsTable: [],
|
pointsTable: [],
|
||||||
flPoints: { points: 1, label: 'FL +1', color: '#8b5cf6', textColor: '#000000', priority: 3 },
|
flPoints: 1,
|
||||||
|
flColor: '#8b5cf6',
|
||||||
|
flTextColor: '#000000',
|
||||||
tracks: []
|
tracks: []
|
||||||
};
|
};
|
||||||
|
|
||||||
// Client-side derived type — never sent to API
|
// Client-side derived type — never sent to API
|
||||||
export type StandingsRow = {
|
export type StandingsRow = {
|
||||||
driver: LeaderboardDriver;
|
driver: LeaderboardDriver;
|
||||||
driverIndex: number;
|
driverId: string;
|
||||||
totalPoints: number;
|
totalPoints: number;
|
||||||
trackResults: ResultValue[];
|
trackResults: ResultValue[];
|
||||||
fastestLapCount: number;
|
fastestLapCount: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user