Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
import { useLoaderData , Link , useRevalidator } from "react-router" ;
2025-10-18 14:55:26 -07:00
import { eq , and , asc , desc , inArray } from "drizzle-orm" ;
2025-10-17 12:30:58 -07:00
import { database } from "~/database/context" ;
import * as schema from "~/database/schema" ;
import { useDraftSocket } from "~/hooks/useDraftSocket" ;
2026-02-20 19:30:53 -08:00
import { useEffect , useState , useMemo , useRef } from "react" ;
2025-10-17 12:30:58 -07:00
import { Card } from "~/components/ui/card" ;
import { Badge } from "~/components/ui/badge" ;
2025-10-17 12:45:30 -07:00
import { Button } from "~/components/ui/button" ;
2025-10-25 03:23:41 -07:00
import { Tabs , TabsContent , TabsList , TabsTrigger } from "~/components/ui/tabs" ;
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
import { Dialog , DialogContent , DialogDescription , DialogFooter , DialogHeader , DialogTitle } from "~/components/ui/dialog" ;
2025-10-17 12:45:30 -07:00
import { getAuth } from "@clerk/react-router/server" ;
import { getTeamQueue } from "~/models/draft-queue" ;
2026-02-22 16:56:07 -08:00
import { buildOwnerMap } from "~/lib/owner-map" ;
2025-10-25 03:23:41 -07:00
import { DraftSidebar } from "~/components/DraftSidebar" ;
import { TeamsDraftedGrid } from "~/components/draft/TeamsDraftedGrid" ;
import { QueueSection } from "~/components/draft/QueueSection" ;
import { AvailableParticipantsSection } from "~/components/draft/AvailableParticipantsSection" ;
2025-10-25 18:25:26 -07:00
import { SidebarRecentPicks } from "~/components/draft/SidebarRecentPicks" ;
2025-10-25 03:23:41 -07:00
import { DraftGridSection } from "~/components/draft/DraftGridSection" ;
2025-10-26 21:01:12 -07:00
import { ConnectionOverlay } from "~/components/draft/ConnectionOverlay" ;
2025-10-26 21:09:30 -07:00
import { calculateDraftEligibility } from "~/lib/draft-eligibility" ;
2026-02-20 19:30:53 -08:00
import { getTeamForPick } from "~/lib/draft-order" ;
import { useDraftNotifications } from "~/hooks/useDraftNotifications" ;
2026-02-22 22:36:12 -08:00
import { useMediaQuery } from "~/hooks/useMediaQuery" ;
2026-02-20 19:30:53 -08:00
import { NotificationSettings } from "~/components/NotificationSettings" ;
2025-10-25 21:02:16 -07:00
import { toast } from "sonner" ;
Add draft clock UI, Fischer increment timer logic, and security fixes (#19)
- Add prominent clock badge to tab bar (lights up on your turn) with
correct Fischer increment chess-clock model: bank starts at initialTime,
+= incrementTime after each pick, other teams' banks untouched
- Extract pure timer helpers to app/lib/draft-timer.ts and add 29 unit
tests covering formatClockTime, calculateTimeAfterPick, getTimerColorClass,
and full snake-draft lifecycle regression scenarios
- Fix make-pick.ts: add status !== 'draft' and draftPaused server guards
- Fix draft-utils.ts: replace (global as any).__socketIO with getSocketIO(),
fix timeUsed to store actual timeRemaining at pick moment (not always 120),
add null timer warning, move timer fetch before pick insert
- Fix draft.start.ts: batch timer inserts, guard against empty draftSlots
- Fix DraftGridSection: memoize currentTeamId, widen teamTimers type to
Record<string, number | undefined>
- Fix duplicate animate-pulse (getTimerColorClass already includes it)
- Clamp negative seconds in formatClockTime to guard against timer drift
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:51:12 -08:00
import { formatClockTime , getTimerColorClass } from "~/lib/draft-timer" ;
2026-02-22 22:36:12 -08:00
import { Users , LayoutGrid , ListChecks , Settings } from "lucide-react" ;
Claude/fix pick timer ghll n (#29)
* Fix force-manual-pick resetting next team's timer to initial time
When a commissioner forced a manual pick, the next team's timer was
being reset to the initial time (2 minutes) instead of carrying
forward their existing time bank balance.
This aligns force-manual-pick with the behavior of regular user picks
and force-autopick: the picking team gets their increment added, and
the next team's timer is left untouched so their bank carries forward.
https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p
* Add regression tests for draft.force-manual-pick timer behavior
18 tests across 5 describe blocks covering:
- Authorization (401/403)
- Input validation (missing fields, bad participant, ineligible sport)
- Successful pick (response shape, draft-complete detection, socket events)
- Timer behavior (increment added to picking team, new timer creation, additive not reset)
- Two regression tests confirming the next team's timer is never touched:
draftTimers.findFirst called exactly once, no timer-update emitted for
next team, db.update called exactly twice (not three times)
https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p
* Add TypeScript types and improve draft validation (#28)
* Code review fixes: type safety, security hardening, and dead code removal
- Fix Socket.IO event types: draft-paused and draft-resumed were typed as
() => void but are emitted with { seasonId, paused } data payloads
- Fix draft.force-manual-pick: add missing season.status === "draft" guard
so commissioners cannot force picks outside an active draft; add duplicate
pick-number check so a slot cannot be assigned two picks (the previous
code only checked participant uniqueness, not slot uniqueness)
- Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all
API routes and league loaders; replace (auth as any).userId casts with
proper const { userId } = await getAuth(args) destructuring
- Remove unused isSnakeDraft = true dead variable from draft.make-pick
- Replace autodraftSettings: any and draftSlots: any[] in draft-utils with
properly typed InferSelectModel / DraftSlot types
- Update force-manual-pick tests: sequence draftPicks.findFirst mock for
the two-call flow; add new tests for status-check and slot-uniqueness
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
* Fix RouterContextProvider type errors in action test files
Cast context argument to RouterContextProvider in test helpers so
ActionFunctionArgs strict typing is satisfied without weakening the
production action signatures back to any.
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
---------
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 19:29:29 -08:00
import type { Route } from "./+types/$leagueId.draft.$seasonId" ;
2025-10-17 12:30:58 -07:00
2026-02-24 11:31:25 -08:00
type QueueItem = typeof schema . draftQueue . $inferSelect ;
2026-02-22 22:36:12 -08:00
const MOBILE_TABS = [
{ id : "lobby" as const , label : "Lobby" , Icon : Users } ,
{ id : "board" as const , label : "Board" , Icon : LayoutGrid } ,
{ id : "roster" as const , label : "Roster" , Icon : ListChecks } ,
{ id : "controls" as const , label : "Controls" , Icon : Settings } ,
] ;
Claude/fix pick timer ghll n (#29)
* Fix force-manual-pick resetting next team's timer to initial time
When a commissioner forced a manual pick, the next team's timer was
being reset to the initial time (2 minutes) instead of carrying
forward their existing time bank balance.
This aligns force-manual-pick with the behavior of regular user picks
and force-autopick: the picking team gets their increment added, and
the next team's timer is left untouched so their bank carries forward.
https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p
* Add regression tests for draft.force-manual-pick timer behavior
18 tests across 5 describe blocks covering:
- Authorization (401/403)
- Input validation (missing fields, bad participant, ineligible sport)
- Successful pick (response shape, draft-complete detection, socket events)
- Timer behavior (increment added to picking team, new timer creation, additive not reset)
- Two regression tests confirming the next team's timer is never touched:
draftTimers.findFirst called exactly once, no timer-update emitted for
next team, db.update called exactly twice (not three times)
https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p
* Add TypeScript types and improve draft validation (#28)
* Code review fixes: type safety, security hardening, and dead code removal
- Fix Socket.IO event types: draft-paused and draft-resumed were typed as
() => void but are emitted with { seasonId, paused } data payloads
- Fix draft.force-manual-pick: add missing season.status === "draft" guard
so commissioners cannot force picks outside an active draft; add duplicate
pick-number check so a slot cannot be assigned two picks (the previous
code only checked participant uniqueness, not slot uniqueness)
- Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all
API routes and league loaders; replace (auth as any).userId casts with
proper const { userId } = await getAuth(args) destructuring
- Remove unused isSnakeDraft = true dead variable from draft.make-pick
- Replace autodraftSettings: any and draftSlots: any[] in draft-utils with
properly typed InferSelectModel / DraftSlot types
- Update force-manual-pick tests: sequence draftPicks.findFirst mock for
the two-call flow; add new tests for status-check and slot-uniqueness
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
* Fix RouterContextProvider type errors in action test files
Cast context argument to RouterContextProvider in test helpers so
ActionFunctionArgs strict typing is satisfied without weakening the
production action signatures back to any.
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
---------
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 19:29:29 -08:00
export async function loader ( args : Route.LoaderArgs ) {
2025-10-17 12:45:30 -07:00
const { params } = args ;
const { seasonId , leagueId } = params ;
Claude/fix pick timer ghll n (#29)
* Fix force-manual-pick resetting next team's timer to initial time
When a commissioner forced a manual pick, the next team's timer was
being reset to the initial time (2 minutes) instead of carrying
forward their existing time bank balance.
This aligns force-manual-pick with the behavior of regular user picks
and force-autopick: the picking team gets their increment added, and
the next team's timer is left untouched so their bank carries forward.
https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p
* Add regression tests for draft.force-manual-pick timer behavior
18 tests across 5 describe blocks covering:
- Authorization (401/403)
- Input validation (missing fields, bad participant, ineligible sport)
- Successful pick (response shape, draft-complete detection, socket events)
- Timer behavior (increment added to picking team, new timer creation, additive not reset)
- Two regression tests confirming the next team's timer is never touched:
draftTimers.findFirst called exactly once, no timer-update emitted for
next team, db.update called exactly twice (not three times)
https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p
* Add TypeScript types and improve draft validation (#28)
* Code review fixes: type safety, security hardening, and dead code removal
- Fix Socket.IO event types: draft-paused and draft-resumed were typed as
() => void but are emitted with { seasonId, paused } data payloads
- Fix draft.force-manual-pick: add missing season.status === "draft" guard
so commissioners cannot force picks outside an active draft; add duplicate
pick-number check so a slot cannot be assigned two picks (the previous
code only checked participant uniqueness, not slot uniqueness)
- Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all
API routes and league loaders; replace (auth as any).userId casts with
proper const { userId } = await getAuth(args) destructuring
- Remove unused isSnakeDraft = true dead variable from draft.make-pick
- Replace autodraftSettings: any and draftSlots: any[] in draft-utils with
properly typed InferSelectModel / DraftSlot types
- Update force-manual-pick tests: sequence draftPicks.findFirst mock for
the two-call flow; add new tests for status-check and slot-uniqueness
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
* Fix RouterContextProvider type errors in action test files
Cast context argument to RouterContextProvider in test helpers so
ActionFunctionArgs strict typing is satisfied without weakening the
production action signatures back to any.
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
---------
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 19:29:29 -08:00
const { userId } = await getAuth ( args ) ;
2025-10-18 14:55:26 -07:00
2025-10-17 12:30:58 -07:00
if ( ! seasonId ) {
throw new Response ( "Season ID is required" , { status : 400 } ) ;
}
const db = database ( ) ;
// Get season details
const season = await db . query . seasons . findFirst ( {
where : eq ( schema . seasons . id , seasonId ) ,
with : {
league : true ,
teams : true ,
} ,
} ) ;
if ( ! season ) {
throw new Response ( "Season not found" , { status : 404 } ) ;
}
2026-02-23 12:00:04 -08:00
// Resolve user's team and commissioner status (null userId means neither)
const userTeam = userId
? season . teams . find ( ( team : any ) = > team . ownerId === userId )
: undefined ;
const isCommissioner = userId
? await db . query . commissioners . findFirst ( {
where : and (
eq ( schema . commissioners . leagueId , leagueId ) ,
eq ( schema . commissioners . userId , userId )
) ,
} )
: null ;
2025-10-17 12:45:30 -07:00
2026-02-23 12:00:04 -08:00
// Access control: if draft board is not public, require authentication and membership
if ( ! season . league . isPublicDraftBoard ) {
if ( ! userId ) {
throw new Response ( "You must be logged in to view the draft room" , {
status : 401 ,
} ) ;
}
if ( ! userTeam && ! isCommissioner ) {
throw new Response ( "You do not have access to this draft room" , {
status : 403 ,
} ) ;
}
2025-10-17 12:45:30 -07:00
}
2025-10-17 12:30:58 -07:00
// Get draft slots (draft order) - using select instead of query builder
const draftSlots = await db
. select ( {
id : schema.draftSlots.id ,
draftOrder : schema.draftSlots.draftOrder ,
team : schema.teams ,
} )
. from ( schema . draftSlots )
. innerJoin ( schema . teams , eq ( schema . draftSlots . teamId , schema . teams . id ) )
. where ( eq ( schema . draftSlots . seasonId , seasonId ) )
. orderBy ( asc ( schema . draftSlots . draftOrder ) ) ;
// Get all draft picks - using select instead of query builder
const draftPicks = await db
. select ( {
id : schema.draftPicks.id ,
pickNumber : schema.draftPicks.pickNumber ,
round : schema.draftPicks.round ,
pickInRound : schema.draftPicks.pickInRound ,
timeUsed : schema.draftPicks.timeUsed ,
team : schema.teams ,
participant : schema.participants ,
sport : schema.sports ,
} )
. from ( schema . draftPicks )
. innerJoin ( schema . teams , eq ( schema . draftPicks . teamId , schema . teams . id ) )
2025-10-18 14:55:26 -07:00
. innerJoin (
schema . participants ,
eq ( schema . draftPicks . participantId , schema . participants . id )
)
. innerJoin (
schema . sportsSeasons ,
eq ( schema . participants . sportsSeasonId , schema . sportsSeasons . id )
)
. innerJoin (
schema . sports ,
eq ( schema . sportsSeasons . sportId , schema . sports . id )
)
2025-10-17 12:30:58 -07:00
. where ( eq ( schema . draftPicks . seasonId , seasonId ) )
. orderBy ( asc ( schema . draftPicks . pickNumber ) ) ;
2025-10-17 16:58:35 -07:00
// Get sports seasons for this season
const seasonSports = await db . query . seasonSports . findMany ( {
where : eq ( schema . seasonSports . seasonId , seasonId ) ,
} ) ;
2025-10-18 14:55:26 -07:00
2025-10-17 16:58:35 -07:00
const sportsSeasonIds = seasonSports . map ( ( ss ) = > ss . sportsSeasonId ) ;
2025-10-18 14:55:26 -07:00
// Get ALL participants (both drafted and undrafted) - sorted by EV desc, then name
// Filtering will be done on the client side based on the "Show Drafted" toggle
2026-02-20 21:26:27 -08:00
const availableParticipants = sportsSeasonIds . length > 0
? await db
. select ( {
id : schema.participants.id ,
name : schema.participants.name ,
sport : schema.sports ,
} )
. from ( schema . participants )
. innerJoin (
schema . sportsSeasons ,
eq ( schema . participants . sportsSeasonId , schema . sportsSeasons . id )
)
. innerJoin (
schema . sports ,
eq ( schema . sportsSeasons . sportId , schema . sports . id )
)
. where (
sportsSeasonIds . length === 1
? eq ( schema . participants . sportsSeasonId , sportsSeasonIds [ 0 ] )
: inArray ( schema . participants . sportsSeasonId , sportsSeasonIds )
)
. orderBy (
desc ( schema . participants . expectedValue ) ,
asc ( schema . participants . name )
)
: [ ] ;
2025-10-17 12:30:58 -07:00
2025-10-17 12:45:30 -07:00
// Load user's team queue if they have a team
2026-02-23 23:23:24 -08:00
const userQueue = userTeam
? await getTeamQueue ( userTeam . id ) . catch ( ( err ) = > {
console . error ( "[DraftLoader] Failed to load queue, defaulting to empty:" , err ) ;
return [ ] ;
} )
: [ ] ;
2025-10-17 12:45:30 -07:00
2025-10-18 23:13:04 -07:00
// Load draft timers for all teams
const timers = await db . query . draftTimers . findMany ( {
where : eq ( schema . draftTimers . seasonId , seasonId ) ,
} ) ;
2025-10-21 23:22:17 -07:00
// Load autodraft settings for all teams
const autodraftSettings = await db . query . autodraftSettings . findMany ( {
where : eq ( schema . autodraftSettings . seasonId , seasonId ) ,
} ) ;
2026-02-20 21:10:26 -08:00
// Derive user's autodraft settings from the already-loaded list
2025-10-21 23:22:17 -07:00
const userAutodraftSettings = userTeam
2026-02-20 21:10:26 -08:00
? ( autodraftSettings . find ( ( s ) = > s . teamId === userTeam . id ) ? ? null )
2025-10-21 23:22:17 -07:00
: null ;
2026-02-22 16:56:07 -08:00
const ownerMap = await buildOwnerMap ( draftSlots ) ;
2025-10-17 12:30:58 -07:00
return {
season ,
draftSlots ,
draftPicks ,
availableParticipants ,
2025-10-17 12:45:30 -07:00
userTeam ,
userQueue ,
2025-10-18 23:13:04 -07:00
timers ,
2025-10-21 23:22:17 -07:00
autodraftSettings ,
userAutodraftSettings ,
2025-10-17 12:45:30 -07:00
isCommissioner : ! ! isCommissioner ,
currentUserId : userId ,
2026-02-20 21:10:26 -08:00
numFlexPicks : Math.max ( 0 , season . draftRounds - seasonSports . length ) ,
2026-02-22 16:56:07 -08:00
ownerMap ,
2025-10-17 12:30:58 -07:00
} ;
}
export default function DraftRoom() {
2025-10-18 14:55:26 -07:00
const {
season ,
draftSlots ,
draftPicks ,
availableParticipants ,
userTeam ,
userQueue ,
2025-10-18 23:13:04 -07:00
timers ,
2025-10-21 23:22:17 -07:00
autodraftSettings ,
userAutodraftSettings ,
2025-10-18 14:55:26 -07:00
isCommissioner ,
2026-02-20 19:30:53 -08:00
currentUserId ,
2026-02-20 21:10:26 -08:00
numFlexPicks ,
2026-02-22 16:56:07 -08:00
ownerMap ,
2025-10-18 14:55:26 -07:00
} = useLoaderData < typeof loader > ( ) ;
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
const { revalidate } = useRevalidator ( ) ;
2025-10-26 21:01:12 -07:00
const { isConnected , connectionError , isReconnecting , on , off } = useDraftSocket ( season . id , userTeam ? . id ) ;
2026-02-20 19:30:53 -08:00
const {
permissionState : notificationsPermission ,
enabled : notificationsEnabled ,
setEnabled : setNotificationsEnabled ,
mode : notificationsMode ,
setMode : setNotificationsMode ,
sendNotification ,
2026-02-23 12:00:04 -08:00
} = useDraftNotifications ( season . id , currentUserId ? ? "" ) ;
2026-02-20 19:30:53 -08:00
// Use refs so the socket effect doesn't re-register all handlers when the user
// changes notification settings (which would change the sendNotification reference).
const sendNotificationRef = useRef ( sendNotification ) ;
useEffect ( ( ) = > {
sendNotificationRef . current = sendNotification ;
} , [ sendNotification ] ) ;
const notificationsModeRef = useRef ( notificationsMode ) ;
useEffect ( ( ) = > {
notificationsModeRef . current = notificationsMode ;
} , [ notificationsMode ] ) ;
2025-10-17 12:30:58 -07:00
const [ picks , setPicks ] = useState ( draftPicks ) ;
const [ currentPick , setCurrentPick ] = useState ( season . currentPickNumber || 1 ) ;
2025-10-17 16:58:35 -07:00
const [ searchQuery , setSearchQuery ] = useState ( "" ) ;
const [ hideDrafted , setHideDrafted ] = useState ( true ) ;
2025-10-26 21:09:30 -07:00
const [ hideIneligible , setHideIneligible ] = useState ( true ) ;
2025-10-17 16:58:35 -07:00
const [ sportFilter , setSportFilter ] = useState < string > ( "all" ) ;
2026-02-24 11:31:25 -08:00
const [ queue , setQueue ] = useState < QueueItem [ ] > ( userQueue ) ;
2025-10-18 23:19:41 -07:00
const [ isPaused , setIsPaused ] = useState ( season . draftPaused || false ) ;
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
const [ isDraftComplete , setIsDraftComplete ] = useState (
season . status === "active" || season . status === "completed"
) ;
2025-10-25 03:23:41 -07:00
// Sidebar and tabs state
const [ sidebarCollapsed , setSidebarCollapsed ] = useState ( ( ) = > {
if ( typeof window === "undefined" ) return false ;
const stored = localStorage . getItem ( "draftSidebarCollapsed" ) ;
return stored ? JSON . parse ( stored ) : false ;
} ) ;
2025-10-25 18:25:26 -07:00
const [ activeTab , setActiveTab ] = useState < "participants" | "board" | "teams" > ( "participants" ) ;
2026-02-22 22:36:12 -08:00
const [ mobileTab , setMobileTab ] = useState < "lobby" | "board" | "roster" | "controls" > (
! userTeam && isCommissioner ? "board" : "lobby"
) ;
const isMobile = useMediaQuery ( "(max-width: 767px)" ) ;
2025-10-25 03:23:41 -07:00
2025-10-21 23:22:17 -07:00
// Track autodraft status for all teams
const [ autodraftStatus , setAutodraftStatus ] = useState < Record < string , boolean > > ( ( ) = > {
const status : Record < string , boolean > = { } ;
autodraftSettings . forEach ( ( setting : any ) = > {
status [ setting . teamId ] = setting . isEnabled ;
} ) ;
return status ;
} ) ;
// Track connected teams
const [ connectedTeams , setConnectedTeams ] = useState < Set < string > > ( new Set ( ) ) ;
// Track user's autodraft settings
const [ userAutodraft , setUserAutodraft ] = useState ( {
isEnabled : userAutodraftSettings?.isEnabled || false ,
mode : ( userAutodraftSettings ? . mode || "next_pick" ) as "next_pick" | "while_on" ,
} ) ;
2025-10-18 23:13:04 -07:00
const [ teamTimers , setTeamTimers ] = useState < Record < string , number > > ( ( ) = > {
2025-10-20 15:03:11 -07:00
// Initialize with timers from loader data, or use initial time if draft hasn't started
2025-10-18 23:13:04 -07:00
const timersMap : Record < string , number > = { } ;
2025-10-20 15:03:11 -07:00
const initialTime = season . draftInitialTime || 120 ;
if ( timers . length > 0 ) {
// Draft has started, use actual timers
timers . forEach ( ( timer : any ) = > {
timersMap [ timer . teamId ] = timer . timeRemaining ;
} ) ;
} else {
// Draft hasn't started, show initial time for all teams
draftSlots . forEach ( ( slot ) = > {
timersMap [ slot . team . id ] = initialTime ;
} ) ;
}
2025-10-18 23:13:04 -07:00
return timersMap ;
} ) ;
2025-10-18 14:55:26 -07:00
const [ forcePickDialogOpen , setForcePickDialogOpen ] = useState ( false ) ;
const [ selectedPickSlot , setSelectedPickSlot ] = useState < {
pickNumber : number ;
teamId : string ;
} | null > ( null ) ;
2026-02-20 21:10:26 -08:00
const [ dialogSearchQuery , setDialogSearchQuery ] = useState ( "" ) ;
const [ dialogSportFilter , setDialogSportFilter ] = useState < string > ( "all" ) ;
2026-02-20 22:47:29 -08:00
// Replace pick dialog state
const [ replacePickDialogOpen , setReplacePickDialogOpen ] = useState ( false ) ;
const [ replacePickSlot , setReplacePickSlot ] = useState < {
pickNumber : number ;
teamId : string ;
oldParticipantId : string ;
} | null > ( null ) ;
// Rollback confirm dialog state
const [ rollbackConfirmOpen , setRollbackConfirmOpen ] = useState ( false ) ;
const [ rollbackPickNumber , setRollbackPickNumber ] = useState < number | null > ( null ) ;
const [ isRollingBack , setIsRollingBack ] = useState ( false ) ;
2026-02-22 16:16:51 -08:00
// Time bank adjustment dialog state
const [ timeBankDialogOpen , setTimeBankDialogOpen ] = useState ( false ) ;
const [ timeBankTeamId , setTimeBankTeamId ] = useState < string | null > ( null ) ;
const [ timeBankAmount , setTimeBankAmount ] = useState ( "1" ) ;
const [ timeBankUnit , setTimeBankUnit ] = useState < "seconds" | "minutes" | "hours" > ( "minutes" ) ;
const [ timeBankDirection , setTimeBankDirection ] = useState < "add" | "remove" > ( "add" ) ;
const [ isAdjustingTimeBank , setIsAdjustingTimeBank ] = useState ( false ) ;
2026-02-20 21:10:26 -08:00
// Shared transforms for eligibility calculations
const transformedPicks = useMemo (
( ) = >
picks . map ( ( p : any ) = > ( {
teamId : p.team.id ,
participant : {
id : p.participant.id ,
sport : { id : p.sport.id , name : p.sport.name } ,
2025-10-24 21:12:07 -07:00
} ,
2026-02-20 21:10:26 -08:00
} ) ) ,
[ picks ]
) ;
2025-10-24 21:12:07 -07:00
2026-02-20 21:10:26 -08:00
const transformedParticipants = useMemo (
( ) = >
availableParticipants . map ( ( p : any ) = > ( {
id : p.id ,
sport : { id : p.sport.id , name : p.sport.name } ,
} ) ) ,
[ availableParticipants ]
) ;
2025-10-24 21:12:07 -07:00
2026-02-20 21:10:26 -08:00
const seasonSportsData = useMemo ( ( ) = > {
const sportsMap = new Map < string , { id : string ; name : string } > ( ) ;
2026-02-20 21:26:27 -08:00
availableParticipants . forEach ( ( p ) = > {
2025-10-24 21:12:07 -07:00
if ( ! sportsMap . has ( p . sport . id ) ) {
sportsMap . set ( p . sport . id , { id : p.sport.id , name : p.sport.name } ) ;
}
} ) ;
2026-02-20 21:26:27 -08:00
return Array . from ( sportsMap . values ( ) ) . sort ( ( a , b ) = >
a . name . localeCompare ( b . name )
) ;
2026-02-20 21:10:26 -08:00
} , [ availableParticipants ] ) ;
2025-10-24 21:12:07 -07:00
2026-02-20 21:10:26 -08:00
// Calculate draft eligibility for current user's team
const eligibility = useMemo ( ( ) = > {
if ( ! userTeam ) return null ;
const userTeamPicks = transformedPicks . filter (
( p : any ) = > p . teamId === userTeam . id
) ;
2025-10-24 21:12:07 -07:00
return calculateDraftEligibility (
userTeam . id ,
userTeamPicks ,
transformedPicks ,
transformedParticipants ,
seasonSportsData ,
season . draftRounds ,
season . teams
) ;
2026-02-20 21:10:26 -08:00
} , [ userTeam , transformedPicks , transformedParticipants , seasonSportsData , season ] ) ;
2025-10-24 21:12:07 -07:00
// Calculate eligibility for force manual pick dialog (selected team)
const forcePickEligibility = useMemo ( ( ) = > {
if ( ! selectedPickSlot ) return null ;
const selectedTeamPicks = transformedPicks . filter (
( p : any ) = > p . teamId === selectedPickSlot . teamId
) ;
return calculateDraftEligibility (
selectedPickSlot . teamId ,
selectedTeamPicks ,
transformedPicks ,
transformedParticipants ,
seasonSportsData ,
season . draftRounds ,
season . teams
) ;
2026-02-20 21:10:26 -08:00
} , [ selectedPickSlot , transformedPicks , transformedParticipants , seasonSportsData , season ] ) ;
2025-10-24 21:12:07 -07:00
2026-02-20 22:47:29 -08:00
// Calculate eligibility for replace pick dialog — exclude the old pick so its slot is free
const replacePickEligibility = useMemo ( ( ) = > {
if ( ! replacePickSlot ) return null ;
const allPicksExcluding = transformedPicks . filter (
( p : any ) = > p . participant . id !== replacePickSlot . oldParticipantId
) ;
const selectedTeamPicks = allPicksExcluding . filter (
( p : any ) = > p . teamId === replacePickSlot . teamId
) ;
return calculateDraftEligibility (
replacePickSlot . teamId ,
selectedTeamPicks ,
allPicksExcluding ,
transformedParticipants ,
seasonSportsData ,
season . draftRounds ,
season . teams
) ;
} , [ replacePickSlot , transformedPicks , transformedParticipants , seasonSportsData , season ] ) ;
2025-10-17 12:30:58 -07:00
// Listen for new picks from other users
useEffect ( ( ) = > {
const handlePickMade = ( data : any ) = > {
setPicks ( ( prev : any ) = > [ . . . prev , data . pick ] ) ;
setCurrentPick ( data . nextPickNumber ) ;
2026-02-20 19:30:53 -08:00
// No meaningful "next turn" notification once the draft is over
if ( data . isDraftComplete ) return ;
const notifTitle = ` ${ season . league . name } Draft ` ;
const nextSlot = getTeamForPick ( data . nextPickNumber , draftSlots ) ;
const isNowMyTurn = ! ! (
userTeam && nextSlot && nextSlot . team . id === userTeam . id
) ;
if ( isNowMyTurn ) {
sendNotificationRef . current ( notifTitle , ` It's your turn to pick! ` ) ;
} else if (
notificationsModeRef . current === "all_picks" &&
data . pick ? . team ? . id !== userTeam ? . id
) {
// Only notify about other teams' picks, not the user's own
const pickerName = data . pick ? . team ? . name || "A team" ;
const participantName = data . pick ? . participant ? . name || "a participant" ;
sendNotificationRef . current (
notifTitle ,
` ${ pickerName } picked ${ participantName } `
) ;
}
2025-10-17 12:30:58 -07:00
} ;
2025-10-18 14:55:26 -07:00
const handleTimerUpdate = ( data : any ) = > {
2025-10-18 23:13:04 -07:00
// Update the specific team's timer
setTeamTimers ( ( prev ) = > ( {
. . . prev ,
[ data . teamId ] : data . timeRemaining ,
} ) ) ;
2025-10-18 14:55:26 -07:00
} ;
2025-10-18 23:19:41 -07:00
const handleDraftPaused = ( ) = > {
setIsPaused ( true ) ;
} ;
const handleDraftResumed = ( ) = > {
setIsPaused ( false ) ;
} ;
2025-10-18 23:33:13 -07:00
const handleDraftCompleted = ( ) = > {
setIsDraftComplete ( true ) ;
} ;
2025-10-21 23:22:17 -07:00
const handleAutodraftUpdated = ( data : { teamId : string ; isEnabled : boolean ; mode : "next_pick" | "while_on" } ) = > {
setAutodraftStatus ( ( prev ) = > ( {
. . . prev ,
[ data . teamId ] : data . isEnabled ,
} ) ) ;
// Update user's local state if it's their team
if ( userTeam && data . teamId === userTeam . id ) {
setUserAutodraft ( {
isEnabled : data.isEnabled ,
mode : data.mode ,
} ) ;
}
} ;
const handleTeamConnected = ( data : { teamId : string } ) = > {
setConnectedTeams ( ( prev ) = > new Set ( prev ) . add ( data . teamId ) ) ;
} ;
const handleTeamDisconnected = ( data : { teamId : string } ) = > {
setConnectedTeams ( ( prev ) = > {
const newSet = new Set ( prev ) ;
newSet . delete ( data . teamId ) ;
return newSet ;
} ) ;
} ;
2025-10-24 21:31:57 -07:00
const handleConnectedTeamsList = ( data : { teamIds : string [ ] } ) = > {
setConnectedTeams ( new Set ( data . teamIds ) ) ;
} ;
2025-10-24 21:46:55 -07:00
const handleParticipantRemovedFromQueues = ( data : { participantId : string } ) = > {
setQueue ( ( prev : any ) = >
prev . filter ( ( item : any ) = > item . participantId !== data . participantId )
) ;
} ;
2026-02-20 22:47:29 -08:00
const handlePickReplaced = ( data : any ) = > {
setPicks ( ( prev : any ) = >
prev . map ( ( p : any ) = > ( p . pickNumber === data . pickNumber ? data.pick : p ) )
) ;
} ;
const handleDraftRolledBack = ( data : any ) = > {
setPicks ( ( prev : any ) = >
prev . filter ( ( p : any ) = > p . pickNumber < data . pickNumber )
) ;
setCurrentPick ( data . pickNumber ) ;
setIsDraftComplete ( false ) ;
setIsPaused ( false ) ;
} ;
2025-10-17 12:30:58 -07:00
on ( "pick-made" , handlePickMade ) ;
2025-10-18 14:55:26 -07:00
on ( "timer-update" , handleTimerUpdate ) ;
2025-10-18 23:19:41 -07:00
on ( "draft-paused" , handleDraftPaused ) ;
on ( "draft-resumed" , handleDraftResumed ) ;
2025-10-18 23:33:13 -07:00
on ( "draft-completed" , handleDraftCompleted ) ;
2025-10-21 23:22:17 -07:00
on ( "autodraft-updated" , handleAutodraftUpdated ) ;
on ( "team-connected" , handleTeamConnected ) ;
on ( "team-disconnected" , handleTeamDisconnected ) ;
2025-10-24 21:31:57 -07:00
on ( "connected-teams-list" , handleConnectedTeamsList ) ;
2025-10-24 21:46:55 -07:00
on ( "participant-removed-from-queues" , handleParticipantRemovedFromQueues ) ;
2026-02-20 22:47:29 -08:00
on ( "pick-replaced" , handlePickReplaced ) ;
on ( "draft-rolled-back" , handleDraftRolledBack ) ;
2025-10-17 12:30:58 -07:00
return ( ) = > {
off ( "pick-made" , handlePickMade ) ;
2025-10-18 14:55:26 -07:00
off ( "timer-update" , handleTimerUpdate ) ;
2025-10-18 23:19:41 -07:00
off ( "draft-paused" , handleDraftPaused ) ;
off ( "draft-resumed" , handleDraftResumed ) ;
2025-10-18 23:33:13 -07:00
off ( "draft-completed" , handleDraftCompleted ) ;
2025-10-21 23:22:17 -07:00
off ( "autodraft-updated" , handleAutodraftUpdated ) ;
off ( "team-connected" , handleTeamConnected ) ;
off ( "team-disconnected" , handleTeamDisconnected ) ;
2025-10-24 21:31:57 -07:00
off ( "connected-teams-list" , handleConnectedTeamsList ) ;
2025-10-24 21:46:55 -07:00
off ( "participant-removed-from-queues" , handleParticipantRemovedFromQueues ) ;
2026-02-20 22:47:29 -08:00
off ( "pick-replaced" , handlePickReplaced ) ;
off ( "draft-rolled-back" , handleDraftRolledBack ) ;
2025-10-17 12:30:58 -07:00
} ;
2026-02-20 19:30:53 -08:00
} , [ on , off , currentPick , userTeam , draftSlots , season . league . name ] ) ;
2025-10-17 12:30:58 -07:00
2025-10-25 03:23:41 -07:00
// Persist sidebar collapsed state
useEffect ( ( ) = > {
if ( typeof window !== "undefined" ) {
localStorage . setItem ( "draftSidebarCollapsed" , JSON . stringify ( sidebarCollapsed ) ) ;
}
} , [ sidebarCollapsed ] ) ;
2025-10-17 17:42:40 -07:00
// Queue handlers
const handleAddToQueue = async ( participantId : string ) = > {
if ( ! userTeam ) return ;
2025-10-25 21:02:16 -07:00
// Check if participant is already in queue (client-side)
2026-02-24 11:31:25 -08:00
const alreadyInQueue = queue . some ( ( item ) = > item . participantId === participantId ) ;
2025-10-25 21:02:16 -07:00
if ( alreadyInQueue ) {
toast . error ( "This participant is already in your queue" ) ;
return ;
}
// Optimistic update - create a temporary queue item
2026-02-24 11:31:25 -08:00
const now = new Date ( ) ;
const tempQueueItem : QueueItem = {
id : ` temp- ${ Date . now ( ) } ` ,
2025-10-25 21:02:16 -07:00
participantId ,
queuePosition : queue.length + 1 ,
2026-02-24 11:31:25 -08:00
seasonId : season.id ,
teamId : userTeam.id ,
createdAt : now ,
updatedAt : now ,
2025-10-25 21:02:16 -07:00
} ;
// Immediately update the UI
2026-02-24 11:31:25 -08:00
setQueue ( ( prev ) = > [ . . . prev , tempQueueItem ] ) ;
2025-10-25 21:02:16 -07:00
2025-10-17 17:42:40 -07:00
const formData = new FormData ( ) ;
formData . append ( "seasonId" , season . id ) ;
formData . append ( "teamId" , userTeam . id ) ;
formData . append ( "participantId" , participantId ) ;
2025-10-25 21:02:16 -07:00
try {
const response = await fetch ( "/api/queue/add" , {
method : "POST" ,
body : formData ,
} ) ;
2025-10-17 17:42:40 -07:00
2025-10-25 21:02:16 -07:00
if ( response . ok ) {
const data = await response . json ( ) ;
// Replace temp item with real item from server
2026-02-24 11:31:25 -08:00
setQueue ( ( prev ) = >
prev . map ( ( item ) = >
2025-10-25 21:02:16 -07:00
item . id === tempQueueItem . id ? data.queueItem : item
)
) ;
} else {
// Revert the optimistic update on error
2026-02-24 11:31:25 -08:00
setQueue ( ( prev ) = > prev . filter ( ( item ) = > item . id !== tempQueueItem . id ) ) ;
2025-10-25 21:02:16 -07:00
const error = await response . json ( ) ;
toast . error ( error . error || "Failed to add to queue" ) ;
}
} catch ( error ) {
// Revert the optimistic update on network error
2026-02-24 11:31:25 -08:00
setQueue ( ( prev ) = > prev . filter ( ( item ) = > item . id !== tempQueueItem . id ) ) ;
2025-10-25 21:02:16 -07:00
toast . error ( "Network error - failed to add to queue" ) ;
2025-10-17 17:42:40 -07:00
}
} ;
const handleRemoveFromQueue = async ( queueId : string ) = > {
if ( ! userTeam ) return ;
2026-02-24 11:31:25 -08:00
// Optimistically remove the item immediately
let previousQueue : QueueItem [ ] = [ ] ;
setQueue ( ( prev ) = > {
previousQueue = prev ;
return prev . filter ( ( item ) = > item . id !== queueId ) ;
} ) ;
2025-10-17 17:42:40 -07:00
const formData = new FormData ( ) ;
formData . append ( "queueId" , queueId ) ;
formData . append ( "teamId" , userTeam . id ) ;
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
try {
const response = await fetch ( "/api/queue/remove" , {
method : "POST" ,
body : formData ,
} ) ;
2025-10-17 17:42:40 -07:00
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
if ( response . ok ) {
const data = await response . json ( ) ;
setQueue ( data . queue ) ;
} else {
2026-02-24 11:31:25 -08:00
setQueue ( previousQueue ) ;
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
const error = await response . json ( ) ;
toast . error ( error . error || "Failed to remove from queue" ) ;
}
} catch {
2026-02-24 11:31:25 -08:00
setQueue ( previousQueue ) ;
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
toast . error ( "Network error - failed to remove from queue" ) ;
2025-10-17 17:42:40 -07:00
}
} ;
2025-10-25 21:02:16 -07:00
const handleReorderQueue = async ( participantIds : string [ ] ) = > {
2025-10-17 17:42:40 -07:00
if ( ! userTeam ) return ;
2026-02-24 11:31:25 -08:00
// Optimistically reorder the queue immediately
let previousQueue : QueueItem [ ] = [ ] ;
setQueue ( ( prev ) = > {
previousQueue = prev ;
return participantIds
. map ( ( pid ) = > prev . find ( ( item ) = > item . participantId === pid ) )
. filter ( ( item ) : item is QueueItem = > item !== undefined ) ;
} ) ;
2025-10-17 17:42:40 -07:00
const formData = new FormData ( ) ;
formData . append ( "teamId" , userTeam . id ) ;
2025-10-25 21:02:16 -07:00
formData . append ( "seasonId" , season . id ) ;
formData . append ( "participantIds" , JSON . stringify ( participantIds ) ) ;
2025-10-17 17:42:40 -07:00
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
try {
const response = await fetch ( "/api/queue/reorder" , {
method : "POST" ,
body : formData ,
} ) ;
2025-10-17 17:42:40 -07:00
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
if ( response . ok ) {
const data = await response . json ( ) ;
setQueue ( data . queue ) ;
} else {
const error = await response . json ( ) ;
2026-02-24 11:31:25 -08:00
setQueue ( previousQueue ) ;
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
toast . error ( error . error || "Failed to reorder queue" ) ;
}
} catch {
2026-02-24 11:31:25 -08:00
setQueue ( previousQueue ) ;
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
toast . error ( "Network error - failed to reorder queue" ) ;
2025-10-17 17:42:40 -07:00
}
} ;
2025-10-18 14:55:26 -07:00
const handleStartDraft = async ( ) = > {
const formData = new FormData ( ) ;
formData . append ( "seasonId" , season . id ) ;
const response = await fetch ( "/api/draft/start" , {
method : "POST" ,
body : formData ,
} ) ;
if ( response . ok ) {
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
revalidate ( ) ;
} else {
const error = await response . json ( ) ;
toast . error ( error . error || "Failed to start draft" ) ;
2025-10-18 14:55:26 -07:00
}
} ;
2025-10-18 23:19:41 -07:00
const handlePauseDraft = async ( ) = > {
const formData = new FormData ( ) ;
formData . append ( "seasonId" , season . id ) ;
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
try {
const response = await fetch ( "/api/draft/pause" , {
method : "POST" ,
body : formData ,
} ) ;
2025-10-18 23:19:41 -07:00
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
if ( response . ok ) {
setIsPaused ( true ) ;
} else {
const error = await response . json ( ) ;
toast . error ( error . error || "Failed to pause draft" ) ;
}
} catch {
toast . error ( "Network error - failed to pause draft" ) ;
2025-10-18 23:19:41 -07:00
}
} ;
const handleResumeDraft = async ( ) = > {
const formData = new FormData ( ) ;
formData . append ( "seasonId" , season . id ) ;
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
try {
const response = await fetch ( "/api/draft/resume" , {
method : "POST" ,
body : formData ,
} ) ;
2025-10-18 23:19:41 -07:00
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
if ( response . ok ) {
setIsPaused ( false ) ;
} else {
const error = await response . json ( ) ;
toast . error ( error . error || "Failed to resume draft" ) ;
}
} catch {
toast . error ( "Network error - failed to resume draft" ) ;
2025-10-18 23:19:41 -07:00
}
} ;
2025-10-18 14:55:26 -07:00
const handleMakePick = async ( participantId : string ) = > {
const formData = new FormData ( ) ;
formData . append ( "seasonId" , season . id ) ;
formData . append ( "participantId" , participantId ) ;
const response = await fetch ( "/api/draft/make-pick" , {
method : "POST" ,
body : formData ,
} ) ;
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
if ( ! response . ok ) {
2025-10-18 14:55:26 -07:00
const error = await response . json ( ) ;
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
toast . error ( error . error || "Failed to make pick" ) ;
2025-10-18 14:55:26 -07:00
}
} ;
const handleForceAutopick = async ( pickNumber : number , teamId : string ) = > {
const formData = new FormData ( ) ;
formData . append ( "seasonId" , season . id ) ;
formData . append ( "teamId" , teamId ) ;
formData . append ( "pickNumber" , pickNumber . toString ( ) ) ;
const response = await fetch ( "/api/draft/force-autopick" , {
method : "POST" ,
body : formData ,
} ) ;
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
if ( ! response . ok ) {
2025-10-18 14:55:26 -07:00
const error = await response . json ( ) ;
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
toast . error ( error . error || "Failed to force autopick" ) ;
2025-10-18 14:55:26 -07:00
}
} ;
const handleForceManualPick = async ( participantId : string ) = > {
if ( ! selectedPickSlot ) return ;
const formData = new FormData ( ) ;
formData . append ( "seasonId" , season . id ) ;
formData . append ( "teamId" , selectedPickSlot . teamId ) ;
formData . append ( "participantId" , participantId ) ;
formData . append ( "pickNumber" , selectedPickSlot . pickNumber . toString ( ) ) ;
const response = await fetch ( "/api/draft/force-manual-pick" , {
method : "POST" ,
body : formData ,
} ) ;
if ( response . ok ) {
setForcePickDialogOpen ( false ) ;
setSelectedPickSlot ( null ) ;
} else {
const error = await response . json ( ) ;
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
toast . error ( error . error || "Failed to force manual pick" ) ;
2025-10-18 14:55:26 -07:00
}
} ;
2026-02-20 22:47:29 -08:00
const handleReplacePickOpen = ( pickNumber : number , teamId : string ) = > {
const pick = picks . find ( ( p : any ) = > p . pickNumber === pickNumber ) ;
if ( ! pick ) {
toast . error ( "Pick not found — try refreshing the page" ) ;
return ;
}
setReplacePickSlot ( { pickNumber , teamId , oldParticipantId : pick.participant.id } ) ;
setDialogSearchQuery ( "" ) ;
setDialogSportFilter ( "all" ) ;
setReplacePickDialogOpen ( true ) ;
} ;
const handleReplacePick = async ( participantId : string ) = > {
if ( ! replacePickSlot ) return ;
const formData = new FormData ( ) ;
formData . append ( "seasonId" , season . id ) ;
formData . append ( "teamId" , replacePickSlot . teamId ) ;
formData . append ( "participantId" , participantId ) ;
formData . append ( "pickNumber" , replacePickSlot . pickNumber . toString ( ) ) ;
const response = await fetch ( "/api/draft/replace-pick" , {
method : "POST" ,
body : formData ,
} ) ;
if ( response . ok ) {
setReplacePickDialogOpen ( false ) ;
setReplacePickSlot ( null ) ;
} else {
const error = await response . json ( ) ;
toast . error ( error . error || "Failed to replace pick" ) ;
}
} ;
const handleRollbackToPick = ( pickNumber : number ) = > {
setRollbackPickNumber ( pickNumber ) ;
setRollbackConfirmOpen ( true ) ;
} ;
const handleConfirmRollback = async ( ) = > {
if ( ! rollbackPickNumber || isRollingBack ) return ;
setIsRollingBack ( true ) ;
const formData = new FormData ( ) ;
formData . append ( "seasonId" , season . id ) ;
formData . append ( "pickNumber" , rollbackPickNumber . toString ( ) ) ;
const response = await fetch ( "/api/draft/rollback" , {
method : "POST" ,
body : formData ,
} ) ;
setIsRollingBack ( false ) ;
if ( response . ok ) {
setRollbackConfirmOpen ( false ) ;
setRollbackPickNumber ( null ) ;
} else {
const error = await response . json ( ) ;
toast . error ( error . error || "Failed to roll back draft" ) ;
}
} ;
2026-02-22 16:16:51 -08:00
const handleAdjustTimeBankOpen = ( teamId : string ) = > {
setTimeBankTeamId ( teamId ) ;
setTimeBankAmount ( "1" ) ;
setTimeBankUnit ( "minutes" ) ;
setTimeBankDirection ( "add" ) ;
setTimeBankDialogOpen ( true ) ;
} ;
const handleConfirmAdjustTimeBank = async ( ) = > {
if ( ! timeBankTeamId || isAdjustingTimeBank ) return ;
const amount = parseFloat ( timeBankAmount ) ;
if ( isNaN ( amount ) || amount <= 0 ) {
toast . error ( "Please enter a valid positive amount" ) ;
return ;
}
const unitMultipliers = { seconds : 1 , minutes : 60 , hours : 3600 } ;
const totalSeconds = Math . round ( amount * unitMultipliers [ timeBankUnit ] ) ;
if ( totalSeconds === 0 ) {
toast . error ( "Adjustment rounds to 0 seconds — please enter a larger value" ) ;
return ;
}
const adjustment = timeBankDirection === "add" ? totalSeconds : - totalSeconds ;
setIsAdjustingTimeBank ( true ) ;
try {
const formData = new FormData ( ) ;
formData . append ( "seasonId" , season . id ) ;
formData . append ( "teamId" , timeBankTeamId ) ;
formData . append ( "adjustment" , adjustment . toString ( ) ) ;
const response = await fetch ( "/api/draft/adjust-time-bank" , {
method : "POST" ,
body : formData ,
} ) ;
if ( response . ok ) {
const data = await response . json ( ) ;
setTeamTimers ( ( prev ) = > ( { . . . prev , [ timeBankTeamId ] : data . timeRemaining } ) ) ;
const teamName = draftSlots . find ( ( s ) = > s . team . id === timeBankTeamId ) ? . team . name ;
toast . success ( ` Time bank adjusted for ${ teamName } ` ) ;
setTimeBankDialogOpen ( false ) ;
setTimeBankTeamId ( null ) ;
} else {
const error = await response . json ( ) ;
toast . error ( error . error || "Failed to adjust time bank" ) ;
}
} catch {
toast . error ( "Network error — failed to adjust time bank" ) ;
} finally {
setIsAdjustingTimeBank ( false ) ;
}
} ;
2026-02-22 22:36:12 -08:00
const handleForceManualPickOpen = ( pickNumber : number , teamId : string ) = > {
setSelectedPickSlot ( { pickNumber , teamId } ) ;
setDialogSearchQuery ( "" ) ;
setDialogSportFilter ( "all" ) ;
setForcePickDialogOpen ( true ) ;
} ;
2025-10-17 12:51:47 -07:00
// Calculate current round
2026-02-20 11:16:34 -08:00
const currentRound = draftSlots . length > 0 ? Math . ceil ( currentPick / draftSlots . length ) : 1 ;
2025-10-17 12:30:58 -07:00
2025-10-18 14:55:26 -07:00
// Determine whose turn it is
2026-02-20 19:30:53 -08:00
const currentDraftSlot = getTeamForPick ( currentPick , draftSlots ) ;
2025-10-21 23:22:17 -07:00
const isMyTurn = ! ! (
userTeam && currentDraftSlot && currentDraftSlot . team . id === userTeam . id
) ;
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
const canPick = isMyTurn && season . status === "draft" && ! isPaused ; // Only team owner on their turn when draft is active
2025-10-18 14:55:26 -07:00
Add draft clock UI, Fischer increment timer logic, and security fixes (#19)
- Add prominent clock badge to tab bar (lights up on your turn) with
correct Fischer increment chess-clock model: bank starts at initialTime,
+= incrementTime after each pick, other teams' banks untouched
- Extract pure timer helpers to app/lib/draft-timer.ts and add 29 unit
tests covering formatClockTime, calculateTimeAfterPick, getTimerColorClass,
and full snake-draft lifecycle regression scenarios
- Fix make-pick.ts: add status !== 'draft' and draftPaused server guards
- Fix draft-utils.ts: replace (global as any).__socketIO with getSocketIO(),
fix timeUsed to store actual timeRemaining at pick moment (not always 120),
add null timer warning, move timer fetch before pick insert
- Fix draft.start.ts: batch timer inserts, guard against empty draftSlots
- Fix DraftGridSection: memoize currentTeamId, widen teamTimers type to
Record<string, number | undefined>
- Fix duplicate animate-pulse (getTimerColorClass already includes it)
- Clamp negative seconds in formatClockTime to guard against timer drift
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:51:12 -08:00
const currentClockTime = currentDraftSlot ? teamTimers [ currentDraftSlot . team . id ] : undefined ;
const currentClockColor = getTimerColorClass ( currentClockTime ) ;
2025-10-17 12:51:47 -07:00
// Generate snake draft grid structure
2026-02-20 21:10:26 -08:00
const draftGrid = useMemo ( ( ) = > {
2025-10-17 12:51:47 -07:00
const teamCount = draftSlots . length ;
const rounds = season . draftRounds ;
2025-10-18 14:55:26 -07:00
const grid : Array <
Array < {
pickNumber : number ;
round : number ;
pickInRound : number ;
teamId : string ;
pick? : any ;
} >
> = [ ] ;
2025-10-17 12:51:47 -07:00
for ( let round = 1 ; round <= rounds ; round ++ ) {
const roundPicks = [ ] ;
const isOddRound = round % 2 === 1 ;
2025-10-18 14:55:26 -07:00
2025-10-17 12:51:47 -07:00
for ( let i = 0 ; i < teamCount ; i ++ ) {
const pickInRound = i + 1 ;
const teamIndex = isOddRound ? i : teamCount - 1 - i ;
const pickNumber = ( round - 1 ) * teamCount + pickInRound ;
const teamId = draftSlots [ teamIndex ] ? . team . id ;
2025-10-18 14:55:26 -07:00
2025-10-17 12:51:47 -07:00
const pick = picks . find ( ( p : any ) = > p . pickNumber === pickNumber ) ;
2025-10-18 14:55:26 -07:00
2026-02-20 21:10:26 -08:00
roundPicks . push ( { pickNumber , round , pickInRound , teamId , pick } ) ;
2025-10-17 12:51:47 -07:00
}
grid . push ( roundPicks ) ;
}
2025-10-18 14:55:26 -07:00
2025-10-17 12:51:47 -07:00
return grid ;
2026-02-20 21:10:26 -08:00
} , [ picks , draftSlots , season . draftRounds ] ) ;
2025-10-17 12:51:47 -07:00
2025-10-17 16:58:35 -07:00
// Get drafted participant IDs for filtering
2026-02-20 21:10:26 -08:00
const draftedParticipantIds = useMemo (
( ) = > new Set ( picks . map ( ( p : any ) = > p . participant . id ) ) ,
[ picks ]
2025-10-18 14:55:26 -07:00
) ;
2025-10-17 16:58:35 -07:00
// Get unique sports for filter dropdown
2026-02-20 21:10:26 -08:00
const uniqueSports = useMemo (
( ) = >
Array . from (
new Set ( availableParticipants . map ( ( p : any ) = > p . sport . name ) )
) . sort ( ) ,
[ availableParticipants ]
) ;
// Participants filtered for the force pick dialog (always excludes drafted, uses dialog-local filters)
const dialogFilteredParticipants = useMemo (
( ) = >
availableParticipants . filter ( ( p : any ) = > {
if ( draftedParticipantIds . has ( p . id ) ) return false ;
if (
dialogSearchQuery &&
! p . name . toLowerCase ( ) . includes ( dialogSearchQuery . toLowerCase ( ) )
)
return false ;
if ( dialogSportFilter !== "all" && p . sport . name !== dialogSportFilter )
return false ;
return true ;
} ) ,
[ availableParticipants , draftedParticipantIds , dialogSearchQuery , dialogSportFilter ]
) ;
2025-10-17 16:58:35 -07:00
2026-02-20 22:47:29 -08:00
// Participants for the replace pick dialog — excludes drafted except the old pick (which is freed)
const replaceDialogFilteredParticipants = useMemo ( ( ) = > {
if ( ! replacePickSlot ) return [ ] ;
return availableParticipants . filter ( ( p : any ) = > {
// Exclude drafted players, but allow the old participant through since they'll be freed
if ( p . id !== replacePickSlot . oldParticipantId && draftedParticipantIds . has ( p . id ) )
return false ;
if (
dialogSearchQuery &&
! p . name . toLowerCase ( ) . includes ( dialogSearchQuery . toLowerCase ( ) )
)
return false ;
if ( dialogSportFilter !== "all" && p . sport . name !== dialogSportFilter )
return false ;
return true ;
} ) ;
} , [ availableParticipants , replacePickSlot , draftedParticipantIds , dialogSearchQuery , dialogSportFilter ] ) ;
2025-10-26 21:09:30 -07:00
// Filter participants based on search, sport, drafted status, and eligibility
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
const filteredParticipants = useMemo (
( ) = >
availableParticipants . filter ( ( participant : any ) = > {
// Drafted filter - hide drafted participants by default
if ( hideDrafted && draftedParticipantIds . has ( participant . id ) ) {
2025-10-26 21:09:30 -07:00
return false ;
}
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
// Eligibility filter - hide ineligible participants by default (if user has a team)
if ( hideIneligible && eligibility ) {
const isEligible = eligibility . eligibleSportIds . has ( participant . sport . id ) ;
if ( ! isEligible ) {
return false ;
}
}
// Search filter
if (
searchQuery &&
! participant . name . toLowerCase ( ) . includes ( searchQuery . toLowerCase ( ) )
) {
return false ;
}
// Sport filter
if ( sportFilter !== "all" && participant . sport . name !== sportFilter ) {
return false ;
}
return true ;
} ) ,
[ availableParticipants , hideDrafted , hideIneligible , eligibility , draftedParticipantIds , searchQuery , sportFilter ]
2025-10-18 14:55:26 -07:00
) ;
2025-10-17 16:58:35 -07:00
2026-02-22 22:36:12 -08:00
// Shared component props — defined once to avoid duplication between desktop and mobile layouts
const availableParticipantsSectionProps = {
participants : filteredParticipants ,
searchQuery ,
sportFilter ,
hideDrafted ,
hideIneligible ,
uniqueSports ,
draftedParticipantIds ,
queue ,
eligibility ,
canPick ,
hasTeam : ! ! userTeam ,
onSearchChange : setSearchQuery ,
onSportFilterChange : setSportFilter ,
onHideDraftedChange : setHideDrafted ,
onHideIneligibleChange : setHideIneligible ,
onMakePick : handleMakePick ,
onAddToQueue : handleAddToQueue ,
onRemoveFromQueue : handleRemoveFromQueue ,
} ;
const draftGridSectionProps = {
draftSlots ,
draftGrid ,
currentPick ,
teamTimers ,
autodraftStatus ,
connectedTeams ,
isCommissioner ,
ownerMap ,
onAdjustTimeBankOpen : isCommissioner ? handleAdjustTimeBankOpen : undefined ,
onForceAutopick : handleForceAutopick ,
onForceManualPickOpen : handleForceManualPickOpen ,
onReplacePick : isCommissioner ? handleReplacePickOpen : undefined ,
onRollbackToPick : isCommissioner && ! isDraftComplete ? handleRollbackToPick : undefined ,
} ;
const teamsDraftedGridProps = {
draftSlots ,
picks ,
sports : seasonSportsData ,
season : { numFlexPicks } ,
ownerMap ,
} ;
const queueSectionProps = userTeam
? {
queue ,
availableParticipants ,
seasonId : season.id ,
teamId : userTeam.id ,
isMyTurn ,
userAutodraft ,
onRemoveFromQueue : handleRemoveFromQueue ,
onAutodraftUpdate : ( isEnabled : boolean , mode : "next_pick" | "while_on" ) = > {
setUserAutodraft ( { isEnabled , mode } ) ;
} ,
onReorder : handleReorderQueue ,
}
: null ;
2025-10-17 12:30:58 -07:00
return (
2026-02-23 08:22:00 -08:00
< div className = "h-dvh bg-background flex flex-col overflow-hidden" >
2025-10-18 23:33:13 -07:00
{ /* Draft Completion Banner */ }
{ isDraftComplete && (
2026-02-20 19:26:11 -08:00
< div className = "bg-emerald-500/20 border-b border-emerald-500/30 text-emerald-400 px-4 py-3 text-center font-semibold flex-shrink-0" >
2025-10-18 23:33:13 -07:00
🎉 Draft Complete ! The season is now active .
2025-10-20 15:03:11 -07:00
{ season . league . isPublicDraftBoard && (
< >
{ " " }
< a
href = { ` /leagues/ ${ season . leagueId } /draft-board/ ${ season . id } ` }
2026-02-20 19:26:11 -08:00
className = "underline hover:text-emerald-300"
2025-10-20 15:03:11 -07:00
>
View Draft Board
< / a >
< / >
) }
2025-10-18 23:33:13 -07:00
< / div >
) }
2025-10-17 12:45:30 -07:00
{ /* Standalone Header - No Main Nav */ }
2025-10-25 03:23:41 -07:00
< div className = "border-b bg-card flex-shrink-0" >
2025-10-17 12:51:47 -07:00
< div className = "w-full px-4 py-4" >
2025-10-17 12:45:30 -07:00
< div className = "flex items-center justify-between" >
2025-10-25 03:23:41 -07:00
< div className = "flex items-center gap-3" >
{ /* Mobile menu button - only show if user has team and sidebar is collapsed */ }
{ userTeam && sidebarCollapsed && (
< Button
variant = "ghost"
size = "icon"
onClick = { ( ) = > setSidebarCollapsed ( false ) }
className = "lg:hidden"
aria - label = "Open sidebar"
>
< svg
xmlns = "http://www.w3.org/2000/svg"
width = "24"
height = "24"
viewBox = "0 0 24 24"
fill = "none"
stroke = "currentColor"
strokeWidth = "2"
strokeLinecap = "round"
strokeLinejoin = "round"
>
< line x1 = "3" y1 = "12" x2 = "21" y2 = "12" > < / line >
< line x1 = "3" y1 = "6" x2 = "21" y2 = "6" > < / line >
< line x1 = "3" y1 = "18" x2 = "21" y2 = "18" > < / line >
< / svg >
< / Button >
) }
< div >
2026-02-22 22:36:12 -08:00
< h1 className = "text-lg md:text-3xl font-bold" >
2025-10-25 03:23:41 -07:00
{ season . league . name } - { season . year } Draft
< / h1 >
< div className = "flex gap-4 text-sm text-muted-foreground mt-1" >
< span > Round : { currentRound } < / span >
< span > Pick : { currentPick } < / span >
< span className = "capitalize" >
{ season . status . replace ( "_" , " " ) }
< / span >
< / div >
2025-10-17 12:45:30 -07:00
< / div >
< / div >
< div className = "flex items-center gap-3" >
2026-02-22 22:36:12 -08:00
{ /* Commissioner Controls - hidden on mobile (shown in Controls tab) */ }
2025-10-18 14:55:26 -07:00
{ isCommissioner && season . status === "pre_draft" && (
2026-02-22 22:36:12 -08:00
< Button className = "hidden md:flex" onClick = { handleStartDraft } > Start Draft < / Button >
2025-10-18 14:55:26 -07:00
) }
2026-02-22 22:36:12 -08:00
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
{ isCommissioner && season . status === "draft" && ! isDraftComplete && (
2026-02-22 22:36:12 -08:00
< div className = "hidden md:flex" >
2025-10-18 23:19:41 -07:00
{ isPaused ? (
< Button onClick = { handleResumeDraft } variant = "default" >
Resume Draft
< / Button >
) : (
< Button onClick = { handlePauseDraft } variant = "outline" >
Pause Draft
< / Button >
) }
2026-02-22 22:36:12 -08:00
< / div >
2025-10-18 23:19:41 -07:00
) }
2025-10-18 14:55:26 -07:00
2025-10-17 12:45:30 -07:00
< div className = "flex items-center gap-2" >
< div
className = { ` w-3 h-3 rounded-full ${
2026-02-20 19:26:11 -08:00
isConnected ? "bg-emerald-500" : "bg-coral-accent"
2025-10-17 12:45:30 -07:00
} ` }
/ >
2026-02-22 22:36:12 -08:00
< span className = "hidden sm:inline text-sm font-medium" >
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
{ isConnected ? "Connected" : "Disconnected" }
2025-10-17 12:45:30 -07:00
< / span >
< / div >
2026-02-22 22:36:12 -08:00
< Button variant = "outline" asChild className = "hidden md:flex" >
2025-10-18 14:55:26 -07:00
< Link to = { ` /leagues/ ${ season . leagueId } ` } > Exit Draft Room < / Link >
2025-10-17 12:45:30 -07:00
< / Button >
< / div >
2025-10-17 12:30:58 -07:00
< / div >
< / div >
< / div >
2026-02-22 22:36:12 -08:00
{ /* Mobile On Clock bar */ }
{ season . status === "draft" && ! isDraftComplete && currentDraftSlot && (
< div className = { ` md:hidden flex-shrink-0 flex items-center justify-between px-4 py-2 border-b ${
isMyTurn
? "bg-electric/25 border-electric"
: "bg-muted/30"
} ` }>
< div >
< div className = { ` text-xs font-bold uppercase tracking-wider ${ isMyTurn ? "text-electric" : "text-muted-foreground" } ` } >
{ isMyTurn ? "Your Turn!" : "On the Clock" }
< / div >
< div className = "text-sm font-bold" > { currentDraftSlot . team . name } < / div >
< / div >
{ isPaused ? (
< span className = "text-sm font-bold text-amber-accent" > Paused < / span >
) : (
< span className = { ` text-xl font-mono font-bold tabular-nums ${ currentClockColor } ` } >
{ formatClockTime ( currentClockTime ) }
< / span >
) }
< / div >
) }
2025-10-25 03:23:41 -07:00
2026-02-22 22:36:12 -08:00
{ /* Main Content — single layout tree based on isMobile to avoid duplicate component instances */ }
< div className = "flex-1 overflow-hidden" >
{ isMobile ? (
< div className = "flex h-full overflow-hidden flex-col" >
{ mobileTab === "lobby" && (
< AvailableParticipantsSection { ...availableParticipantsSectionProps } / >
) }
{ mobileTab === "board" && (
< DraftGridSection { ...draftGridSectionProps } / >
) }
{ mobileTab === "roster" && (
< TeamsDraftedGrid { ...teamsDraftedGridProps } / >
) }
{ mobileTab === "controls" && (
< div className = "h-full overflow-y-auto p-4 space-y-6" >
{ isCommissioner && season . status === "pre_draft" && (
< Button className = "w-full min-h-[48px]" onClick = { handleStartDraft } >
Start Draft
< / Button >
) }
{ isCommissioner && season . status === "draft" && ! isDraftComplete && (
isPaused ? (
< Button className = "w-full min-h-[48px]" onClick = { handleResumeDraft } >
Resume Draft
< / Button >
) : (
< Button className = "w-full min-h-[48px]" variant = "outline" onClick = { handlePauseDraft } >
Pause Draft
< / Button >
)
Add draft clock UI, Fischer increment timer logic, and security fixes (#19)
- Add prominent clock badge to tab bar (lights up on your turn) with
correct Fischer increment chess-clock model: bank starts at initialTime,
+= incrementTime after each pick, other teams' banks untouched
- Extract pure timer helpers to app/lib/draft-timer.ts and add 29 unit
tests covering formatClockTime, calculateTimeAfterPick, getTimerColorClass,
and full snake-draft lifecycle regression scenarios
- Fix make-pick.ts: add status !== 'draft' and draftPaused server guards
- Fix draft-utils.ts: replace (global as any).__socketIO with getSocketIO(),
fix timeUsed to store actual timeRemaining at pick moment (not always 120),
add null timer warning, move timer fetch before pick insert
- Fix draft.start.ts: batch timer inserts, guard against empty draftSlots
- Fix DraftGridSection: memoize currentTeamId, widen teamTimers type to
Record<string, number | undefined>
- Fix duplicate animate-pulse (getTimerColorClass already includes it)
- Clamp negative seconds in formatClockTime to guard against timer drift
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:51:12 -08:00
) }
2025-10-25 03:23:41 -07:00
2026-02-22 22:36:12 -08:00
{ queueSectionProps && (
< section >
< h2 className = "font-semibold text-sm mb-2" > My Queue < / h2 >
< QueueSection { ...queueSectionProps } / >
< / section >
) }
< section >
2026-02-23 22:25:28 -08:00
< h2 className = "font-semibold text-sm mb-2" > Picks < / h2 >
2026-02-22 22:36:12 -08:00
< SidebarRecentPicks picks = { picks } / >
< / section >
< NotificationSettings
enabled = { notificationsEnabled }
onEnabledChange = { setNotificationsEnabled }
mode = { notificationsMode }
onModeChange = { setNotificationsMode }
permissionState = { notificationsPermission }
2025-10-25 18:25:26 -07:00
/ >
2026-02-22 22:36:12 -08:00
< Button variant = "outline" asChild className = "w-full min-h-[48px]" >
< Link to = { ` /leagues/ ${ season . leagueId } ` } > Exit Draft Room < / Link >
< / Button >
2025-10-25 18:25:26 -07:00
< / div >
2026-02-22 22:36:12 -08:00
) }
< / div >
) : (
< div className = "flex h-full overflow-hidden" >
{ userTeam && queueSectionProps && (
< DraftSidebar
collapsed = { sidebarCollapsed }
onCollapsedChange = { setSidebarCollapsed }
queueSection = { < QueueSection { ...queueSectionProps } / > }
recentPicksSection = { < SidebarRecentPicks picks = { picks } / > }
settingsSection = {
< NotificationSettings
enabled = { notificationsEnabled }
onEnabledChange = { setNotificationsEnabled }
mode = { notificationsMode }
onModeChange = { setNotificationsMode }
permissionState = { notificationsPermission }
/ >
}
2025-10-25 03:23:41 -07:00
/ >
2026-02-22 22:36:12 -08:00
) }
< div className = "flex-1 overflow-hidden" >
< Tabs
value = { activeTab }
onValueChange = { ( value ) = >
setActiveTab ( value as "participants" | "board" | "teams" )
}
className = "h-full flex flex-col"
>
< div className = { ` mt-4 transition-all duration-300 ${
isMyTurn && season . status === "draft" && ! isDraftComplete
? "bg-electric/25 border-y-2 border-electric shadow-[0_0_24px_0_rgb(0_200_255_/_0.18)]"
: ""
} ` }>
< div className = "flex items-center gap-3 px-4 py-2" >
< TabsList >
< TabsTrigger value = "participants" > Available Participants < / TabsTrigger >
< TabsTrigger value = "board" > Draft Board < / TabsTrigger >
< TabsTrigger value = "teams" > Teams Drafted < / TabsTrigger >
< / TabsList >
{ season . status === "draft" && ! isDraftComplete && currentDraftSlot && (
< div className = { ` ml-auto flex items-center gap-2.5 rounded-lg px-3 py-1.5 flex-shrink-0 transition-all ${
isMyTurn
? "bg-electric/30 border-2 border-electric shadow-md shadow-electric/30"
: "border border-border/50"
} ` }>
< div className = "leading-tight" >
< div className = { ` text-xs font-bold uppercase tracking-wider ${ isMyTurn ? "text-electric" : "text-muted-foreground" } ` } >
{ isMyTurn ? "Your Turn!" : "On the Clock" }
< / div >
< div className = { ` text-sm font-bold ${ isMyTurn ? "text-foreground" : "text-muted-foreground" } ` } >
{ currentDraftSlot . team . name }
< / div >
< / div >
{ isPaused ? (
< span className = "text-sm font-bold text-amber-accent" > Paused < / span >
) : (
< span className = { ` text-2xl font-mono font-bold tabular-nums ${ currentClockColor } ` } >
{ formatClockTime ( currentClockTime ) }
< / span >
) }
< / div >
) }
< / div >
< / div >
< TabsContent value = "participants" className = "flex-1 overflow-hidden m-0" >
< div className = "h-full" >
< AvailableParticipantsSection { ...availableParticipantsSectionProps } / >
< / div >
< / TabsContent >
< TabsContent value = "board" className = "flex-1 overflow-hidden m-0" >
< DraftGridSection { ...draftGridSectionProps } / >
< / TabsContent >
< TabsContent value = "teams" className = "flex-1 overflow-hidden m-0" >
< div className = "h-full" >
< TeamsDraftedGrid { ...teamsDraftedGridProps } / >
< / div >
< / TabsContent >
< / Tabs >
< / div >
< / div >
) }
2025-10-18 14:55:26 -07:00
< / div >
2025-10-17 12:30:58 -07:00
2026-02-22 22:36:12 -08:00
{ /* Mobile Bottom Nav */ }
< nav className = "md:hidden flex-shrink-0 border-t bg-card grid grid-cols-4" >
{ MOBILE_TABS . map ( ( tab ) = > {
const showTurnIndicator =
tab . id === "lobby" &&
isMyTurn &&
season . status === "draft" &&
! isDraftComplete ;
return (
< button
key = { tab . id }
onClick = { ( ) = > setMobileTab ( tab . id ) }
className = { ` relative flex flex-col items-center justify-center py-2 gap-0.5 min-h-[56px] transition-colors ${
mobileTab === tab . id ? "text-electric" : "text-muted-foreground"
} ` }
>
< div className = "relative" >
< tab.Icon className = "h-5 w-5" / >
{ showTurnIndicator && (
< span className = "absolute -top-1 -right-1 w-2 h-2 rounded-full bg-electric" / >
) }
< / div >
< span className = "text-[10px] font-medium" > { tab . label } < / span >
< / button >
) ;
} ) }
< / nav >
2025-10-18 14:55:26 -07:00
{ /* Force Manual Pick Dialog */ }
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
< Dialog
open = { forcePickDialogOpen && ! ! selectedPickSlot }
onOpenChange = { ( open ) = > {
setForcePickDialogOpen ( open ) ;
if ( ! open ) setSelectedPickSlot ( null ) ;
} }
>
< DialogContent className = "max-w-2xl max-h-[80vh] flex flex-col gap-0 p-0" >
< DialogHeader className = "p-6 pb-4" >
< DialogTitle > Force Manual Pick < / DialogTitle >
< DialogDescription >
Select a participant to draft for Pick # { selectedPickSlot ? . pickNumber }
< / DialogDescription >
< / DialogHeader >
< div className = "px-6 pb-4" >
{ /* Search and Filter */ }
< div className = "flex gap-2 mb-4" >
< input
type = "text"
placeholder = "Search participants..."
2026-02-22 18:00:21 -08:00
className = "flex-1 px-3 py-2 border rounded-md bg-background text-foreground"
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
value = { dialogSearchQuery }
onChange = { ( e ) = > setDialogSearchQuery ( e . target . value ) }
/ >
< select
2026-02-22 18:00:21 -08:00
className = "px-3 py-2 border rounded-md bg-background text-foreground"
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
value = { dialogSportFilter }
onChange = { ( e ) = > setDialogSportFilter ( e . target . value ) }
>
< option value = "all" > All Sports < / option >
{ uniqueSports . map ( ( sport ) = > (
< option key = { sport } value = { sport } >
{ sport }
< / option >
) ) }
< / select >
< / div >
2025-10-18 14:55:26 -07:00
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
{ /* Participant List */ }
< div className = "max-h-96 overflow-y-auto border rounded-lg" >
< table className = "w-full text-sm" >
< thead className = "bg-muted sticky top-0" >
< tr >
< th className = "text-left p-3 font-semibold" > Participant < / th >
< th className = "text-left p-3 font-semibold" > Sport < / th >
< th className = "text-right p-3 font-semibold" > Action < / th >
< / tr >
< / thead >
< tbody >
{ dialogFilteredParticipants . map ( ( participant : any ) = > {
const isEligible = forcePickEligibility
? forcePickEligibility . eligibleSportIds . has ( participant . sport . id )
: true ;
const ineligibleReason = forcePickEligibility ? . ineligibleReasons [ participant . sport . id ] ;
return (
< tr
key = { participant . id }
className = { ` border-t ${
isEligible ? "hover:bg-muted/50" : "bg-destructive/10 opacity-60"
} ` }
title = { ! isEligible ? ineligibleReason : undefined }
>
< td className = "p-3" >
< div className = "flex items-center gap-2" >
< span className = { ` font-medium ${ ! isEligible ? "text-muted-foreground" : "" } ` } >
{ participant . name }
< / span >
{ ! isEligible && (
< Badge
variant = "destructive"
className = "text-xs"
title = { ineligibleReason }
>
Ineligible
< / Badge >
) }
< / div >
< / td >
< td className = "p-3 text-muted-foreground" >
{ participant . sport . name }
< / td >
< td className = "p-3 text-right" >
< Button
size = "sm"
onClick = { ( ) = > handleForceManualPick ( participant . id ) }
disabled = { ! isEligible }
2025-10-24 21:12:07 -07:00
title = { ! isEligible ? ineligibleReason : undefined }
>
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
Draft
< / Button >
< / td >
< / tr >
) ;
} ) }
< / tbody >
< / table >
2025-10-17 12:30:58 -07:00
< / div >
Improve draft room UX with better error handling and UI refinements (#17)
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:50:27 -08:00
< / div >
< DialogFooter className = "px-6 pb-6" >
< Button variant = "outline" onClick = { ( ) = > setForcePickDialogOpen ( false ) } >
Cancel
< / Button >
< / DialogFooter >
< / DialogContent >
< / Dialog >
2025-10-26 21:01:12 -07:00
2026-02-20 22:47:29 -08:00
{ /* Replace Pick Dialog */ }
< Dialog
open = { replacePickDialogOpen && ! ! replacePickSlot }
onOpenChange = { ( open ) = > {
setReplacePickDialogOpen ( open ) ;
if ( ! open ) setReplacePickSlot ( null ) ;
} }
>
< DialogContent className = "max-w-2xl max-h-[80vh] flex flex-col gap-0 p-0" >
< DialogHeader className = "p-6 pb-4" >
< DialogTitle > Replace Pick < / DialogTitle >
< DialogDescription >
Select a participant to replace the current pick at slot # { replacePickSlot ? . pickNumber }
< / DialogDescription >
< / DialogHeader >
< div className = "px-6 pb-4" >
< div className = "flex gap-2 mb-4" >
< input
type = "text"
placeholder = "Search participants..."
2026-02-22 18:00:21 -08:00
className = "flex-1 px-3 py-2 border rounded-md bg-background text-foreground"
2026-02-20 22:47:29 -08:00
value = { dialogSearchQuery }
onChange = { ( e ) = > setDialogSearchQuery ( e . target . value ) }
/ >
< select
2026-02-22 18:00:21 -08:00
className = "px-3 py-2 border rounded-md bg-background text-foreground"
2026-02-20 22:47:29 -08:00
value = { dialogSportFilter }
onChange = { ( e ) = > setDialogSportFilter ( e . target . value ) }
>
< option value = "all" > All Sports < / option >
{ uniqueSports . map ( ( sport ) = > (
< option key = { sport } value = { sport } >
{ sport }
< / option >
) ) }
< / select >
< / div >
< div className = "max-h-96 overflow-y-auto border rounded-lg" >
< table className = "w-full text-sm" >
< thead className = "bg-muted sticky top-0" >
< tr >
< th className = "text-left p-3 font-semibold" > Participant < / th >
< th className = "text-left p-3 font-semibold" > Sport < / th >
< th className = "text-right p-3 font-semibold" > Action < / th >
< / tr >
< / thead >
< tbody >
{ replaceDialogFilteredParticipants . map ( ( participant : any ) = > {
const isCurrentPick = participant . id === replacePickSlot ? . oldParticipantId ;
const isEligible = replacePickEligibility
? replacePickEligibility . eligibleSportIds . has ( participant . sport . id )
: true ;
const ineligibleReason = replacePickEligibility ? . ineligibleReasons [ participant . sport . id ] ;
return (
< tr
key = { participant . id }
className = { ` border-t ${
isEligible ? "hover:bg-muted/50" : "bg-destructive/10 opacity-60"
} ` }
title = { ! isEligible ? ineligibleReason : undefined }
>
< td className = "p-3" >
< div className = "flex items-center gap-2" >
< span className = { ` font-medium ${ ! isEligible ? "text-muted-foreground" : "" } ` } >
{ participant . name }
< / span >
{ isCurrentPick && (
< Badge variant = "outline" className = "text-xs" >
Current
< / Badge >
) }
{ ! isEligible && (
< Badge
variant = "destructive"
className = "text-xs"
title = { ineligibleReason }
>
Ineligible
< / Badge >
) }
< / div >
< / td >
< td className = "p-3 text-muted-foreground" >
{ participant . sport . name }
< / td >
< td className = "p-3 text-right" >
< Button
size = "sm"
onClick = { ( ) = > handleReplacePick ( participant . id ) }
disabled = { ! isEligible }
title = { ! isEligible ? ineligibleReason : undefined }
>
{ isCurrentPick ? "Keep" : "Draft" }
< / Button >
< / td >
< / tr >
) ;
} ) }
< / tbody >
< / table >
< / div >
< / div >
< DialogFooter className = "px-6 pb-6" >
< Button variant = "outline" onClick = { ( ) = > setReplacePickDialogOpen ( false ) } >
Cancel
< / Button >
< / DialogFooter >
< / DialogContent >
< / Dialog >
{ /* Rollback Confirmation Dialog */ }
< Dialog
open = { rollbackConfirmOpen }
onOpenChange = { ( open ) = > {
setRollbackConfirmOpen ( open ) ;
if ( ! open ) setRollbackPickNumber ( null ) ;
} }
>
< DialogContent >
< DialogHeader >
< DialogTitle > Roll Back Draft < / DialogTitle >
< DialogDescription >
{ rollbackPickNumber !== null && ( ( ) = > {
const count = picks . filter ( ( p : any ) = > p . pickNumber >= rollbackPickNumber ) . length ;
return ` This will erase ${ count } pick ${ count === 1 ? "" : "s" } (pick # ${ rollbackPickNumber } through the most recent) and return the draft to pick # ${ rollbackPickNumber } . This cannot be undone. ` ;
} ) ( ) }
< / DialogDescription >
< / DialogHeader >
< DialogFooter >
< Button variant = "outline" onClick = { ( ) = > setRollbackConfirmOpen ( false ) } >
Cancel
< / Button >
< Button variant = "destructive" onClick = { handleConfirmRollback } disabled = { isRollingBack } >
{ isRollingBack ? "Rolling Back..." : "Roll Back" }
< / Button >
< / DialogFooter >
< / DialogContent >
< / Dialog >
2026-02-22 16:16:51 -08:00
{ /* Time Bank Adjustment Dialog */ }
< Dialog
open = { timeBankDialogOpen }
onOpenChange = { ( open ) = > {
setTimeBankDialogOpen ( open ) ;
if ( ! open ) setTimeBankTeamId ( null ) ;
} }
>
< DialogContent >
< DialogHeader >
< DialogTitle > Adjust Time Bank < / DialogTitle >
< DialogDescription >
{ timeBankTeamId
? ` Adjusting time for ${ draftSlots . find ( ( s ) = > s . team . id === timeBankTeamId ) ? . team . name } `
: "Adjust a team's time bank" }
< / DialogDescription >
< / DialogHeader >
< div className = "flex flex-col gap-4 py-2" >
< div className = "flex gap-2" >
< button
type = "button"
onClick = { ( ) = > setTimeBankDirection ( "add" ) }
className = { ` flex-1 py-2 rounded-md border text-sm font-medium transition-colors ${
timeBankDirection === "add"
? "bg-emerald-500/20 border-emerald-500 text-emerald-400"
: "border-border text-muted-foreground hover:bg-muted"
} ` }
>
Add Time
< / button >
< button
type = "button"
onClick = { ( ) = > setTimeBankDirection ( "remove" ) }
className = { ` flex-1 py-2 rounded-md border text-sm font-medium transition-colors ${
timeBankDirection === "remove"
? "bg-destructive/20 border-destructive text-destructive"
: "border-border text-muted-foreground hover:bg-muted"
} ` }
>
Remove Time
< / button >
< / div >
< div className = "flex gap-2" >
< input
type = "number"
min = "0.001"
step = "any"
value = { timeBankAmount }
onChange = { ( e ) = > setTimeBankAmount ( e . target . value ) }
2026-02-22 18:00:21 -08:00
className = "flex-1 px-3 py-2 border rounded-md bg-background text-foreground text-sm"
2026-02-22 16:16:51 -08:00
placeholder = "Amount"
/ >
< select
value = { timeBankUnit }
onChange = { ( e ) = >
setTimeBankUnit ( e . target . value as "seconds" | "minutes" | "hours" )
}
2026-02-22 18:00:21 -08:00
className = "px-3 py-2 border rounded-md bg-background text-foreground text-sm"
2026-02-22 16:16:51 -08:00
>
< option value = "seconds" > Seconds < / option >
< option value = "minutes" > Minutes < / option >
< option value = "hours" > Hours < / option >
< / select >
< / div >
< / div >
< DialogFooter >
< Button variant = "outline" onClick = { ( ) = > setTimeBankDialogOpen ( false ) } >
Cancel
< / Button >
< Button
onClick = { handleConfirmAdjustTimeBank }
disabled = { isAdjustingTimeBank }
variant = { timeBankDirection === "remove" ? "destructive" : "default" }
>
{ isAdjustingTimeBank
? "Adjusting..."
: timeBankDirection === "add"
? "Add Time"
: "Remove Time" }
< / Button >
< / DialogFooter >
< / DialogContent >
< / Dialog >
2025-10-26 21:01:12 -07:00
{ /* Connection Overlay - blocks interaction until socket connects */ }
< ConnectionOverlay
isConnected = { isConnected }
isReconnecting = { isReconnecting }
connectionError = { connectionError }
/ >
2025-10-17 12:30:58 -07:00
< / div >
) ;
}