From 06db7fde7a48da7b5630870405da3105c7659ebe Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Sat, 18 Oct 2025 23:19:41 -0700 Subject: [PATCH] feat: add draft pause/resume functionality with UI controls and socket events --- app/routes.ts | 2 + app/routes/api/draft.pause.ts | 77 +++++++++++++++++++ app/routes/api/draft.resume.ts | 77 +++++++++++++++++++ .../leagues/$leagueId.draft.$seasonId.tsx | 55 +++++++++++++ 4 files changed, 211 insertions(+) create mode 100644 app/routes/api/draft.pause.ts create mode 100644 app/routes/api/draft.resume.ts diff --git a/app/routes.ts b/app/routes.ts index 7eac0f5..5fb8e41 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -18,6 +18,8 @@ export default [ route("api/queue/reorder", "routes/api/queue.reorder.ts"), route("api/draft/make-pick", "routes/api/draft.make-pick.ts"), route("api/draft/start", "routes/api/draft.start.ts"), + route("api/draft/pause", "routes/api/draft.pause.ts"), + route("api/draft/resume", "routes/api/draft.resume.ts"), route("api/draft/force-autopick", "routes/api/draft.force-autopick.ts"), route("api/draft/force-manual-pick", "routes/api/draft.force-manual-pick.ts"), route("user-profile", "routes/user-profile.tsx"), diff --git a/app/routes/api/draft.pause.ts b/app/routes/api/draft.pause.ts new file mode 100644 index 0000000..63858a8 --- /dev/null +++ b/app/routes/api/draft.pause.ts @@ -0,0 +1,77 @@ +import { getAuth } from "@clerk/react-router/ssr.server"; +import { eq, and } from "drizzle-orm"; +import { database } from "~/database/context"; +import * as schema from "~/database/schema"; + +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; + + if (!seasonId) { + return Response.json({ error: "Season ID is required" }, { status: 400 }); + } + + const db = database(); + + // Get season + const season = await db.query.seasons.findFirst({ + where: eq(schema.seasons.id, seasonId), + with: { + league: true, + }, + }); + + if (!season) { + return Response.json({ error: "Season not found" }, { status: 404 }); + } + + // Verify user is commissioner + 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 pause the draft" }, + { status: 403 } + ); + } + + // Check if draft is active + if (season.status !== "draft") { + return Response.json( + { error: "Draft is not active" }, + { status: 400 } + ); + } + + // Pause the draft + await db + .update(schema.seasons) + .set({ draftPaused: true }) + .where(eq(schema.seasons.id, seasonId)); + + // Emit socket event + try { + const io = (global as any).__socketIO; + io.to(`draft-${seasonId}`).emit("draft-paused", { + seasonId, + paused: true, + }); + } catch (error) { + console.error("Socket.IO error:", error); + } + + return Response.json({ success: true, paused: true }); +} diff --git a/app/routes/api/draft.resume.ts b/app/routes/api/draft.resume.ts new file mode 100644 index 0000000..bb09462 --- /dev/null +++ b/app/routes/api/draft.resume.ts @@ -0,0 +1,77 @@ +import { getAuth } from "@clerk/react-router/ssr.server"; +import { eq, and } from "drizzle-orm"; +import { database } from "~/database/context"; +import * as schema from "~/database/schema"; + +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; + + if (!seasonId) { + return Response.json({ error: "Season ID is required" }, { status: 400 }); + } + + const db = database(); + + // Get season + const season = await db.query.seasons.findFirst({ + where: eq(schema.seasons.id, seasonId), + with: { + league: true, + }, + }); + + if (!season) { + return Response.json({ error: "Season not found" }, { status: 404 }); + } + + // Verify user is commissioner + 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 resume the draft" }, + { status: 403 } + ); + } + + // Check if draft is active + if (season.status !== "draft") { + return Response.json( + { error: "Draft is not active" }, + { status: 400 } + ); + } + + // Resume the draft + await db + .update(schema.seasons) + .set({ draftPaused: false }) + .where(eq(schema.seasons.id, seasonId)); + + // Emit socket event + try { + const io = (global as any).__socketIO; + io.to(`draft-${seasonId}`).emit("draft-resumed", { + seasonId, + paused: false, + }); + } catch (error) { + console.error("Socket.IO error:", error); + } + + return Response.json({ success: true, paused: false }); +} diff --git a/app/routes/leagues/$leagueId.draft.$seasonId.tsx b/app/routes/leagues/$leagueId.draft.$seasonId.tsx index bd21980..47d5a52 100644 --- a/app/routes/leagues/$leagueId.draft.$seasonId.tsx +++ b/app/routes/leagues/$leagueId.draft.$seasonId.tsx @@ -183,6 +183,7 @@ export default function DraftRoom() { const [sportFilter, setSportFilter] = useState("all"); const [queue, setQueue] = useState(userQueue); const [timeRemaining, setTimeRemaining] = useState(null); + const [isPaused, setIsPaused] = useState(season.draftPaused || false); const [teamTimers, setTeamTimers] = useState>(() => { // Initialize with timers from loader data const timersMap: Record = {}; @@ -218,12 +219,24 @@ export default function DraftRoom() { } }; + const handleDraftPaused = () => { + setIsPaused(true); + }; + + const handleDraftResumed = () => { + setIsPaused(false); + }; + on("pick-made", handlePickMade); on("timer-update", handleTimerUpdate); + on("draft-paused", handleDraftPaused); + on("draft-resumed", handleDraftResumed); return () => { off("pick-made", handlePickMade); off("timer-update", handleTimerUpdate); + off("draft-paused", handleDraftPaused); + off("draft-resumed", handleDraftResumed); }; }, [on, off, currentPick]); @@ -295,6 +308,34 @@ export default function DraftRoom() { } }; + const handlePauseDraft = async () => { + const formData = new FormData(); + formData.append("seasonId", season.id); + + const response = await fetch("/api/draft/pause", { + method: "POST", + body: formData, + }); + + if (response.ok) { + setIsPaused(true); + } + }; + + const handleResumeDraft = async () => { + const formData = new FormData(); + formData.append("seasonId", season.id); + + const response = await fetch("/api/draft/resume", { + method: "POST", + body: formData, + }); + + if (response.ok) { + setIsPaused(false); + } + }; + const handleMakePick = async (participantId: string) => { const formData = new FormData(); formData.append("seasonId", season.id); @@ -494,6 +535,20 @@ export default function DraftRoom() { {isCommissioner && season.status === "pre_draft" && ( )} + + {isCommissioner && season.status === "draft" && ( + <> + {isPaused ? ( + + ) : ( + + )} + + )}