From e2f86fbcb66586b2826015ac269bf46edca26f30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=20Jurmanovi=C4=87?= Date: Sun, 1 Mar 2026 16:41:16 +0100 Subject: [PATCH] update leaderboard schema to match new API wire format with flattened FL points --- .../leaderboard/LeaderboardManager.tsx | 42 +++++++++++++++---- .../leaderboard/LeaderboardStandings.tsx | 8 ++-- .../leaderboard/editors/PointsTableEditor.tsx | 2 +- .../leaderboard/editors/TracksEditor.tsx | 1 + src/lib/schemas/leaderboard.ts | 20 +++++++-- 5 files changed, 57 insertions(+), 16 deletions(-) diff --git a/src/components/leaderboard/LeaderboardManager.tsx b/src/components/leaderboard/LeaderboardManager.tsx index 068ec7c..fa028f7 100644 --- a/src/components/leaderboard/LeaderboardManager.tsx +++ b/src/components/leaderboard/LeaderboardManager.tsx @@ -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(initialData); - const [saved, setSaved] = useState(initialData); + const [draft, setDraft] = useState(toUiFormat(initialData)); + const [saved, setSaved] = useState(toUiFormat(initialData)); const [activeSection, setActiveSection] = useState
('standings'); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(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 ; + return ; case 'drivers': return ; case 'points': @@ -90,8 +116,8 @@ export function LeaderboardManager({ serverId, initialData }: LeaderboardManager case 'fl': return ( setDraft({ ...draft, flPoints })} + flPoints={draft.flPointsData} + onChange={(flPointsData) => setDraft({ ...draft, flPointsData })} /> ); case 'tracks': diff --git a/src/components/leaderboard/LeaderboardStandings.tsx b/src/components/leaderboard/LeaderboardStandings.tsx index db8f865..1721b2f 100644 --- a/src/components/leaderboard/LeaderboardStandings.tsx +++ b/src/components/leaderboard/LeaderboardStandings.tsx @@ -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) FL diff --git a/src/components/leaderboard/editors/PointsTableEditor.tsx b/src/components/leaderboard/editors/PointsTableEditor.tsx index f298462..6b7ffc2 100644 --- a/src/components/leaderboard/editors/PointsTableEditor.tsx +++ b/src/components/leaderboard/editors/PointsTableEditor.tsx @@ -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 } ]); }; diff --git a/src/components/leaderboard/editors/TracksEditor.tsx b/src/components/leaderboard/editors/TracksEditor.tsx index fda9eb2..1534dda 100644 --- a/src/components/leaderboard/editors/TracksEditor.tsx +++ b/src/components/leaderboard/editors/TracksEditor.tsx @@ -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: '' diff --git a/src/lib/schemas/leaderboard.ts b/src/lib/schemas/leaderboard.ts index 842f473..c12348d 100644 --- a/src/lib/schemas/leaderboard.ts +++ b/src/lib/schemas/leaderboard.ts @@ -12,6 +12,7 @@ export const leaderboardDriverSchema = z.object({ export type LeaderboardDriver = z.infer; 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; -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; 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; 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; @@ -43,7 +55,9 @@ export type Leaderboard = z.infer; 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: [] };