From 02d50e43eec57ba64feb703b0e0a6630a0b5565c Mon Sep 17 00:00:00 2001 From: Zac Jones Date: Fri, 10 Apr 2026 22:25:54 -0600 Subject: [PATCH 1/3] feat: add competition division cutoff rank with leaderboard line Add configurable cutoff rank that renders a bold orange line on the leaderboard after the Nth-ranked athlete. Uses the same two-level fallback as capacity: competition-wide default with per-division override. - Schema: defaultCutoffRank on competitionsTable, cutoffRank on competitionDivisionsTable - Organizer UI: cutoff input in CapacitySettingsForm and per-division items - Leaderboard: orange line in both in-person and online tables (desktop + mobile) - Only shown on overall view, hidden in single-event view - Handles ties correctly (line after all tied athletes at cutoff) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../competition-leaderboard-table.tsx | 77 ++++++++--- .../divisions/organizer-division-item.tsx | 53 +++++++ .../divisions/organizer-division-manager.tsx | 46 +++++++ .../components/leaderboard-page-content.tsx | 5 + .../online-competition-leaderboard-table.tsx | 130 +++++++++++------- .../wodsmith-start/src/db/schemas/commerce.ts | 2 + .../src/db/schemas/competitions.ts | 2 + .../-components/capacity-settings-form.tsx | 45 +++++- .../organizer/$competitionId/divisions.tsx | 4 + .../organizer/$competitionId/settings.tsx | 1 + .../src/server-fns/competition-detail-fns.ts | 1 + .../server-fns/competition-divisions-fns.ts | 87 +++++++++++- .../src/server-fns/competition-fns.ts | 6 + .../leaderboard-page-content.test.tsx | 2 + lat.md/domain.md | 2 +- lat.md/organizer-dashboard.md | 8 +- 16 files changed, 399 insertions(+), 72 deletions(-) diff --git a/apps/wodsmith-start/src/components/competition-leaderboard-table.tsx b/apps/wodsmith-start/src/components/competition-leaderboard-table.tsx index ef2d7addc..675a863dc 100644 --- a/apps/wodsmith-start/src/components/competition-leaderboard-table.tsx +++ b/apps/wodsmith-start/src/components/competition-leaderboard-table.tsx @@ -74,6 +74,7 @@ interface CompetitionLeaderboardTableProps { }> selectedEventId: string | null // null = overall view scoringAlgorithm: ScoringAlgorithm + cutoffRank: number | null } function getRankIcon(rank: number) { @@ -426,6 +427,7 @@ export function CompetitionLeaderboardTable({ events, selectedEventId, scoringAlgorithm, + cutoffRank, }: CompetitionLeaderboardTableProps) { // Compute the correct default sort column based on view mode const defaultSortColumn = selectedEventId ? "eventRank" : "overallRank" @@ -825,14 +827,30 @@ export function CompetitionLeaderboardTable({ ) : (
- {tableData.map((entry) => ( - - ))} + {tableData.map((entry, idx) => { + const nextEntry = tableData[idx + 1] + const showCutoff = + cutoffRank != null && + !selectedEventId && + entry.overallRank <= cutoffRank && + (!nextEntry || nextEntry.overallRank > cutoffRank) + return ( + + + {showCutoff && ( +
+ )} + + ) + })}
)}
@@ -885,18 +903,37 @@ export function CompetitionLeaderboardTable({ ) : ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} - - )) + table.getRowModel().rows.map((row, rowIdx) => { + const entry = row.original + const rows = table.getRowModel().rows + const nextRow = rows[rowIdx + 1] + const showCutoff = + cutoffRank != null && + !selectedEventId && + entry.overallRank <= cutoffRank && + (!nextRow || nextRow.original.overallRank > cutoffRank) + return ( + + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + {showCutoff && ( + + +
+ + + )} + + ) + }) )} diff --git a/apps/wodsmith-start/src/components/divisions/organizer-division-item.tsx b/apps/wodsmith-start/src/components/divisions/organizer-division-item.tsx index 2387c931a..4b3e0dcc9 100644 --- a/apps/wodsmith-start/src/components/divisions/organizer-division-item.tsx +++ b/apps/wodsmith-start/src/components/divisions/organizer-division-item.tsx @@ -38,6 +38,8 @@ export interface OrganizerDivisionItemProps { description: string | null maxSpots: number | null defaultMaxSpots: number | null + cutoffRank: number | null + defaultCutoffRank: number | null index: number registrationCount: number isOnly: boolean @@ -45,6 +47,7 @@ export interface OrganizerDivisionItemProps { onLabelSave: (value: string) => void onDescriptionSave: (value: string | null) => void onMaxSpotsSave: (value: number | null) => void + onCutoffRankSave: (value: number | null) => void onRemove: () => void onDrop: (sourceIndex: number, targetIndex: number) => void } @@ -55,6 +58,8 @@ export function OrganizerDivisionItem({ description, maxSpots, defaultMaxSpots, + cutoffRank, + defaultCutoffRank, index, registrationCount, isOnly, @@ -62,6 +67,7 @@ export function OrganizerDivisionItem({ onLabelSave, onDescriptionSave, onMaxSpotsSave, + onCutoffRankSave, onRemove, onDrop, }: OrganizerDivisionItemProps) { @@ -74,6 +80,7 @@ export function OrganizerDivisionItem({ const labelRef = useRef(label) const [localDescription, setLocalDescription] = useState(description ?? "") const [localMaxSpots, setLocalMaxSpots] = useState(maxSpots?.toString() ?? "") + const [localCutoffRank, setLocalCutoffRank] = useState(cutoffRank?.toString() ?? "") const [isExpanded, setIsExpanded] = useState(false) // Sync local state when prop changes (e.g., after server update) @@ -90,6 +97,10 @@ export function OrganizerDivisionItem({ setLocalMaxSpots(maxSpots?.toString() ?? "") }, [maxSpots]) + useEffect(() => { + setLocalCutoffRank(cutoffRank?.toString() ?? "") + }, [cutoffRank]) + const canDelete = registrationCount === 0 && !isOnly useEffect(() => { @@ -318,6 +329,48 @@ export function OrganizerDivisionItem({ : "Leave blank for unlimited"}
+
+ + setLocalCutoffRank(e.target.value)} + onBlur={() => { + const newVal = + localCutoffRank.trim() === "" + ? null + : parseInt(localCutoffRank, 10) + if (newVal !== cutoffRank) { + if ( + newVal !== null && + (Number.isNaN(newVal) || newVal < 1) + ) { + setLocalCutoffRank(cutoffRank?.toString() ?? "") + return + } + onCutoffRankSave(newVal) + } + }} + placeholder={ + defaultCutoffRank + ? `${defaultCutoffRank} (default)` + : "No cutoff" + } + className="w-32 text-sm" + /> + + {defaultCutoffRank + ? "Leave blank to use competition default" + : "Leave blank for no cutoff line"} + +