feat: add queue management UI and API endpoints for draft room
This commit is contained in:
parent
83e40bd59f
commit
1ad14bfef1
6 changed files with 338 additions and 11 deletions
|
|
@ -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"),
|
||||
|
|
|
|||
53
app/routes/api/queue.add.ts
Normal file
53
app/routes/api/queue.add.ts
Normal file
|
|
@ -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 });
|
||||
}
|
||||
42
app/routes/api/queue.clear.ts
Normal file
42
app/routes/api/queue.clear.ts
Normal file
|
|
@ -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 });
|
||||
}
|
||||
46
app/routes/api/queue.remove.ts
Normal file
46
app/routes/api/queue.remove.ts
Normal file
|
|
@ -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 });
|
||||
}
|
||||
49
app/routes/api/queue.reorder.ts
Normal file
49
app/routes/api/queue.reorder.ts
Normal file
|
|
@ -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 });
|
||||
}
|
||||
|
|
@ -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<typeof loader>();
|
||||
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<string>("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() {
|
|||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* My Queue */}
|
||||
{userTeam && (
|
||||
<div>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold">My Queue ({queue.length})</h2>
|
||||
{queue.length > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleClearQueue}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{queue.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm text-center py-8">
|
||||
Click participants to add to your queue
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{queue.map((item: any, index: number) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-center justify-between p-3 bg-muted rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="default">{index + 1}</Badge>
|
||||
<div>
|
||||
<p className="font-semibold text-sm">
|
||||
{availableParticipants.find((p: any) => p.id === item.participantId)?.name || "Unknown"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveFromQueue(item.id)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recent Picks */}
|
||||
<div>
|
||||
<div className={userTeam ? "" : "lg:col-span-2"}>
|
||||
<Card className="p-4">
|
||||
<h2 className="text-xl font-semibold mb-4">Recent Picks</h2>
|
||||
{picks.length === 0 ? (
|
||||
|
|
@ -457,6 +562,7 @@ export default function DraftRoom() {
|
|||
<th className="text-left p-3 font-semibold">Participant</th>
|
||||
<th className="text-left p-3 font-semibold">Sport</th>
|
||||
<th className="text-right p-3 font-semibold">EV</th>
|
||||
{userTeam && <th className="text-right p-3 font-semibold">Queue</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -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 (
|
||||
<tr
|
||||
key={participant.id}
|
||||
className={`border-t transition-colors ${
|
||||
isDrafted
|
||||
? "bg-muted/50 opacity-60 cursor-not-allowed"
|
||||
: "hover:bg-muted/50 cursor-pointer"
|
||||
? "bg-muted/50 opacity-60"
|
||||
: "hover:bg-muted/50"
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (!isDrafted) {
|
||||
// TODO: Add to queue functionality (Phase 7)
|
||||
console.log("Add to queue:", participant.name);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<td className="p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -493,6 +594,11 @@ export default function DraftRoom() {
|
|||
Drafted
|
||||
</Badge>
|
||||
)}
|
||||
{isInQueue && !isDrafted && (
|
||||
<Badge variant="default" className="text-xs">
|
||||
Queued
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-3 text-muted-foreground">
|
||||
|
|
@ -501,6 +607,33 @@ export default function DraftRoom() {
|
|||
<td className="p-3 text-right font-mono font-semibold">
|
||||
{participant.expectedValue}
|
||||
</td>
|
||||
{userTeam && (
|
||||
<td className="p-3 text-right">
|
||||
{!isDrafted && !isInQueue && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleAddToQueue(participant.id)}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
)}
|
||||
{!isDrafted && isInQueue && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const queueItem = queue.find((item: any) => item.participantId === participant.id);
|
||||
if (queueItem) {
|
||||
handleRemoveFromQueue(queueItem.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue