feat: implement real-time draft room with commissioner controls and socket events
This commit is contained in:
parent
c820dfbdad
commit
d93812315d
9 changed files with 786 additions and 12 deletions
107
app/components/draft/CommissionerControls.tsx
Normal file
107
app/components/draft/CommissionerControls.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { Button } from "~/components/ui/button";
|
||||
import { Play, Pause, FastForward } from "lucide-react";
|
||||
|
||||
interface CommissionerControlsProps {
|
||||
isDraftStarted: boolean;
|
||||
isDraftPaused: boolean;
|
||||
isDraftComplete: boolean;
|
||||
currentTeamName: string;
|
||||
onStartDraft: () => void;
|
||||
onPauseDraft: () => void;
|
||||
onResumeDraft: () => void;
|
||||
onForcePick: () => void;
|
||||
}
|
||||
|
||||
export function CommissionerControls({
|
||||
isDraftStarted,
|
||||
isDraftPaused,
|
||||
isDraftComplete,
|
||||
currentTeamName,
|
||||
onStartDraft,
|
||||
onPauseDraft,
|
||||
onResumeDraft,
|
||||
onForcePick,
|
||||
}: CommissionerControlsProps) {
|
||||
if (isDraftComplete) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg bg-green-500/10 p-4 text-center">
|
||||
<p className="font-medium text-green-600 dark:text-green-400">
|
||||
Draft Complete
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDraftStarted) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
The draft has not started yet. Click below to begin the draft.
|
||||
</p>
|
||||
<Button onClick={onStartDraft} className="w-full" size="lg">
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
Start Draft
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Draft Status */}
|
||||
<div className="rounded-lg border bg-muted/50 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Draft Status:</span>
|
||||
<span
|
||||
className={`text-sm font-semibold ${
|
||||
isDraftPaused
|
||||
? "text-yellow-600 dark:text-yellow-400"
|
||||
: "text-green-600 dark:text-green-400"
|
||||
}`}
|
||||
>
|
||||
{isDraftPaused ? "Paused" : "Active"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Current Pick:</span>
|
||||
<span className="text-sm text-muted-foreground">{currentTeamName}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Control Buttons */}
|
||||
<div className="space-y-2">
|
||||
{isDraftPaused ? (
|
||||
<Button onClick={onResumeDraft} className="w-full" variant="default">
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
Resume Draft
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={onPauseDraft} className="w-full" variant="outline">
|
||||
<Pause className="mr-2 h-4 w-4" />
|
||||
Pause Draft
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={onForcePick}
|
||||
className="w-full"
|
||||
variant="destructive"
|
||||
disabled={isDraftPaused}
|
||||
>
|
||||
<FastForward className="mr-2 h-4 w-4" />
|
||||
Force Pick for {currentTeamName}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Help Text */}
|
||||
<div className="rounded-lg bg-muted/30 p-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<strong>Force Pick:</strong> Automatically selects the top player from the
|
||||
current team's queue, or the highest EV available player if queue is empty.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ export default [
|
|||
route("leagues/:leagueId/draft", "routes/leagues/$leagueId.draft.tsx"),
|
||||
route("teams/:teamId/settings", "routes/teams/$teamId.settings.tsx"),
|
||||
route("api/webhooks/clerk", "routes/api/webhooks/clerk.ts"),
|
||||
route("api/draft-actions", "routes/api/draft-actions.ts"),
|
||||
route("user-profile", "routes/user-profile.tsx"),
|
||||
route("how-to-play", "routes/how-to-play.tsx"),
|
||||
|
||||
|
|
|
|||
95
app/routes/api/draft-actions.ts
Normal file
95
app/routes/api/draft-actions.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { data, type ActionFunctionArgs } from "react-router";
|
||||
import { getAuth } from "@clerk/react-router/server";
|
||||
import {
|
||||
addToQueue,
|
||||
removeFromQueue,
|
||||
reorderQueueWithSeason,
|
||||
findTeamById,
|
||||
getTeamQueue,
|
||||
} from "~/models";
|
||||
|
||||
export async function action(args: ActionFunctionArgs) {
|
||||
const { userId } = await getAuth(args);
|
||||
if (!userId) {
|
||||
return data({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { request } = args;
|
||||
|
||||
const formData = await request.formData();
|
||||
const actionType = formData.get("action");
|
||||
const teamId = formData.get("teamId");
|
||||
|
||||
if (!teamId || typeof teamId !== "string") {
|
||||
return data({ error: "Team ID is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Verify user owns this team
|
||||
const team = await findTeamById(teamId);
|
||||
if (!team || team.ownerId !== userId) {
|
||||
return data({ error: "You do not own this team" }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
switch (actionType) {
|
||||
case "add-to-queue": {
|
||||
const participantId = formData.get("participantId");
|
||||
const seasonId = formData.get("seasonId");
|
||||
|
||||
if (!participantId || typeof participantId !== "string") {
|
||||
return data({ error: "Participant ID is required" }, { status: 400 });
|
||||
}
|
||||
if (!seasonId || typeof seasonId !== "string") {
|
||||
return data({ error: "Season ID is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get current queue to determine position
|
||||
const currentQueue = await getTeamQueue(teamId);
|
||||
const queuePosition = currentQueue.length + 1;
|
||||
|
||||
await addToQueue({
|
||||
seasonId,
|
||||
teamId,
|
||||
participantId,
|
||||
queuePosition,
|
||||
});
|
||||
|
||||
return data({ success: true });
|
||||
}
|
||||
|
||||
case "remove-from-queue": {
|
||||
const queueItemId = formData.get("queueItemId");
|
||||
|
||||
if (!queueItemId || typeof queueItemId !== "string") {
|
||||
return data({ error: "Queue item ID is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
await removeFromQueue(queueItemId);
|
||||
return data({ success: true });
|
||||
}
|
||||
|
||||
case "reorder-queue": {
|
||||
const participantIds = formData.get("participantIds");
|
||||
const seasonId = formData.get("seasonId");
|
||||
|
||||
if (!participantIds || typeof participantIds !== "string") {
|
||||
return data({ error: "Participant IDs are required" }, { status: 400 });
|
||||
}
|
||||
if (!seasonId || typeof seasonId !== "string") {
|
||||
return data({ error: "Season ID is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const idsArray = JSON.parse(participantIds);
|
||||
await reorderQueueWithSeason(seasonId, teamId, idsArray);
|
||||
|
||||
return data({ success: true });
|
||||
}
|
||||
|
||||
default:
|
||||
return data({ error: "Invalid action" }, { status: 400 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Draft action error:", error);
|
||||
return data({ error: "Failed to perform action" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
203
app/routes/api/draft-commissioner.ts
Normal file
203
app/routes/api/draft-commissioner.ts
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
import { data, type ActionFunctionArgs } from "react-router";
|
||||
import { getAuth } from "@clerk/react-router/server";
|
||||
import {
|
||||
findSeasonById,
|
||||
updateSeason,
|
||||
updateSeasonStatus,
|
||||
findTeamsBySeasonId,
|
||||
isCommissioner,
|
||||
createDraftPick,
|
||||
initializeDraftTimers,
|
||||
updateTeamTimer,
|
||||
getSeasonTimers,
|
||||
autoPickForTeam,
|
||||
calculatePickInfo,
|
||||
removeFromQueue,
|
||||
getTeamQueue,
|
||||
} from "~/models";
|
||||
import { getSocketIO } from "../../../server/socket.js";
|
||||
|
||||
export async function action(args: ActionFunctionArgs) {
|
||||
const { userId } = await getAuth(args);
|
||||
if (!userId) {
|
||||
return data({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { request } = args;
|
||||
const formData = await request.formData();
|
||||
const actionType = formData.get("action");
|
||||
const seasonId = formData.get("seasonId");
|
||||
|
||||
if (!seasonId || typeof seasonId !== "string") {
|
||||
return data({ error: "Season ID is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get season
|
||||
const season = await findSeasonById(seasonId);
|
||||
if (!season) {
|
||||
return data({ error: "Season not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Verify user is commissioner
|
||||
const isUserCommissioner = await isCommissioner(season.leagueId, userId);
|
||||
if (!isUserCommissioner) {
|
||||
return data({ error: "Only commissioners can perform this action" }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const io = getSocketIO();
|
||||
|
||||
switch (actionType) {
|
||||
case "start-draft": {
|
||||
// Check if draft already started
|
||||
if (season.draftStartedAt) {
|
||||
return data({ error: "Draft has already started" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get teams
|
||||
const teams = await findTeamsBySeasonId(seasonId);
|
||||
|
||||
// Initialize timers
|
||||
await initializeDraftTimers(seasonId, teams, season.draftInitialTime || 120);
|
||||
|
||||
// Update season
|
||||
await updateSeason(seasonId, {
|
||||
draftStartedAt: new Date(),
|
||||
currentPickNumber: 1,
|
||||
draftPaused: false,
|
||||
});
|
||||
await updateSeasonStatus(seasonId, "draft");
|
||||
|
||||
// Get updated timers
|
||||
const timers = await getSeasonTimers(seasonId);
|
||||
|
||||
// Broadcast to all clients
|
||||
io.to(`draft-${seasonId}`).emit("draft-started", {
|
||||
seasonId,
|
||||
startedAt: new Date(),
|
||||
timers,
|
||||
});
|
||||
|
||||
return data({ success: true });
|
||||
}
|
||||
|
||||
case "pause-draft": {
|
||||
const pause = formData.get("pause") === "true";
|
||||
|
||||
// Check if draft has started
|
||||
if (!season.draftStartedAt) {
|
||||
return data({ error: "Draft has not started yet" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Update season
|
||||
await updateSeason(seasonId, { draftPaused: pause });
|
||||
|
||||
// Broadcast to all clients
|
||||
io.to(`draft-${seasonId}`).emit("draft-paused", {
|
||||
seasonId,
|
||||
isPaused: pause,
|
||||
});
|
||||
|
||||
return data({ success: true });
|
||||
}
|
||||
|
||||
case "force-pick": {
|
||||
const teamId = formData.get("teamId");
|
||||
|
||||
if (!teamId || typeof teamId !== "string") {
|
||||
return data({ error: "Team ID is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if draft has started
|
||||
if (!season.draftStartedAt) {
|
||||
return data({ error: "Draft has not started" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if draft is complete
|
||||
const teams = await findTeamsBySeasonId(seasonId);
|
||||
const totalPicks = season.draftRounds * teams.length;
|
||||
const currentPickNumber = season.currentPickNumber || 1;
|
||||
|
||||
if (currentPickNumber > totalPicks) {
|
||||
return data({ error: "Draft is already complete" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Auto-pick for the team
|
||||
const participantId = await autoPickForTeam(seasonId, teamId);
|
||||
|
||||
if (!participantId) {
|
||||
return data({ error: "No available participants to pick" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Calculate pick info
|
||||
const { round, pickInRound } = calculatePickInfo(currentPickNumber, teams.length);
|
||||
|
||||
// Create the draft pick
|
||||
const pick = await createDraftPick({
|
||||
seasonId,
|
||||
teamId,
|
||||
participantId,
|
||||
pickNumber: currentPickNumber,
|
||||
round,
|
||||
pickInRound,
|
||||
pickedByUserId: userId,
|
||||
pickedByType: "commissioner",
|
||||
timeUsed: 0,
|
||||
});
|
||||
|
||||
// Remove from queue if it was queued
|
||||
const queue = await getTeamQueue(teamId);
|
||||
const queueItem = queue.find((item: any) => item.participantId === participantId);
|
||||
if (queueItem) {
|
||||
await removeFromQueue(queueItem.id);
|
||||
}
|
||||
|
||||
// Update season to next pick
|
||||
const nextPickNumber = currentPickNumber + 1;
|
||||
await updateSeason(seasonId, { currentPickNumber: nextPickNumber });
|
||||
|
||||
// Add increment time to current team's timer
|
||||
if (season.draftIncrementTime) {
|
||||
const timers = await getSeasonTimers(seasonId);
|
||||
const currentTimer = timers.find((t: any) => t.teamId === teamId);
|
||||
if (currentTimer) {
|
||||
const newTime = currentTimer.timeRemaining + season.draftIncrementTime;
|
||||
await updateTeamTimer(teamId, newTime);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if draft is complete
|
||||
if (nextPickNumber > totalPicks) {
|
||||
await updateSeasonStatus(seasonId, "active");
|
||||
io.to(`draft-${seasonId}`).emit("draft-completed", {
|
||||
seasonId,
|
||||
});
|
||||
}
|
||||
|
||||
// Broadcast pick to all clients in the room
|
||||
io.to(`draft-${seasonId}`).emit("pick-made", {
|
||||
pick,
|
||||
nextPickNumber,
|
||||
isDraftComplete: nextPickNumber > totalPicks,
|
||||
});
|
||||
|
||||
// Emit queue update to team owner
|
||||
const updatedQueue = await getTeamQueue(teamId);
|
||||
io.to(`draft-${seasonId}`).emit("queue-updated", {
|
||||
teamId,
|
||||
queue: updatedQueue,
|
||||
});
|
||||
|
||||
return data({ success: true, pick });
|
||||
}
|
||||
|
||||
default:
|
||||
return data({ error: "Invalid action" }, { status: 400 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Commissioner action error:", error);
|
||||
return data({
|
||||
error: error instanceof Error ? error.message : "Failed to perform action"
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
175
app/routes/api/draft-pick.ts
Normal file
175
app/routes/api/draft-pick.ts
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
import { data, type ActionFunctionArgs } from "react-router";
|
||||
import { getAuth } from "@clerk/react-router/server";
|
||||
import {
|
||||
findSeasonById,
|
||||
updateSeason,
|
||||
updateSeasonStatus,
|
||||
findTeamsBySeasonId,
|
||||
isCommissioner,
|
||||
createDraftPick,
|
||||
isParticipantDrafted,
|
||||
updateTeamTimer,
|
||||
getSeasonTimers,
|
||||
calculatePickInfo,
|
||||
getTeamForPick,
|
||||
findDraftSlotsBySeasonId,
|
||||
removeFromQueue,
|
||||
getTeamQueue,
|
||||
} from "~/models";
|
||||
import { getSocketIO } from "../../../server/socket.js";
|
||||
|
||||
export async function action(args: ActionFunctionArgs) {
|
||||
const { userId } = await getAuth(args);
|
||||
if (!userId) {
|
||||
return data({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { request } = args;
|
||||
const formData = await request.formData();
|
||||
|
||||
const seasonId = formData.get("seasonId");
|
||||
const teamId = formData.get("teamId");
|
||||
const participantId = formData.get("participantId");
|
||||
|
||||
if (!seasonId || typeof seasonId !== "string") {
|
||||
return data({ error: "Season ID is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!teamId || typeof teamId !== "string") {
|
||||
return data({ error: "Team ID is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!participantId || typeof participantId !== "string") {
|
||||
return data({ error: "Participant ID is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
// Get season
|
||||
const season = await findSeasonById(seasonId);
|
||||
if (!season) {
|
||||
return data({ error: "Season not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Check if draft has started
|
||||
if (!season.draftStartedAt) {
|
||||
return data({ error: "Draft has not started" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if draft is paused
|
||||
if (season.draftPaused) {
|
||||
return data({ error: "Draft is currently paused" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if draft is complete
|
||||
const teams = await findTeamsBySeasonId(seasonId);
|
||||
const totalPicks = season.draftRounds * teams.length;
|
||||
const currentPickNumber = season.currentPickNumber || 1;
|
||||
|
||||
if (currentPickNumber > totalPicks) {
|
||||
return data({ error: "Draft is already complete" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get draft slots to determine current team
|
||||
const draftSlots = await findDraftSlotsBySeasonId(seasonId);
|
||||
const draftOrder = draftSlots.map((slot: any) => ({
|
||||
teamId: slot.teamId,
|
||||
draftOrder: slot.draftOrder,
|
||||
}));
|
||||
|
||||
const currentTeamId = getTeamForPick(currentPickNumber, draftOrder);
|
||||
|
||||
if (!currentTeamId) {
|
||||
return data({ error: "Could not determine current team" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Verify it's the correct team's turn
|
||||
if (teamId !== currentTeamId) {
|
||||
return data({ error: "It is not this team's turn" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Verify user has permission (team owner or commissioner)
|
||||
const team = teams.find((t: any) => t.id === teamId);
|
||||
const isTeamOwner = team?.ownerId === userId;
|
||||
const isUserCommissioner = await isCommissioner(season.leagueId, userId);
|
||||
|
||||
if (!isTeamOwner && !isUserCommissioner) {
|
||||
return data({ error: "You do not have permission to make this pick" }, { status: 403 });
|
||||
}
|
||||
|
||||
// Check if participant is already drafted
|
||||
const isDrafted = await isParticipantDrafted(seasonId, participantId);
|
||||
if (isDrafted) {
|
||||
return data({ error: "This participant has already been drafted" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Calculate pick info
|
||||
const { round, pickInRound } = calculatePickInfo(currentPickNumber, teams.length);
|
||||
|
||||
// Determine picked by type
|
||||
const pickedByType = isUserCommissioner && !isTeamOwner ? "commissioner" : "owner";
|
||||
|
||||
// Create the draft pick
|
||||
const pick = await createDraftPick({
|
||||
seasonId,
|
||||
teamId,
|
||||
participantId,
|
||||
pickNumber: currentPickNumber,
|
||||
round,
|
||||
pickInRound,
|
||||
pickedByUserId: userId,
|
||||
pickedByType,
|
||||
timeUsed: 0,
|
||||
});
|
||||
|
||||
// Remove from queue if it was queued
|
||||
const queue = await getTeamQueue(teamId);
|
||||
const queueItem = queue.find((item: any) => item.participantId === participantId);
|
||||
if (queueItem) {
|
||||
await removeFromQueue(queueItem.id);
|
||||
}
|
||||
|
||||
// Update season to next pick
|
||||
const nextPickNumber = currentPickNumber + 1;
|
||||
await updateSeason(seasonId, { currentPickNumber: nextPickNumber });
|
||||
|
||||
// Add increment time to current team's timer
|
||||
if (season.draftIncrementTime) {
|
||||
const timers = await getSeasonTimers(seasonId);
|
||||
const currentTimer = timers.find((t: any) => t.teamId === teamId);
|
||||
if (currentTimer) {
|
||||
const newTime = currentTimer.timeRemaining + season.draftIncrementTime;
|
||||
await updateTeamTimer(teamId, newTime);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if draft is complete
|
||||
const io = getSocketIO();
|
||||
if (nextPickNumber > totalPicks) {
|
||||
await updateSeasonStatus(seasonId, "active");
|
||||
io.to(`draft-${seasonId}`).emit("draft-completed", {
|
||||
seasonId,
|
||||
});
|
||||
}
|
||||
|
||||
// Broadcast pick to all clients in the room
|
||||
io.to(`draft-${seasonId}`).emit("pick-made", {
|
||||
pick,
|
||||
nextPickNumber,
|
||||
isDraftComplete: nextPickNumber > totalPicks,
|
||||
});
|
||||
|
||||
// Emit queue update to team owner
|
||||
const updatedQueue = await getTeamQueue(teamId);
|
||||
io.to(`draft-${seasonId}`).emit("queue-updated", {
|
||||
teamId,
|
||||
queue: updatedQueue,
|
||||
});
|
||||
|
||||
return data({ success: true, pick });
|
||||
} catch (error) {
|
||||
console.error("Error making pick:", error);
|
||||
return data({
|
||||
error: error instanceof Error ? error.message : "Failed to make pick",
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { data, redirect } from "react-router";
|
||||
import type { LoaderFunctionArgs } from "react-router";
|
||||
import { getAuth } from "@clerk/react-router/server";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
findLeagueById,
|
||||
findCurrentSeason,
|
||||
|
|
@ -11,10 +12,13 @@ import {
|
|||
getSeasonTimers,
|
||||
findSeasonWithSportsSeasons,
|
||||
getTeamQueue,
|
||||
getTeamForPick,
|
||||
} from "~/models";
|
||||
import { DraftGrid } from "~/components/draft/DraftGrid";
|
||||
import { PlayerList } from "~/components/draft/PlayerList";
|
||||
import { DraftQueue } from "~/components/draft/DraftQueue";
|
||||
import { CommissionerControls } from "~/components/draft/CommissionerControls";
|
||||
import { useDraftSocket } from "~/hooks/useDraftSocket";
|
||||
|
||||
export async function loader(args: LoaderFunctionArgs) {
|
||||
const { userId } = await getAuth(args);
|
||||
|
|
@ -114,17 +118,173 @@ export default function DraftRoom({ loaderData }: { loaderData: Awaited<ReturnTy
|
|||
league,
|
||||
season,
|
||||
teams,
|
||||
draftPicks,
|
||||
draftPicks: initialDraftPicks,
|
||||
participants,
|
||||
queueItems,
|
||||
timers,
|
||||
queueItems: initialQueueItems,
|
||||
timers: initialTimers,
|
||||
isCommissioner,
|
||||
userTeam,
|
||||
currentPickNumber,
|
||||
currentPickNumber: initialPickNumber,
|
||||
totalPicks,
|
||||
isDraftComplete,
|
||||
isDraftComplete: initialIsDraftComplete,
|
||||
draftSlots,
|
||||
} = loaderData;
|
||||
|
||||
// Local state for real-time updates
|
||||
const [draftPicks, setDraftPicks] = useState(initialDraftPicks);
|
||||
const [queueItems, setQueueItems] = useState(initialQueueItems);
|
||||
const [timers, setTimers] = useState(initialTimers);
|
||||
const [currentPickNumber, setCurrentPickNumber] = useState(initialPickNumber);
|
||||
const [isDraftComplete, setIsDraftComplete] = useState(initialIsDraftComplete);
|
||||
const [isDraftStarted, setIsDraftStarted] = useState(!!season.draftStartedAt);
|
||||
const [isDraftPaused, setIsDraftPaused] = useState(season.draftPaused || false);
|
||||
|
||||
// Socket connection
|
||||
const { on, off } = useDraftSocket(season.id);
|
||||
|
||||
// Calculate current team
|
||||
const draftOrder = draftSlots.map((slot: any) => ({
|
||||
teamId: slot.teamId,
|
||||
draftOrder: slot.draftOrder,
|
||||
}));
|
||||
const currentTeamId = getTeamForPick(currentPickNumber, draftOrder);
|
||||
const currentTeam = teams.find((t: any) => t.id === currentTeamId);
|
||||
const currentTeamName = currentTeam?.name || "Unknown";
|
||||
|
||||
// Socket event handlers
|
||||
useEffect(() => {
|
||||
// Listen for pick made
|
||||
const handlePickMade = (data: any) => {
|
||||
setDraftPicks((prev: any) => [...prev, data.pick]);
|
||||
setCurrentPickNumber(data.nextPickNumber);
|
||||
setIsDraftComplete(data.isDraftComplete);
|
||||
};
|
||||
|
||||
// Listen for draft started
|
||||
const handleDraftStarted = (data: any) => {
|
||||
setIsDraftStarted(true);
|
||||
setIsDraftPaused(false);
|
||||
setTimers(data.timers);
|
||||
};
|
||||
|
||||
// Listen for draft paused
|
||||
const handleDraftPaused = (data: any) => {
|
||||
setIsDraftPaused(data.isPaused);
|
||||
};
|
||||
|
||||
// Listen for draft completed
|
||||
const handleDraftCompleted = () => {
|
||||
setIsDraftComplete(true);
|
||||
};
|
||||
|
||||
// Listen for queue updates
|
||||
const handleQueueUpdated = (data: any) => {
|
||||
if (userTeam && data.teamId === userTeam.id) {
|
||||
setQueueItems(data.queue);
|
||||
}
|
||||
};
|
||||
|
||||
on("pick-made", handlePickMade);
|
||||
on("draft-started", handleDraftStarted);
|
||||
on("draft-paused", handleDraftPaused);
|
||||
on("draft-completed", handleDraftCompleted);
|
||||
on("queue-updated", handleQueueUpdated);
|
||||
|
||||
return () => {
|
||||
off("pick-made", handlePickMade);
|
||||
off("draft-started", handleDraftStarted);
|
||||
off("draft-paused", handleDraftPaused);
|
||||
off("draft-completed", handleDraftCompleted);
|
||||
off("queue-updated", handleQueueUpdated);
|
||||
};
|
||||
}, [on, off, userTeam]);
|
||||
|
||||
// Commissioner action handlers
|
||||
const handleStartDraft = async () => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("action", "start-draft");
|
||||
formData.append("seasonId", season.id);
|
||||
|
||||
const response = await fetch("/api/draft-commissioner", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
console.error("Failed to start draft:", error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error starting draft:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePauseDraft = async () => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("action", "pause-draft");
|
||||
formData.append("seasonId", season.id);
|
||||
formData.append("pause", "true");
|
||||
|
||||
const response = await fetch("/api/draft-commissioner", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
console.error("Failed to pause draft:", error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error pausing draft:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResumeDraft = async () => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("action", "pause-draft");
|
||||
formData.append("seasonId", season.id);
|
||||
formData.append("pause", "false");
|
||||
|
||||
const response = await fetch("/api/draft-commissioner", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
console.error("Failed to resume draft:", error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error resuming draft:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleForcePick = async () => {
|
||||
if (!currentTeamId) return;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("action", "force-pick");
|
||||
formData.append("seasonId", season.id);
|
||||
formData.append("teamId", currentTeamId);
|
||||
|
||||
const response = await fetch("/api/draft-commissioner", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
console.error("Failed to force pick:", error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error forcing pick:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
|
|
@ -213,9 +373,16 @@ export default function DraftRoom({ loaderData }: { loaderData: Awaited<ReturnTy
|
|||
{isCommissioner ? (
|
||||
<div className="rounded-lg border bg-card p-6">
|
||||
<h2 className="mb-4 text-xl font-bold">Commissioner Controls</h2>
|
||||
<div className="flex items-center justify-center py-12 text-muted-foreground">
|
||||
Controls will be implemented in Phase 9
|
||||
</div>
|
||||
<CommissionerControls
|
||||
isDraftStarted={isDraftStarted}
|
||||
isDraftPaused={isDraftPaused}
|
||||
isDraftComplete={isDraftComplete}
|
||||
currentTeamName={currentTeamName}
|
||||
onStartDraft={handleStartDraft}
|
||||
onPauseDraft={handlePauseDraft}
|
||||
onResumeDraft={handleResumeDraft}
|
||||
onForcePick={handleForcePick}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border bg-card p-6">
|
||||
|
|
@ -229,6 +396,10 @@ export default function DraftRoom({ loaderData }: { loaderData: Awaited<ReturnTy
|
|||
<span className="text-muted-foreground">Draft Rounds:</span>
|
||||
<span className="ml-2 font-medium">{season.draftRounds}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Current Team:</span>
|
||||
<span className="ml-2 font-medium">{currentTeamName}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -32,13 +32,29 @@ export function setupSocketIO(httpServer: HTTPServer): SocketServer {
|
|||
console.log(`Socket ${socket.id} left draft room: ${roomName}`);
|
||||
});
|
||||
|
||||
// Draft Actions - Phase 9
|
||||
// These events will be handled by API routes and then broadcast back via socket
|
||||
// The client will call API routes which will then use getSocketIO() to broadcast
|
||||
socket.on("make-pick", (_data) => {
|
||||
console.log("make-pick event received, should use API route instead");
|
||||
});
|
||||
|
||||
socket.on("commissioner-start-draft", (_data) => {
|
||||
console.log("commissioner-start-draft event received, should use API route instead");
|
||||
});
|
||||
|
||||
socket.on("commissioner-pause-draft", (_data) => {
|
||||
console.log("commissioner-pause-draft event received, should use API route instead");
|
||||
});
|
||||
|
||||
socket.on("commissioner-force-pick", (_data) => {
|
||||
console.log("commissioner-force-pick event received, should use API route instead");
|
||||
});
|
||||
|
||||
// Handle disconnection
|
||||
socket.on("disconnect", () => {
|
||||
console.log(`Client disconnected: ${socket.id}`);
|
||||
});
|
||||
|
||||
// Draft-specific events will be added in Phase 9
|
||||
// For now, we're just setting up the infrastructure
|
||||
});
|
||||
|
||||
return io;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
"server/**/*.ts",
|
||||
"vite.config.ts",
|
||||
"database/**/*.ts",
|
||||
"app/contexts/**/*.ts"
|
||||
"app/contexts/**/*.ts",
|
||||
"app/models/**/*.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
|
|
|
|||
|
|
@ -15,4 +15,9 @@ export default defineConfig(({ isSsrBuild }) => ({
|
|||
server: {
|
||||
allowedHosts: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"~/database": "/database",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue