brackt/app/routes/leagues/$leagueId.draft-board.$seasonId.tsx
Chris Parsons ab3437cd73
Display team owner names instead of team names in draft UI (#24)
* 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>
2026-02-22 16:56:07 -08:00

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