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" ;
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" ;
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" ;
2025-10-17 12:30:58 -07:00
2025-10-17 12:45:30 -07:00
export async function loader ( args : any ) {
const { params } = args ;
const { seasonId , leagueId } = params ;
const auth = await getAuth ( args ) ;
const userId = ( auth as any ) . userId as string | null ;
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 } ) ;
}
2025-10-17 12:45:30 -07:00
if ( ! userId ) {
2025-10-18 14:55:26 -07:00
throw new Response ( "You must be logged in to view the draft room" , {
status : 401 ,
} ) ;
2025-10-17 12:45:30 -07:00
}
2025-10-17 12:30:58 -07:00
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 } ) ;
}
2025-10-17 12:45:30 -07:00
// Verify user access: must be team owner or commissioner
const userTeam = season . teams . find ( ( team : any ) = > team . ownerId === userId ) ;
const isCommissioner = await db . query . commissioners . findFirst ( {
where : and (
eq ( schema . commissioners . leagueId , leagueId ) ,
eq ( schema . commissioners . userId , userId )
) ,
} ) ;
if ( ! userTeam && ! isCommissioner ) {
2025-10-18 14:55:26 -07:00
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
const userQueue = userTeam ? await getTeamQueue ( userTeam . id ) : [ ] ;
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 ;
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 ) ,
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 ,
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 ,
} = useDraftNotifications ( season . id , currentUserId ) ;
// 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" ) ;
2025-10-17 17:42:40 -07:00
const [ queue , setQueue ] = useState ( 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" ) ;
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)
const alreadyInQueue = queue . some ( ( item : any ) = > item . participantId === participantId ) ;
if ( alreadyInQueue ) {
toast . error ( "This participant is already in your queue" ) ;
return ;
}
// Optimistic update - create a temporary queue item
const tempQueueItem = {
id : ` temp- ${ Date . now ( ) } ` , // Temporary ID
participantId ,
queuePosition : queue.length + 1 ,
} ;
// Immediately update the UI
setQueue ( ( prev : any ) = > [ . . . prev , tempQueueItem ] ) ;
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
setQueue ( ( prev : any ) = >
prev . map ( ( item : any ) = >
item . id === tempQueueItem . id ? data.queueItem : item
)
) ;
} else {
// Revert the optimistic update on error
setQueue ( ( prev : any ) = >
prev . filter ( ( item : any ) = > item . id !== tempQueueItem . id )
) ;
const error = await response . json ( ) ;
toast . error ( error . error || "Failed to add to queue" ) ;
}
} catch ( error ) {
// Revert the optimistic update on network error
setQueue ( ( prev : any ) = >
prev . filter ( ( item : any ) = > item . id !== tempQueueItem . id )
) ;
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 ;
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 {
const error = await response . json ( ) ;
toast . error ( error . error || "Failed to remove from queue" ) ;
}
} catch {
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 ;
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 ( ) ;
toast . error ( error . error || "Failed to reorder queue" ) ;
}
} catch {
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 ) ;
}
} ;
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
2025-10-17 12:30:58 -07:00
return (
2025-10-25 18:25:26 -07:00
< div className = "h-screen 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 >
< h1 className = "text-3xl font-bold" >
{ 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" >
2025-10-18 14:55:26 -07:00
{ /* Commissioner Controls */ }
{ isCommissioner && season . status === "pre_draft" && (
< Button onClick = { handleStartDraft } > Start Draft < / Button >
) }
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
{ isCommissioner && season . status === "draft" && ! isDraftComplete && (
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 >
) }
< / >
) }
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
} ` }
/ >
< span className = "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 >
< Button variant = "outline" asChild >
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 >
2025-10-25 03:23:41 -07:00
{ /* Main Content - Flex Row: Sidebar + Tabs */ }
< div className = "flex-1 flex overflow-hidden" >
{ /* Sidebar */ }
{ userTeam && (
< DraftSidebar
collapsed = { sidebarCollapsed }
onCollapsedChange = { setSidebarCollapsed }
queueSection = {
< QueueSection
queue = { queue }
availableParticipants = { availableParticipants }
eligibility = { eligibility }
seasonId = { season . id }
teamId = { userTeam . id }
isMyTurn = { isMyTurn }
userAutodraft = { userAutodraft }
onRemoveFromQueue = { handleRemoveFromQueue }
onAutodraftUpdate = { ( isEnabled , mode ) = > {
setUserAutodraft ( { isEnabled , mode } ) ;
} }
2025-10-25 21:02:16 -07:00
onReorder = { handleReorderQueue }
2025-10-25 03:23:41 -07:00
/ >
}
2025-10-25 18:25:26 -07:00
recentPicksSection = {
< SidebarRecentPicks picks = { picks } / >
2025-10-25 03:23:41 -07:00
}
2026-02-20 19:30:53 -08:00
settingsSection = {
< NotificationSettings
enabled = { notificationsEnabled }
onEnabledChange = { setNotificationsEnabled }
mode = { notificationsMode }
onModeChange = { setNotificationsMode }
permissionState = { notificationsPermission }
/ >
}
2025-10-25 03:23:41 -07:00
/ >
) }
{ /* Main Tabbed Content */ }
< div className = "flex-1 overflow-hidden" >
< Tabs
value = { activeTab }
onValueChange = { ( value ) = >
2025-10-25 18:25:26 -07:00
setActiveTab ( value as "participants" | "board" | "teams" )
2025-10-25 03:23:41 -07:00
}
className = "h-full flex flex-col"
>
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
< 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 >
2025-10-25 03:23:41 -07:00
2025-10-25 18:25:26 -07:00
< TabsContent value = "participants" className = "flex-1 overflow-hidden m-0" >
< div className = "h-full" >
< AvailableParticipantsSection
participants = { filteredParticipants }
searchQuery = { searchQuery }
sportFilter = { sportFilter }
hideDrafted = { hideDrafted }
2025-10-26 21:09:30 -07:00
hideIneligible = { hideIneligible }
2025-10-25 18:25:26 -07:00
uniqueSports = { uniqueSports }
draftedParticipantIds = { draftedParticipantIds }
queue = { queue }
eligibility = { eligibility }
canPick = { canPick }
hasTeam = { ! ! userTeam }
onSearchChange = { setSearchQuery }
onSportFilterChange = { setSportFilter }
onHideDraftedChange = { setHideDrafted }
2025-10-26 21:09:30 -07:00
onHideIneligibleChange = { setHideIneligible }
2025-10-25 18:25:26 -07:00
onMakePick = { handleMakePick }
onAddToQueue = { handleAddToQueue }
onRemoveFromQueue = { handleRemoveFromQueue }
/ >
< / div >
< / TabsContent >
2025-10-25 03:23:41 -07:00
< TabsContent value = "board" className = "flex-1 overflow-hidden m-0" >
< DraftGridSection
draftSlots = { draftSlots }
draftGrid = { draftGrid }
currentPick = { currentPick }
teamTimers = { teamTimers }
autodraftStatus = { autodraftStatus }
connectedTeams = { connectedTeams }
isCommissioner = { isCommissioner }
2026-02-22 16:16:51 -08:00
onAdjustTimeBankOpen = { isCommissioner ? handleAdjustTimeBankOpen : undefined }
2025-10-25 03:23:41 -07:00
onForceAutopick = { handleForceAutopick }
onForceManualPickOpen = { ( pickNumber , teamId ) = > {
setSelectedPickSlot ( { pickNumber , teamId } ) ;
2026-02-20 21:10:26 -08:00
setDialogSearchQuery ( "" ) ;
setDialogSportFilter ( "all" ) ;
2025-10-25 03:23:41 -07:00
setForcePickDialogOpen ( true ) ;
} }
2026-02-20 22:47:29 -08:00
onReplacePick = { isCommissioner ? handleReplacePickOpen : undefined }
onRollbackToPick = { isCommissioner && ! isDraftComplete ? handleRollbackToPick : undefined }
2025-10-25 03:23:41 -07:00
/ >
< / TabsContent >
< TabsContent value = "teams" className = "flex-1 overflow-hidden m-0" >
2025-10-25 18:25:26 -07:00
< div className = "h-full" >
2025-10-25 03:23:41 -07:00
< TeamsDraftedGrid
draftSlots = { draftSlots }
picks = { picks }
2026-02-20 21:26:27 -08:00
sports = { seasonSportsData }
2026-02-20 21:10:26 -08:00
season = { { numFlexPicks } }
2025-10-21 23:22:17 -07:00
/ >
2025-10-17 12:51:47 -07:00
< / div >
2025-10-25 03:23:41 -07:00
< / TabsContent >
< / Tabs >
2025-10-17 12:30:58 -07:00
< / div >
2025-10-18 14:55:26 -07:00
< / div >
2025-10-17 12:30:58 -07:00
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..."
className = "flex-1 px-3 py-2 border rounded-md bg-background"
value = { dialogSearchQuery }
onChange = { ( e ) = > setDialogSearchQuery ( e . target . value ) }
/ >
< select
className = "px-3 py-2 border rounded-md bg-background"
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..."
className = "flex-1 px-3 py-2 border rounded-md bg-background"
value = { dialogSearchQuery }
onChange = { ( e ) = > setDialogSearchQuery ( e . target . value ) }
/ >
< select
className = "px-3 py-2 border rounded-md bg-background"
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 ) }
className = "flex-1 px-3 py-2 border rounded-md bg-background text-sm"
placeholder = "Amount"
/ >
< select
value = { timeBankUnit }
onChange = { ( e ) = >
setTimeBankUnit ( e . target . value as "seconds" | "minutes" | "hours" )
}
className = "px-3 py-2 border rounded-md bg-background text-sm"
>
< 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 >
) ;
}