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"}
+
+