diff --git a/app/routes.ts b/app/routes.ts index 8212555..303e1bd 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -12,6 +12,10 @@ export default [ ), route("teams/:teamId/settings", "routes/teams/$teamId.settings.tsx"), route("api/webhooks/clerk", "routes/api/webhooks/clerk.ts"), + route("api/queue/add", "routes/api/queue.add.ts"), + route("api/queue/remove", "routes/api/queue.remove.ts"), + route("api/queue/clear", "routes/api/queue.clear.ts"), + route("api/queue/reorder", "routes/api/queue.reorder.ts"), route("user-profile", "routes/user-profile.tsx"), route("how-to-play", "routes/how-to-play.tsx"), route("test-socket", "routes/test-socket.tsx"), diff --git a/app/routes/api/queue.add.ts b/app/routes/api/queue.add.ts new file mode 100644 index 0000000..aed87e3 --- /dev/null +++ b/app/routes/api/queue.add.ts @@ -0,0 +1,53 @@ +import { getAuth } from "@clerk/react-router/server"; +import { database } from "~/database/context"; +import * as schema from "~/database/schema"; +import { eq } from "drizzle-orm"; +import { addToQueue, getTeamQueue } from "~/models/draft-queue"; + +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"); + const teamId = formData.get("teamId"); + const participantId = formData.get("participantId"); + + if (!seasonId || !teamId || !participantId) { + return Response.json({ error: "Missing required fields" }, { status: 400 }); + } + + const db = database(); + + // Verify user owns this team + const team = await db.query.teams.findFirst({ + where: eq(schema.teams.id, teamId as string), + }); + + if (!team || team.ownerId !== userId) { + return Response.json({ error: "Unauthorized" }, { status: 403 }); + } + + // Get current queue to determine position + const currentQueue = await getTeamQueue(teamId as string); + const queuePosition = currentQueue.length + 1; + + // Add to queue + const queueItem = await addToQueue({ + seasonId: seasonId as string, + teamId: teamId as string, + participantId: participantId as string, + queuePosition, + }); + + // TODO: Emit socket event for real-time update + // const io = getSocketIO(); + // io.to(`draft-${seasonId}`).emit("queue-updated", { teamId, queue: [...currentQueue, queueItem] }); + + return Response.json({ success: true, queueItem }); +} diff --git a/app/routes/api/queue.clear.ts b/app/routes/api/queue.clear.ts new file mode 100644 index 0000000..cc3bf4b --- /dev/null +++ b/app/routes/api/queue.clear.ts @@ -0,0 +1,42 @@ +import { getAuth } from "@clerk/react-router/server"; +import { database } from "~/database/context"; +import * as schema from "~/database/schema"; +import { eq } from "drizzle-orm"; +import { clearTeamQueue } from "~/models/draft-queue"; + +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 teamId = formData.get("teamId"); + + if (!teamId) { + return Response.json({ error: "Missing required fields" }, { status: 400 }); + } + + const db = database(); + + // Verify user owns this team + const team = await db.query.teams.findFirst({ + where: eq(schema.teams.id, teamId as string), + }); + + if (!team || team.ownerId !== userId) { + return Response.json({ error: "Unauthorized" }, { status: 403 }); + } + + // Clear queue + await clearTeamQueue(teamId as string); + + // TODO: Emit socket event for real-time update + // const io = getSocketIO(); + // io.to(`draft-${seasonId}`).emit("queue-updated", { teamId, queue: [] }); + + return Response.json({ success: true }); +} diff --git a/app/routes/api/queue.remove.ts b/app/routes/api/queue.remove.ts new file mode 100644 index 0000000..f5aceb3 --- /dev/null +++ b/app/routes/api/queue.remove.ts @@ -0,0 +1,46 @@ +import { getAuth } from "@clerk/react-router/server"; +import { database } from "~/database/context"; +import * as schema from "~/database/schema"; +import { eq } from "drizzle-orm"; +import { removeFromQueue, getTeamQueue } from "~/models/draft-queue"; + +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 queueId = formData.get("queueId"); + const teamId = formData.get("teamId"); + + if (!queueId || !teamId) { + return Response.json({ error: "Missing required fields" }, { status: 400 }); + } + + const db = database(); + + // Verify user owns this team + const team = await db.query.teams.findFirst({ + where: eq(schema.teams.id, teamId as string), + }); + + if (!team || team.ownerId !== userId) { + return Response.json({ error: "Unauthorized" }, { status: 403 }); + } + + // Remove from queue + await removeFromQueue(queueId as string); + + // Get updated queue + const updatedQueue = await getTeamQueue(teamId as string); + + // TODO: Emit socket event for real-time update + // const io = getSocketIO(); + // io.to(`draft-${seasonId}`).emit("queue-updated", { teamId, queue: updatedQueue }); + + return Response.json({ success: true, queue: updatedQueue }); +} diff --git a/app/routes/api/queue.reorder.ts b/app/routes/api/queue.reorder.ts new file mode 100644 index 0000000..f7a6837 --- /dev/null +++ b/app/routes/api/queue.reorder.ts @@ -0,0 +1,49 @@ +import { getAuth } from "@clerk/react-router/server"; +import { database } from "~/database/context"; +import * as schema from "~/database/schema"; +import { eq } from "drizzle-orm"; +import { reorderQueueWithSeason, getTeamQueue } from "~/models/draft-queue"; + +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 teamId = formData.get("teamId"); + const seasonId = formData.get("seasonId"); + const participantIdsJson = formData.get("participantIds"); + + if (!teamId || !seasonId || !participantIdsJson) { + return Response.json({ error: "Missing required fields" }, { status: 400 }); + } + + const participantIds = JSON.parse(participantIdsJson as string); + + const db = database(); + + // Verify user owns this team + const team = await db.query.teams.findFirst({ + where: eq(schema.teams.id, teamId as string), + }); + + if (!team || team.ownerId !== userId) { + return Response.json({ error: "Unauthorized" }, { status: 403 }); + } + + // Reorder queue + await reorderQueueWithSeason(seasonId as string, teamId as string, participantIds); + + // Get updated queue + const updatedQueue = await getTeamQueue(teamId as string); + + // TODO: Emit socket event for real-time update + // const io = getSocketIO(); + // io.to(`draft-${seasonId}`).emit("queue-updated", { teamId, queue: updatedQueue }); + + return Response.json({ success: true, queue: updatedQueue }); +} diff --git a/app/routes/leagues/$leagueId.draft.$seasonId.tsx b/app/routes/leagues/$leagueId.draft.$seasonId.tsx index 11bec40..b4b769b 100644 --- a/app/routes/leagues/$leagueId.draft.$seasonId.tsx +++ b/app/routes/leagues/$leagueId.draft.$seasonId.tsx @@ -136,7 +136,7 @@ export async function loader(args: any) { } export default function DraftRoom() { - const { season, draftSlots, draftPicks, availableParticipants, userTeam: _userTeam, isCommissioner: _isCommissioner } = + const { season, draftSlots, draftPicks, availableParticipants, userTeam, userQueue, isCommissioner: _isCommissioner } = useLoaderData(); const { isConnected, on, off } = useDraftSocket(season.id); const [picks, setPicks] = useState(draftPicks); @@ -144,6 +144,7 @@ export default function DraftRoom() { const [searchQuery, setSearchQuery] = useState(""); const [hideDrafted, setHideDrafted] = useState(true); const [sportFilter, setSportFilter] = useState("all"); + const [queue, setQueue] = useState(userQueue); // Listen for new picks from other users useEffect(() => { @@ -160,6 +161,60 @@ export default function DraftRoom() { }; }, [on, off]); + // Queue handlers + const handleAddToQueue = async (participantId: string) => { + if (!userTeam) return; + + const formData = new FormData(); + formData.append("seasonId", season.id); + formData.append("teamId", userTeam.id); + formData.append("participantId", participantId); + + const response = await fetch("/api/queue/add", { + method: "POST", + body: formData, + }); + + if (response.ok) { + const data = await response.json(); + setQueue((prev: any) => [...prev, data.queueItem]); + } + }; + + const handleRemoveFromQueue = async (queueId: string) => { + if (!userTeam) return; + + const formData = new FormData(); + formData.append("queueId", queueId); + formData.append("teamId", userTeam.id); + + const response = await fetch("/api/queue/remove", { + method: "POST", + body: formData, + }); + + if (response.ok) { + const data = await response.json(); + setQueue(data.queue); + } + }; + + const handleClearQueue = async () => { + if (!userTeam) return; + + const formData = new FormData(); + formData.append("teamId", userTeam.id); + + const response = await fetch("/api/queue/clear", { + method: "POST", + body: formData, + }); + + if (response.ok) { + setQueue([]); + } + }; + // Calculate current round const currentRound = Math.ceil(currentPick / draftSlots.length); @@ -359,9 +414,59 @@ export default function DraftRoom() { -
+
+ {/* My Queue */} + {userTeam && ( +
+ +
+

My Queue ({queue.length})

+ {queue.length > 0 && ( + + )} +
+ {queue.length === 0 ? ( +

+ Click participants to add to your queue +

+ ) : ( +
+ {queue.map((item: any, index: number) => ( +
+
+ {index + 1} +
+

+ {availableParticipants.find((p: any) => p.id === item.participantId)?.name || "Unknown"} +

+
+
+ +
+ ))} +
+ )} +
+
+ )} + {/* Recent Picks */} -
+

Recent Picks

{picks.length === 0 ? ( @@ -457,6 +562,7 @@ export default function DraftRoom() { Participant Sport EV + {userTeam && Queue} @@ -469,21 +575,16 @@ export default function DraftRoom() { ) : ( filteredParticipants.map((participant: any) => { const isDrafted = draftedParticipantIds.has(participant.id); + const isInQueue = queue.some((item: any) => item.participantId === participant.id); return ( { - if (!isDrafted) { - // TODO: Add to queue functionality (Phase 7) - console.log("Add to queue:", participant.name); - } - }} >
@@ -493,6 +594,11 @@ export default function DraftRoom() { Drafted )} + {isInQueue && !isDrafted && ( + + Queued + + )}
@@ -501,6 +607,33 @@ export default function DraftRoom() { {participant.expectedValue} + {userTeam && ( + + {!isDrafted && !isInQueue && ( + + )} + {!isDrafted && isInQueue && ( + + )} + + )} ); })