import { useEffect, type MutableRefObject, type RefObject } from "react"; import { toast } from "sonner"; import { getTeamForPick } from "~/lib/draft-order"; import { getAutodraftLabel } from "~/components/AutodraftSettings"; import type { getDraftPicksForSeason } from "~/models/draft-pick"; import type { findDraftSlotsBySeasonId } from "~/models/draft-slot"; import type { QueueItem, AutodraftStatusEntry } from "~/hooks/useDraftRoomState"; type DraftPicks = Awaited>; type DraftSlots = Awaited>; type DraftPick = DraftPicks[number]; interface UseDraftSocketEventsParams { on: (event: string, handler: (data: unknown) => void) => void; off: (event: string, handler: (data: unknown) => void) => void; socketVersion: number; // Loader-derived, stable for lifetime of page load userTeam: { id: string } | undefined; draftSlots: DraftSlots; leagueName: string; // Refs from useDraftAuthRecovery isRevalidatingRef: RefObject; pendingPicksDuringRevalidationRef: MutableRefObject; pendingQueueMutationsRef: MutableRefObject; userAutodraftRef: MutableRefObject<{ isEnabled: boolean; mode: "next_pick" | "while_on"; queueOnly: boolean }>; // Notification refs from the component sendNotificationRef: MutableRefObject<(title: string, body: string) => void>; notificationsModeRef: MutableRefObject; // State setters setPicks: (fn: (prev: DraftPicks) => DraftPicks) => void; setCurrentPick: (pick: number) => void; setIsPaused: (paused: boolean) => void; setIsDraftComplete: (complete: boolean) => void; setAutodraftStatus: (fn: (prev: Record) => Record) => void; setUserAutodraft: (value: { isEnabled: boolean; mode: "next_pick" | "while_on"; queueOnly: boolean }) => void; setConnectedTeams: (fn: (prev: Set) => Set) => void; setQueue: (fn: (prev: QueueItem[]) => QueueItem[]) => void; setTeamTimers: (fn: (prev: Record) => Record) => void; setIsOvernightPause: (value: boolean) => void; setOvernightResumesAt: (value: Date | null) => void; setWatchedParticipantIds: (value: Set) => void; setRoomClosed: (value: boolean) => void; setIsSyncing: (value: boolean) => void; setBracktVorps: (fn: (prev: Map) => Map) => void; setAnimatingOutParticipantId: (id: string) => void; } export function useDraftSocketEvents({ on, off, socketVersion, userTeam, draftSlots, leagueName, isRevalidatingRef, pendingPicksDuringRevalidationRef, pendingQueueMutationsRef, userAutodraftRef, sendNotificationRef, notificationsModeRef, setPicks, setCurrentPick, setIsPaused, setIsDraftComplete, setAutodraftStatus, setUserAutodraft, setConnectedTeams, setQueue, setTeamTimers, setIsOvernightPause, setOvernightResumesAt, setWatchedParticipantIds, setRoomClosed, setIsSyncing, setBracktVorps, setAnimatingOutParticipantId, }: UseDraftSocketEventsParams) { useEffect(() => { type PickMadePayload = { pick: DraftPick & { team?: { id?: string; name?: string }; participant?: { name?: string } }; nextPickNumber: number; isDraftComplete?: boolean; }; const handlePickMade = (data: PickMadePayload) => { if (isRevalidatingRef.current) { pendingPicksDuringRevalidationRef.current.push(data.pick); } else { setPicks((prev) => [...prev, data.pick]); setCurrentPick(data.nextPickNumber); if (data.pick?.participant?.id) { setAnimatingOutParticipantId(data.pick.participant.id); } } if (data.isDraftComplete) return; const notifTitle = `${leagueName} 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 ) { const pickerName = data.pick?.team?.name || "A team"; const participantName = data.pick?.participant?.name || "a participant"; sendNotificationRef.current(notifTitle, `${pickerName} picked ${participantName}`); } }; const handleTimerUpdate = (data: { teamId: string; timeRemaining: number; currentPickNumber: number | null; overnightPauseActive?: boolean; resumesAtUTC?: number; }) => { setTeamTimers((prev) => { if (prev[data.teamId] === data.timeRemaining) return prev; return { ...prev, [data.teamId]: data.timeRemaining }; }); if (data.currentPickNumber !== null) { setCurrentPick(data.currentPickNumber); } const pauseActive = data.overnightPauseActive ?? false; setIsOvernightPause(pauseActive); setOvernightResumesAt(pauseActive && data.resumesAtUTC ? new Date(data.resumesAtUTC) : null); }; const handleDraftPaused = () => setIsPaused(true); const handleDraftResumed = () => setIsPaused(false); const handleDraftCompleted = () => setIsDraftComplete(true); const handleAutodraftUpdated = (data: { teamId: string; isEnabled: boolean; mode: "next_pick" | "while_on"; queueOnly: boolean; source?: "commissioner" | "user"; }) => { setAutodraftStatus((prev) => ({ ...prev, [data.teamId]: { isEnabled: data.isEnabled, mode: data.mode, queueOnly: data.queueOnly }, })); if (userTeam && data.teamId === userTeam.id) { if (data.source === "commissioner") { if (!data.isEnabled) { toast.info("Commissioner turned off your autodraft"); } else { toast.info(`Commissioner changed your autodraft to "${getAutodraftLabel(data.isEnabled, data.mode, data.queueOnly)}"`); } } else { const prev = userAutodraftRef.current; if (!data.isEnabled && prev.isEnabled && prev.queueOnly && prev.mode === "while_on") { toast.info("Autodraft disabled — your queue is empty"); } } setUserAutodraft({ isEnabled: data.isEnabled, mode: data.mode, queueOnly: data.queueOnly }); } }; 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; }); }; const handleConnectedTeamsList = (data: { teamIds: string[] }) => { setConnectedTeams(() => new Set(data.teamIds)); }; const handleParticipantRemovedFromQueues = (data: { participantId: string }) => { setQueue((prev) => prev.filter((item) => item.participantId !== data.participantId)); }; const handleQueueEligibilityPruned = (data: { teamId: string; removedParticipantIds: string[] }) => { if (data.teamId !== userTeam?.id) return; const removed = new Set(data.removedParticipantIds); setQueue((prev) => prev.filter((item) => !removed.has(item.participantId))); }; const handleQueueUpdated = (data: { queue: QueueItem[] }) => { if (pendingQueueMutationsRef.current > 0) return; setQueue(() => data.queue); }; const handlePickReplaced = (data: { pickNumber: number; pick: DraftPick }) => { setPicks((prev) => prev.map((p) => (p.pickNumber === data.pickNumber ? data.pick : p))); }; const handleDraftRolledBack = (data: { pickNumber: number }) => { setPicks((prev) => prev.filter((p) => p.pickNumber < data.pickNumber)); setCurrentPick(data.pickNumber); setIsDraftComplete(false); setIsPaused(false); }; const handleDraftStateSync = (data: { picks: DraftPicks; currentPickNumber: number; isPaused: boolean; status: string; timers?: Array<{ teamId: string; timeRemaining: number }>; queue?: QueueItem[]; watchlistParticipantIds?: string[]; }) => { if (!isRevalidatingRef.current) { setPicks(() => data.picks); setCurrentPick(data.currentPickNumber); setIsPaused(data.isPaused); setIsDraftComplete(data.status === "active" || data.status === "completed"); if (data.timers) { setTeamTimers((prev) => { const updated = { ...prev }; data.timers?.forEach((t) => { updated[t.teamId] = t.timeRemaining; }); return updated; }); } if (data.queue) { const q = data.queue; setQueue(() => q); } if (data.watchlistParticipantIds) { setWatchedParticipantIds(new Set(data.watchlistParticipantIds)); } } setIsSyncing(false); }; const handleWatchlistUpdated = (data: { participantIds: string[] }) => { setWatchedParticipantIds(new Set(data.participantIds)); }; const handleDraftRoomClosed = () => { setRoomClosed(true); }; const handleBracktEvsUpdated = (data: { updates: Array<{ participantId: string; vorpValue: number }>; }) => { setBracktVorps((prev) => { const next = new Map(prev); for (const { participantId, vorpValue } of data.updates) { next.set(participantId, vorpValue); } return next; }); }; on("pick-made", handlePickMade as (data: unknown) => void); on("timer-update", handleTimerUpdate as (data: unknown) => void); on("draft-paused", handleDraftPaused as (data: unknown) => void); on("draft-resumed", handleDraftResumed as (data: unknown) => void); on("draft-completed", handleDraftCompleted as (data: unknown) => void); on("autodraft-updated", handleAutodraftUpdated as (data: unknown) => void); on("team-connected", handleTeamConnected as (data: unknown) => void); on("team-disconnected", handleTeamDisconnected as (data: unknown) => void); on("connected-teams-list", handleConnectedTeamsList as (data: unknown) => void); on("participant-removed-from-queues", handleParticipantRemovedFromQueues as (data: unknown) => void); on("queue-eligibility-pruned", handleQueueEligibilityPruned as (data: unknown) => void); on("queue-updated", handleQueueUpdated as (data: unknown) => void); on("pick-replaced", handlePickReplaced as (data: unknown) => void); on("draft-rolled-back", handleDraftRolledBack as (data: unknown) => void); on("draft-state-sync", handleDraftStateSync as (data: unknown) => void); on("watchlist-updated", handleWatchlistUpdated as (data: unknown) => void); on("draft-room-closed", handleDraftRoomClosed as (data: unknown) => void); on("brackt-evs-updated", handleBracktEvsUpdated as (data: unknown) => void); return () => { off("pick-made", handlePickMade as (data: unknown) => void); off("timer-update", handleTimerUpdate as (data: unknown) => void); off("draft-paused", handleDraftPaused as (data: unknown) => void); off("draft-resumed", handleDraftResumed as (data: unknown) => void); off("draft-completed", handleDraftCompleted as (data: unknown) => void); off("autodraft-updated", handleAutodraftUpdated as (data: unknown) => void); off("team-connected", handleTeamConnected as (data: unknown) => void); off("team-disconnected", handleTeamDisconnected as (data: unknown) => void); off("connected-teams-list", handleConnectedTeamsList as (data: unknown) => void); off("participant-removed-from-queues", handleParticipantRemovedFromQueues as (data: unknown) => void); off("queue-eligibility-pruned", handleQueueEligibilityPruned as (data: unknown) => void); off("queue-updated", handleQueueUpdated as (data: unknown) => void); off("pick-replaced", handlePickReplaced as (data: unknown) => void); off("draft-rolled-back", handleDraftRolledBack as (data: unknown) => void); off("draft-state-sync", handleDraftStateSync as (data: unknown) => void); off("watchlist-updated", handleWatchlistUpdated as (data: unknown) => void); off("draft-room-closed", handleDraftRoomClosed as (data: unknown) => void); off("brackt-evs-updated", handleBracktEvsUpdated as (data: unknown) => void); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [on, off, socketVersion]); }