update leaderboard schema to match new API wire format with flattened FL points

This commit is contained in:
2026-03-01 16:41:16 +01:00
parent adf123f56a
commit a3aade5aae
5 changed files with 57 additions and 16 deletions

View File

@@ -1,7 +1,7 @@
'use client';
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 { LeaderboardStandings } from './LeaderboardStandings';
import { DriversEditor } from './editors/DriversEditor';
@@ -24,13 +24,38 @@ interface LeaderboardManagerProps {
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);
}
export function LeaderboardManager({ serverId, initialData }: LeaderboardManagerProps) {
const [draft, setDraft] = useState<Leaderboard>(initialData);
const [saved, setSaved] = useState<Leaderboard>(initialData);
const [draft, setDraft] = useState<LeaderboardDraft>(toUiFormat(initialData));
const [saved, setSaved] = useState<LeaderboardDraft>(toUiFormat(initialData));
const [activeSection, setActiveSection] = useState<Section>('standings');
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -61,7 +86,8 @@ export function LeaderboardManager({ serverId, initialData }: LeaderboardManager
setIsSubmitting(true);
setError(null);
try {
const result = await saveLeaderboardAction(serverId, draft);
const apiData = toApiFormat(draft);
const result = await saveLeaderboardAction(serverId, apiData);
if (result.success) {
setSaved(draft);
} else {
@@ -77,7 +103,7 @@ export function LeaderboardManager({ serverId, initialData }: LeaderboardManager
const renderSection = () => {
switch (activeSection) {
case 'standings':
return <LeaderboardStandings leaderboard={draft} />;
return <LeaderboardStandings leaderboard={toApiFormat(draft)} />;
case 'drivers':
return <DriversEditor drivers={draft.drivers} onChange={handleDriversChange} />;
case 'points':
@@ -90,8 +116,8 @@ export function LeaderboardManager({ serverId, initialData }: LeaderboardManager
case 'fl':
return (
<FlPointsEditor
flPoints={draft.flPoints}
onChange={(flPoints) => setDraft({ ...draft, flPoints })}
flPoints={draft.flPointsData}
onChange={(flPointsData) => setDraft({ ...draft, flPointsData })}
/>
);
case 'tracks':

View File

@@ -19,7 +19,7 @@ export function LeaderboardStandings({ leaderboard }: LeaderboardStandingsProps)
).length;
const totalPoints =
trackResults.reduce((sum: number, r) => sum + (typeof r === 'number' ? r : 0), 0) +
fastestLapCount * leaderboard.flPoints.points;
fastestLapCount * leaderboard.flPoints;
return { driver, driverId: driver.id, totalPoints, trackResults, fastestLapCount };
})
.sort((a, b) => b.totalPoints - a.totalPoints);
@@ -87,10 +87,10 @@ export function LeaderboardStandings({ leaderboard }: LeaderboardStandingsProps)
<span
className="ml-0.5 rounded px-0.5 text-xs"
style={{
backgroundColor: leaderboard.flPoints.color,
color: leaderboard.flPoints.textColor
backgroundColor: leaderboard.flColor,
color: leaderboard.flTextColor
}}
title={`Fastest lap: ${leaderboard.flPoints.label}`}
title="Fastest lap"
>
FL
</span>

View File

@@ -20,7 +20,7 @@ export function PointsTableEditor({ pointsTable, onChange }: PointsTableEditorPr
const pos = pointsTable.length + 1;
onChange([
...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 }
]);
};

View File

@@ -72,6 +72,7 @@ export function TracksEditor({ tracks, drivers, pointsTable, onChange }: TracksE
onChange([
...tracks,
{
id: crypto.randomUUID(),
name: `Race ${tracks.length + 1}`,
results: drivers.map((d) => ({ driverId: d.id, score: 'DNS' as const })),
fastestLapDriverId: ''

View File

@@ -12,6 +12,7 @@ export const leaderboardDriverSchema = z.object({
export type LeaderboardDriver = z.infer<typeof leaderboardDriverSchema>;
const pointsEntryFields = {
id: z.string(),
points: z.number().int().min(0),
label: z.string().min(1),
color: z.string().regex(/^#[0-9a-fA-F]{6}$/),
@@ -22,10 +23,17 @@ const pointsEntryFields = {
export const pointsEntrySchema = z.object(pointsEntryFields);
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 const leaderboardTrackSchema = z.object({
id: z.string(),
name: z.string().min(1),
results: z.array(z.object({ driverId: z.string(), score: resultValueSchema })),
fastestLapDriverId: z.string()
@@ -33,9 +41,13 @@ export const leaderboardTrackSchema = z.object({
export type LeaderboardTrack = z.infer<typeof leaderboardTrackSchema>;
export const leaderboardSchema = z.object({
id: z.string().optional(),
serverId: z.string().optional(),
drivers: z.array(leaderboardDriverSchema),
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)
});
export type Leaderboard = z.infer<typeof leaderboardSchema>;
@@ -43,7 +55,9 @@ export type Leaderboard = z.infer<typeof leaderboardSchema>;
export const emptyLeaderboard: Leaderboard = {
drivers: [],
pointsTable: [],
flPoints: { points: 1, label: 'FL +1', color: '#8b5cf6', textColor: '#000000', priority: 3 },
flPoints: 1,
flColor: '#8b5cf6',
flTextColor: '#000000',
tracks: []
};