diff --git a/apps/wodsmith-start/src/components/competition-leaderboard-table.tsx b/apps/wodsmith-start/src/components/competition-leaderboard-table.tsx index ef2d7addc..a52d1a0ec 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,26 @@ 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 +899,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"} + +