brackt/app/hooks/useDraftRoomState.ts
Chris Parsons 3611f5620e
Show "Syncing Draft State..." overlay during draft reconnection (#361)
* Show "Syncing Draft State..." overlay during draft reconnection

When a user reconnects to a draft room (socket reconnect, tab visibility
return, etc.), there was a gap between when the socket connected and when
the full draft state sync arrived. The ConnectionOverlay vanished instantly
on reconnect, showing potentially stale data.

Add an isSyncing flag that keeps the overlay visible with "Syncing Draft
State..." messaging until the actual data arrives via socket or HTTP
revalidation. A 5-second safety timeout prevents the overlay from getting
stuck.

Fixes #78

* Add team names utility tests
2026-04-30 20:33:00 -07:00

183 lines
7.1 KiB
TypeScript

import { useState } from "react";
import { useMediaQuery } from "~/hooks/useMediaQuery";
import type * as schema from "~/database/schema";
import type { getDraftPicksForSeason } from "~/models/draft-pick";
import type { findSeasonWithTeamsAndLeague } from "~/models/season";
import type { findDraftSlotsBySeasonId } from "~/models/draft-slot";
import type { getSeasonAutodraftSettings } from "~/models/autodraft-settings";
import type { getSeasonTimers } from "~/models/draft-timer";
export type QueueItem = typeof schema.draftQueue.$inferSelect;
export type AutodraftStatusEntry = {
isEnabled: boolean;
mode: "next_pick" | "while_on";
queueOnly: boolean;
};
type DraftPicks = Awaited<ReturnType<typeof getDraftPicksForSeason>>;
type Season = NonNullable<Awaited<ReturnType<typeof findSeasonWithTeamsAndLeague>>>;
type DraftSlots = Awaited<ReturnType<typeof findDraftSlotsBySeasonId>>;
type AutodraftSettings = Awaited<ReturnType<typeof getSeasonAutodraftSettings>>;
type Timers = Awaited<ReturnType<typeof getSeasonTimers>>;
interface UseDraftRoomStateParams {
draftPicks: DraftPicks;
season: Season;
userTeam: { id: string } | undefined;
isCommissioner: boolean;
autodraftSettings: AutodraftSettings;
timers: Timers;
draftSlots: DraftSlots;
userQueue: QueueItem[];
initialWatchedParticipantIds: string[];
}
export function useDraftRoomState({
draftPicks,
season,
userTeam,
isCommissioner,
autodraftSettings,
timers,
draftSlots,
userQueue,
initialWatchedParticipantIds,
}: UseDraftRoomStateParams) {
const [picks, setPicks] = useState(draftPicks);
const [currentPick, setCurrentPick] = useState(season.currentPickNumber || 1);
const [searchQuery, setSearchQuery] = useState("");
const [hideDrafted, setHideDrafted] = useState(true);
const [hideIneligible, setHideIneligible] = useState(true);
const [hideCompletedSports, setHideCompletedSports] = useState(false);
const [sportFilters, setSportFilters] = useState<string[]>([]);
const [queue, setQueue] = useState<QueueItem[]>(userQueue);
const [isPaused, setIsPaused] = useState(season.draftPaused || false);
const [isDraftComplete, setIsDraftComplete] = useState(
season.status === "active" || season.status === "completed"
);
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
if (typeof window === "undefined") return false;
const stored = localStorage.getItem("draftSidebarCollapsed");
return stored ? JSON.parse(stored) : false;
});
const [activeTab, setActiveTab] = useState<"participants" | "board" | "rosters" | "summary">("participants");
const [mobileTab, setMobileTab] = useState<"available" | "queue" | "board" | "teams" | "controls">(
!userTeam && isCommissioner ? "board" : "available"
);
const [teamsSubTab, setTeamsSubTab] = useState<"rosters" | "summary">("rosters");
const [boardSubTab, setBoardSubTab] = useState<"board" | "pick-list">("board");
const isMobile = useMediaQuery("(max-width: 767px)");
const [autodraftStatus, setAutodraftStatus] = useState<Record<string, AutodraftStatusEntry>>(() => {
const status: Record<string, AutodraftStatusEntry> = {};
autodraftSettings.forEach((setting) => {
status[setting.teamId] = {
isEnabled: setting.isEnabled,
mode: setting.mode,
queueOnly: setting.queueOnly,
};
});
return status;
});
const [connectedTeams, setConnectedTeams] = useState<Set<string>>(new Set());
const [watchedParticipantIds, setWatchedParticipantIds] = useState<Set<string>>(
() => new Set(initialWatchedParticipantIds)
);
const [showOnlyWatched, setShowOnlyWatched] = useState(false);
const [teamTimers, setTeamTimers] = useState<Record<string, number>>(() => {
const timersMap: Record<string, number> = {};
const initialTime = season.draftInitialTime || 120;
if (timers.length > 0) {
timers.forEach((timer) => {
timersMap[timer.teamId] = timer.timeRemaining;
});
} else {
draftSlots.forEach((slot) => {
timersMap[slot.team.id] = initialTime;
});
}
return timersMap;
});
const [forcePickDialogOpen, setForcePickDialogOpen] = useState(false);
const [selectedPickSlot, setSelectedPickSlot] = useState<{
pickNumber: number;
teamId: string;
} | null>(null);
const [replacePickDialogOpen, setReplacePickDialogOpen] = useState(false);
const [replacePickSlot, setReplacePickSlot] = useState<{
pickNumber: number;
teamId: string;
oldParticipantId: string;
} | null>(null);
const [rollbackConfirmOpen, setRollbackConfirmOpen] = useState(false);
const [rollbackPickNumber, setRollbackPickNumber] = useState<number | null>(null);
const [isRollingBack, setIsRollingBack] = useState(false);
const [timeBankDialogOpen, setTimeBankDialogOpen] = useState(false);
const [timeBankTeamId, setTimeBankTeamId] = useState<string | null>(null);
const [commissionerAutodraftDialogOpen, setCommissionerAutodraftDialogOpen] = useState(false);
const [commissionerAutodraftTeamId, setCommissionerAutodraftTeamId] = 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);
const [isOvernightPause, setIsOvernightPause] = useState(false);
const [overnightResumesAt, setOvernightResumesAt] = useState<Date | null>(null);
const [roomClosed, setRoomClosed] = useState(false);
const [isSyncing, setIsSyncing] = useState(false);
return {
picks, setPicks,
currentPick, setCurrentPick,
searchQuery, setSearchQuery,
hideDrafted, setHideDrafted,
hideIneligible, setHideIneligible,
hideCompletedSports, setHideCompletedSports,
sportFilters, setSportFilters,
queue, setQueue,
isPaused, setIsPaused,
isDraftComplete, setIsDraftComplete,
sidebarCollapsed, setSidebarCollapsed,
activeTab, setActiveTab,
mobileTab, setMobileTab,
teamsSubTab, setTeamsSubTab,
boardSubTab, setBoardSubTab,
isMobile,
autodraftStatus, setAutodraftStatus,
connectedTeams, setConnectedTeams,
watchedParticipantIds, setWatchedParticipantIds,
showOnlyWatched, setShowOnlyWatched,
teamTimers, setTeamTimers,
forcePickDialogOpen, setForcePickDialogOpen,
selectedPickSlot, setSelectedPickSlot,
replacePickDialogOpen, setReplacePickDialogOpen,
replacePickSlot, setReplacePickSlot,
rollbackConfirmOpen, setRollbackConfirmOpen,
rollbackPickNumber, setRollbackPickNumber,
isRollingBack, setIsRollingBack,
timeBankDialogOpen, setTimeBankDialogOpen,
timeBankTeamId, setTimeBankTeamId,
commissionerAutodraftDialogOpen, setCommissionerAutodraftDialogOpen,
commissionerAutodraftTeamId, setCommissionerAutodraftTeamId,
timeBankAmount, setTimeBankAmount,
timeBankUnit, setTimeBankUnit,
timeBankDirection, setTimeBankDirection,
isAdjustingTimeBank, setIsAdjustingTimeBank,
isOvernightPause, setIsOvernightPause,
overnightResumesAt, setOvernightResumesAt,
roomClosed, setRoomClosed,
isSyncing, setIsSyncing,
};
}