* Show manager username instead of team name on draft board Both the read-only draft board and the active draft room now display the manager's username (falling back to displayName if no username is set, and to team name if the team is unassigned) in the column headers of the draft grid. https://claude.ai/code/session_01C97GauJaAB83NVWxdpNVh1 * Address code review: extract ownerMap helper, fix TeamsDraftedGrid - Extract ownerMap building into app/lib/owner-map.ts to eliminate duplication across the two loaders - Guard against null username AND displayName so the map only contains real string values - Apply username display to TeamsDraftedGrid (the "Teams" tab in the active draft room), which was missed in the initial change https://claude.ai/code/session_01C97GauJaAB83NVWxdpNVh1 --------- Co-authored-by: Claude <noreply@anthropic.com>
198 lines
6.2 KiB
TypeScript
198 lines
6.2 KiB
TypeScript
import { useLoaderData } from "react-router";
|
|
import { eq, asc, and } from "drizzle-orm";
|
|
import { getAuth } from "@clerk/react-router/server";
|
|
import { database } from "~/database/context";
|
|
import * as schema from "~/database/schema";
|
|
import { DraftGrid } from "~/components/DraftGrid";
|
|
import { useDraftSocket } from "~/hooks/useDraftSocket";
|
|
import { useState, useEffect } from "react";
|
|
import { buildOwnerMap } from "~/lib/owner-map";
|
|
|
|
export async function loader(args: any) {
|
|
const { params } = args;
|
|
const { leagueId, seasonId } = params;
|
|
|
|
if (!seasonId) {
|
|
throw new Response("Season ID is required", { status: 400 });
|
|
}
|
|
|
|
const db = database();
|
|
|
|
// Get season details
|
|
const season = await db.query.seasons.findFirst({
|
|
where: eq(schema.seasons.id, seasonId),
|
|
with: {
|
|
league: true,
|
|
},
|
|
});
|
|
|
|
if (!season) {
|
|
throw new Response("Season not found", { status: 404 });
|
|
}
|
|
|
|
// Validate that the season actually belongs to the league in the URL
|
|
if (season.leagueId !== leagueId) {
|
|
throw new Response("Season not found", { status: 404 });
|
|
}
|
|
|
|
// Check access: public boards are accessible to everyone without auth
|
|
if (!season.league.isPublicDraftBoard) {
|
|
// Not public - check if the user is a league member or commissioner
|
|
const auth = await getAuth(args);
|
|
const userId = (auth as any).userId as string | null;
|
|
|
|
if (!userId) {
|
|
throw new Response("This draft board is not public", { status: 403 });
|
|
}
|
|
|
|
// Check if user is a commissioner
|
|
const isCommissioner = await db.query.commissioners.findFirst({
|
|
where: and(
|
|
eq(schema.commissioners.leagueId, season.leagueId),
|
|
eq(schema.commissioners.userId, userId)
|
|
),
|
|
});
|
|
|
|
// Check if user has a team in this season
|
|
const hasTeam = await db.query.teams.findFirst({
|
|
where: and(
|
|
eq(schema.teams.seasonId, seasonId),
|
|
eq(schema.teams.ownerId, userId)
|
|
),
|
|
});
|
|
|
|
if (!isCommissioner && !hasTeam) {
|
|
throw new Response("You don't have access to this draft board", { status: 403 });
|
|
}
|
|
}
|
|
|
|
// Get draft slots (draft order)
|
|
const draftSlots = await db
|
|
.select({
|
|
id: schema.draftSlots.id,
|
|
draftOrder: schema.draftSlots.draftOrder,
|
|
team: schema.teams,
|
|
})
|
|
.from(schema.draftSlots)
|
|
.innerJoin(schema.teams, eq(schema.draftSlots.teamId, schema.teams.id))
|
|
.where(eq(schema.draftSlots.seasonId, seasonId))
|
|
.orderBy(asc(schema.draftSlots.draftOrder));
|
|
|
|
// Get all draft picks with participant and sport info
|
|
const draftPicks = await db
|
|
.select({
|
|
id: schema.draftPicks.id,
|
|
pickNumber: schema.draftPicks.pickNumber,
|
|
round: schema.draftPicks.round,
|
|
pickInRound: schema.draftPicks.pickInRound,
|
|
team: schema.teams,
|
|
participant: schema.participants,
|
|
sport: schema.sports,
|
|
})
|
|
.from(schema.draftPicks)
|
|
.innerJoin(schema.teams, eq(schema.draftPicks.teamId, schema.teams.id))
|
|
.innerJoin(
|
|
schema.participants,
|
|
eq(schema.draftPicks.participantId, schema.participants.id)
|
|
)
|
|
.innerJoin(
|
|
schema.sportsSeasons,
|
|
eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id)
|
|
)
|
|
.innerJoin(schema.sports, eq(schema.sportsSeasons.sportId, schema.sports.id))
|
|
.where(eq(schema.draftPicks.seasonId, seasonId))
|
|
.orderBy(asc(schema.draftPicks.pickNumber));
|
|
|
|
const ownerMap = await buildOwnerMap(draftSlots);
|
|
|
|
return {
|
|
season,
|
|
draftSlots,
|
|
draftPicks,
|
|
ownerMap,
|
|
};
|
|
}
|
|
|
|
export default function DraftBoard() {
|
|
const { season, draftSlots, draftPicks: initialPicks, ownerMap } = useLoaderData<typeof loader>();
|
|
const { isConnected, on, off } = useDraftSocket(season.id);
|
|
const [picks, setPicks] = useState(initialPicks);
|
|
const [currentPick, setCurrentPick] = useState(season.currentPickNumber || 1);
|
|
|
|
// Listen for new picks (only if draft is still active)
|
|
useEffect(() => {
|
|
if (season.status !== "draft") return;
|
|
|
|
const handlePickMade = (data: any) => {
|
|
setPicks((prev: any) => [...prev, data.pick]);
|
|
setCurrentPick(data.nextPickNumber);
|
|
};
|
|
|
|
on("pick-made", handlePickMade);
|
|
|
|
return () => {
|
|
off("pick-made", handlePickMade);
|
|
};
|
|
}, [on, off, season.status]);
|
|
|
|
// Generate draft grid
|
|
const totalTeams = draftSlots.length;
|
|
const totalRounds = season.draftRounds || 1;
|
|
const draftGrid: any[][] = [];
|
|
|
|
for (let round = 0; round < totalRounds; round++) {
|
|
const roundPicks: any[] = [];
|
|
for (let teamIndex = 0; teamIndex < totalTeams; teamIndex++) {
|
|
const pickNumber = round * totalTeams + teamIndex + 1;
|
|
const pick = picks.find((p: any) => p.pickNumber === pickNumber);
|
|
roundPicks.push(pick || null);
|
|
}
|
|
draftGrid.push(roundPicks);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background">
|
|
{/* Header */}
|
|
<div className="border-b bg-card sticky top-0 z-10">
|
|
<div className="w-full px-4 py-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold">
|
|
{season.league.name} - {season.year} Draft Board
|
|
</h1>
|
|
<div className="flex gap-4 text-sm text-muted-foreground mt-1">
|
|
<span>Round: {totalTeams > 0 ? Math.ceil(currentPick / totalTeams) : 1}</span>
|
|
<span>Pick: {currentPick}</span>
|
|
<span className="capitalize">
|
|
{season.status.replace("_", " ")}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{season.status === "draft" && (
|
|
<div className="flex items-center gap-2">
|
|
<div
|
|
className={`w-3 h-3 rounded-full ${
|
|
isConnected ? "bg-emerald-500" : "bg-coral-accent"
|
|
}`}
|
|
/>
|
|
<span className="text-sm font-medium">
|
|
{isConnected ? "Connected" : "Disconnected"}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Draft Grid */}
|
|
<div className="w-full px-4 py-4">
|
|
<DraftGrid
|
|
draftSlots={draftSlots}
|
|
draftGrid={draftGrid}
|
|
currentPick={currentPick}
|
|
ownerMap={ownerMap}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|