feat: add draft pause/resume functionality with UI controls and socket events

This commit is contained in:
Chris Parsons 2025-10-18 23:19:41 -07:00
parent a7f5df923f
commit 06db7fde7a
4 changed files with 211 additions and 0 deletions

View file

@ -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"),

View file

@ -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 });
}

View file

@ -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 });
}

View file

@ -183,6 +183,7 @@ export default function DraftRoom() {
const [sportFilter, setSportFilter] = useState<string>("all");
const [queue, setQueue] = useState(userQueue);
const [timeRemaining, setTimeRemaining] = useState<number | null>(null);
const [isPaused, setIsPaused] = useState(season.draftPaused || false);
const [teamTimers, setTeamTimers] = useState<Record<string, number>>(() => {
// Initialize with timers from loader data
const timersMap: Record<string, number> = {};
@ -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" && (
<Button onClick={handleStartDraft}>Start Draft</Button>
)}
{isCommissioner && season.status === "draft" && (
<>
{isPaused ? (
<Button onClick={handleResumeDraft} variant="default">
Resume Draft
</Button>
) : (
<Button onClick={handlePauseDraft} variant="outline">
Pause Draft
</Button>
)}
</>
)}
<div className="flex items-center gap-2">
<div