From 3dbe113ad1f0d5132a1e292c762bfe2e8dcfa233 Mon Sep 17 00:00:00 2001 From: Ian Jones Date: Thu, 11 Jun 2026 17:40:08 -0600 Subject: [PATCH 1/2] improve division capacity editing --- .../divisions/organizer-division-item.tsx | 267 +++++++++++++----- .../divisions/organizer-division-manager.tsx | 170 +++++++++-- .../-components/capacity-settings-form.tsx | 15 +- .../server-fns/competition-divisions-fns.ts | 9 +- 4 files changed, 372 insertions(+), 89 deletions(-) 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..02aba0f5a 100644 --- a/apps/wodsmith-start/src/components/divisions/organizer-division-item.tsx +++ b/apps/wodsmith-start/src/components/divisions/organizer-division-item.tsx @@ -14,7 +14,15 @@ import { extractClosestEdge, } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge" import { DropIndicator } from "@atlaskit/pragmatic-drag-and-drop-react-drop-indicator/box" -import { ChevronDown, GripVertical, Trash2, Users } from "lucide-react" +import { + AlertTriangle, + ChevronDown, + GripVertical, + Info, + Pencil, + Trash2, + Users, +} from "lucide-react" import { useEffect, useRef, useState } from "react" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" @@ -24,6 +32,13 @@ import { CollapsibleTrigger, } from "@/components/ui/collapsible" import { Input } from "@/components/ui/input" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" import { Textarea } from "@/components/ui/textarea" import { Tooltip, @@ -31,6 +46,7 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip" +import { cn } from "@/utils/cn" export interface OrganizerDivisionItemProps { id: string @@ -38,11 +54,13 @@ export interface OrganizerDivisionItemProps { description: string | null maxSpots: number | null defaultMaxSpots: number | null + teamSize: number index: number registrationCount: number isOnly: boolean instanceId: symbol onLabelSave: (value: string) => void + onTeamSizeSave: (value: number) => void onDescriptionSave: (value: string | null) => void onMaxSpotsSave: (value: number | null) => void onRemove: () => void @@ -55,11 +73,13 @@ export function OrganizerDivisionItem({ description, maxSpots, defaultMaxSpots, + teamSize, index, registrationCount, isOnly, instanceId, onLabelSave, + onTeamSizeSave, onDescriptionSave, onMaxSpotsSave, onRemove, @@ -74,8 +94,31 @@ export function OrganizerDivisionItem({ const labelRef = useRef(label) const [localDescription, setLocalDescription] = useState(description ?? "") const [localMaxSpots, setLocalMaxSpots] = useState(maxSpots?.toString() ?? "") + const [localTeamSize, setLocalTeamSize] = useState(teamSize.toString()) const [isExpanded, setIsExpanded] = useState(false) + const effectiveMaxSpots = maxSpots ?? defaultMaxSpots + const isOverCapacity = + effectiveMaxSpots !== null && registrationCount > effectiveMaxSpots + const isAtCapacity = + effectiveMaxSpots !== null && registrationCount === effectiveMaxSpots + const isNearCapacity = + effectiveMaxSpots !== null && + registrationCount < effectiveMaxSpots && + registrationCount >= Math.ceil(effectiveMaxSpots * 0.8) + const capacityState = isOverCapacity + ? "over" + : isAtCapacity + ? "full" + : isNearCapacity + ? "near" + : "open" + const capacityLabel = + effectiveMaxSpots === null + ? `${registrationCount} / unlimited` + : `${registrationCount} / ${effectiveMaxSpots}` + const teamSizeLabel = teamSize === 1 ? "Individual" : `Team of ${teamSize}` + // Sync local state when prop changes (e.g., after server update) useEffect(() => { setLocalLabel(label) @@ -90,8 +133,32 @@ export function OrganizerDivisionItem({ setLocalMaxSpots(maxSpots?.toString() ?? "") }, [maxSpots]) + useEffect(() => { + setLocalTeamSize(teamSize.toString()) + }, [teamSize]) + const canDelete = registrationCount === 0 && !isOnly + const handleMaxSpotsBlur = () => { + const newVal = + localMaxSpots.trim() === "" ? null : parseInt(localMaxSpots, 10) + if (newVal !== maxSpots) { + if (newVal !== null && (Number.isNaN(newVal) || newVal < 1)) { + setLocalMaxSpots(maxSpots?.toString() ?? "") + return + } + onMaxSpotsSave(newVal) + } + } + + const handleTeamSizeChange = (value: string) => { + setLocalTeamSize(value) + const nextTeamSize = Number(value) + if (Number.isInteger(nextTeamSize) && nextTeamSize !== teamSize) { + onTeamSizeSave(nextTeamSize) + } + } + useEffect(() => { const element = ref.current const dragHandle = dragHandleRef.current @@ -201,13 +268,19 @@ export function OrganizerDivisionItem({ {closestEdge && }
-
+
+
+
+ {teamSizeLabel} + {description ? ( + + + {description} + + ) : ( + + + Add category or gender notes in details + + )} + {capacityState === "near" && ( + + Near capacity + + )} + {capacityState === "full" && ( + At capacity + )} + {capacityState === "over" && ( + + Over capacity + + )} +
+
-
- - setLocalMaxSpots(e.target.value)} - onBlur={() => { - const newVal = - localMaxSpots.trim() === "" - ? null - : parseInt(localMaxSpots, 10) - if (newVal !== maxSpots) { - if ( - newVal !== null && - (Number.isNaN(newVal) || newVal < 1) - ) { - setLocalMaxSpots(maxSpots?.toString() ?? "") - return - } - onMaxSpotsSave(newVal) - } - }} - placeholder={ - defaultMaxSpots - ? `${defaultMaxSpots} (default)` - : "Unlimited" - } - className="w-32 text-sm" - /> - - {defaultMaxSpots - ? "Leave blank to use competition default" - : "Leave blank for unlimited"} - -
+

+ {defaultMaxSpots + ? "Blank capacity uses the competition default. Add metadata here for gender, category, or eligibility notes." + : "Blank capacity keeps this division unlimited. Add metadata here for gender, category, or eligibility notes."} +