From 554a24eafbcc953ea5b3636ed225ac2b6e96577a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Feb 2026 18:46:23 +0000 Subject: [PATCH] Add commissioner time bank adjustment via right-click on draft board team headers Commissioners can right-click any team header on the Draft Board tab to open an "Adjust Time Bank..." dialog. The dialog supports adding or removing an arbitrary amount of time in seconds, minutes, or hours. The change is applied immediately to the database and broadcast to all clients via the existing timer-update Socket.IO event so every participant sees the updated clock in real time. - New API route: POST /api/draft/adjust-time-bank (commissioner-only) - DraftGridSection: wraps team headers in a ContextMenu for commissioners - Draft room: dialog state, handler, and updated DraftGridSection props https://claude.ai/code/session_013wxPKzLUCx3nC3LpxgjvQL --- app/components/draft/DraftGridSection.tsx | 31 +++- app/routes.ts | 1 + app/routes/api/draft.adjust-time-bank.ts | 85 +++++++++++ .../leagues/$leagueId.draft.$seasonId.tsx | 143 ++++++++++++++++++ 4 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 app/routes/api/draft.adjust-time-bank.ts diff --git a/app/components/draft/DraftGridSection.tsx b/app/components/draft/DraftGridSection.tsx index bfc69d3..13bb2fb 100644 --- a/app/components/draft/DraftGridSection.tsx +++ b/app/components/draft/DraftGridSection.tsx @@ -37,6 +37,7 @@ interface DraftGridSectionProps { autodraftStatus: Record; connectedTeams: Set; isCommissioner: boolean; + onAdjustTimeBankOpen?: (teamId: string) => void; onForceAutopick: (pickNumber: number, teamId: string) => void; onForceManualPickOpen: (pickNumber: number, teamId: string) => void; onReplacePick?: (pickNumber: number, teamId: string) => void; @@ -51,6 +52,7 @@ export function DraftGridSection({ autodraftStatus, connectedTeams, isCommissioner, + onAdjustTimeBankOpen, onForceAutopick, onForceManualPickOpen, onReplacePick, @@ -73,8 +75,8 @@ export function DraftGridSection({ const isAutodraft = autodraftStatus[slot.team.id] || false; const isConnected = connectedTeams.has(slot.team.id); - return ( -
+ const headerInner = ( + <>
)}
+ + ); + + if (isCommissioner && onAdjustTimeBankOpen) { + return ( + + +
+ {headerInner} +
+
+ + onAdjustTimeBankOpen(slot.team.id)} + > + Adjust Time Bank... + + +
+ ); + } + + return ( +
+ {headerInner}
); })} diff --git a/app/routes.ts b/app/routes.ts index 82cf73a..081252e 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -40,6 +40,7 @@ export default [ route("api/draft/force-manual-pick", "routes/api/draft.force-manual-pick.ts"), route("api/draft/replace-pick", "routes/api/draft.replace-pick.ts"), route("api/draft/rollback", "routes/api/draft.rollback.ts"), + route("api/draft/adjust-time-bank", "routes/api/draft.adjust-time-bank.ts"), route("api/autodraft/update", "routes/api/autodraft.update.ts"), route("user-profile", "routes/user-profile.tsx"), route("how-to-play", "routes/how-to-play.tsx"), diff --git a/app/routes/api/draft.adjust-time-bank.ts b/app/routes/api/draft.adjust-time-bank.ts new file mode 100644 index 0000000..3bf5591 --- /dev/null +++ b/app/routes/api/draft.adjust-time-bank.ts @@ -0,0 +1,85 @@ +import { getAuth } from "@clerk/react-router/server"; +import { eq, and } from "drizzle-orm"; +import { database } from "~/database/context"; +import * as schema from "~/database/schema"; +import { getSocketIO } from "../../../server/socket"; + +export async function action(args: any) { + const { request } = args; + const auth = await getAuth(args); + const userId = (auth as any).userId as string | null; + + if (!userId) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + const formData = await request.formData(); + const seasonId = formData.get("seasonId") as string; + const teamId = formData.get("teamId") as string; + const adjustmentRaw = formData.get("adjustment"); + const adjustment = adjustmentRaw !== null ? parseInt(adjustmentRaw as string, 10) : NaN; + + if (!seasonId || !teamId || isNaN(adjustment)) { + return Response.json({ error: "seasonId, teamId, and adjustment are required" }, { status: 400 }); + } + + const db = database(); + + const season = await db.query.seasons.findFirst({ + where: eq(schema.seasons.id, seasonId), + }); + + if (!season) { + return Response.json({ error: "Season not found" }, { status: 404 }); + } + + const isCommissioner = await db.query.commissioners.findFirst({ + where: and( + eq(schema.commissioners.leagueId, season.leagueId), + eq(schema.commissioners.userId, userId) + ), + }); + + if (!isCommissioner) { + return Response.json( + { error: "Only commissioners can adjust time banks" }, + { status: 403 } + ); + } + + const [currentTimer] = await db + .select() + .from(schema.draftTimers) + .where( + and( + eq(schema.draftTimers.seasonId, seasonId), + eq(schema.draftTimers.teamId, teamId) + ) + ); + + if (!currentTimer) { + return Response.json({ error: "Timer not found for this team" }, { status: 404 }); + } + + const newTime = Math.max(0, currentTimer.timeRemaining + adjustment); + + await db + .update(schema.draftTimers) + .set({ timeRemaining: newTime, updatedAt: new Date() }) + .where(eq(schema.draftTimers.id, currentTimer.id)); + + try { + getSocketIO() + .to(`draft-${seasonId}`) + .emit("timer-update", { + seasonId, + teamId, + timeRemaining: newTime, + currentPickNumber: season.currentPickNumber ?? 1, + }); + } catch (error) { + console.error("Socket.IO error:", error); + } + + return Response.json({ success: true, timeRemaining: newTime }); +} diff --git a/app/routes/leagues/$leagueId.draft.$seasonId.tsx b/app/routes/leagues/$leagueId.draft.$seasonId.tsx index 96a81fe..8968e9e 100644 --- a/app/routes/leagues/$leagueId.draft.$seasonId.tsx +++ b/app/routes/leagues/$leagueId.draft.$seasonId.tsx @@ -296,6 +296,14 @@ export default function DraftRoom() { const [rollbackPickNumber, setRollbackPickNumber] = useState(null); const [isRollingBack, setIsRollingBack] = useState(false); + // Time bank adjustment dialog state + const [timeBankDialogOpen, setTimeBankDialogOpen] = useState(false); + const [timeBankTeamId, setTimeBankTeamId] = useState(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); + // Shared transforms for eligibility calculations const transformedPicks = useMemo( () => @@ -810,6 +818,53 @@ export default function DraftRoom() { } }; + 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]); + const adjustment = timeBankDirection === "add" ? totalSeconds : -totalSeconds; + + setIsAdjustingTimeBank(true); + 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, + }); + + setIsAdjustingTimeBank(false); + + 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"); + } + }; + // Calculate current round const currentRound = draftSlots.length > 0 ? Math.ceil(currentPick / draftSlots.length) : 1; @@ -1159,6 +1214,7 @@ export default function DraftRoom() { autodraftStatus={autodraftStatus} connectedTeams={connectedTeams} isCommissioner={isCommissioner} + onAdjustTimeBankOpen={isCommissioner ? handleAdjustTimeBankOpen : undefined} onForceAutopick={handleForceAutopick} onForceManualPickOpen={(pickNumber, teamId) => { setSelectedPickSlot({ pickNumber, teamId }); @@ -1438,6 +1494,93 @@ export default function DraftRoom() { + {/* Time Bank Adjustment Dialog */} + { + setTimeBankDialogOpen(open); + if (!open) setTimeBankTeamId(null); + }} + > + + + Adjust Time Bank + + {timeBankTeamId + ? `Adjusting time for ${draftSlots.find((s) => s.team.id === timeBankTeamId)?.team.name}` + : "Adjust a team's time bank"} + + + +
+
+ + +
+ +
+ setTimeBankAmount(e.target.value)} + className="flex-1 px-3 py-2 border rounded-md bg-background text-sm" + placeholder="Amount" + /> + +
+
+ + + + + +
+
+ {/* Connection Overlay - blocks interaction until socket connects */}