feat: add public draft board and simplify draft speed settings

This commit is contained in:
Chris Parsons 2025-10-20 15:03:11 -07:00
parent f86dfade38
commit 6c5c9f709b
13 changed files with 1908 additions and 102 deletions

View file

@ -0,0 +1,157 @@
import { Card } from "~/components/ui/card";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "~/components/ui/context-menu";
interface DraftGridProps {
draftSlots: Array<{
id: string;
draftOrder: number;
team: {
id: string;
name: string;
};
}>;
draftGrid: any[][];
currentPick: number;
teamTimers?: Record<string, number>;
formatTime?: (seconds: number | undefined) => string;
title?: string;
isCommissioner?: boolean;
onForceAutopick?: (pickNumber: number, teamId: string) => void;
onForceManualPick?: (pickNumber: number, teamId: string) => void;
}
export function DraftGrid({
draftSlots,
draftGrid,
currentPick,
teamTimers,
formatTime,
title,
isCommissioner = false,
onForceAutopick,
onForceManualPick,
}: DraftGridProps) {
const totalTeams = draftSlots.length;
return (
<Card className="p-4">
{title && <h2 className="text-xl font-semibold mb-4">{title}</h2>}
<div className="w-full">
{/* Team Headers */}
<div className="flex gap-2 mb-2">
{draftSlots.map((slot) => {
const teamTime = teamTimers?.[slot.team.id];
return (
<div key={slot.id} className="flex-1 min-w-32 text-center">
<div className="font-semibold text-sm truncate px-2">
{slot.team.name}
</div>
{teamTimers && formatTime && (
<div
className={`text-xs font-mono ${
teamTime === undefined
? "text-muted-foreground"
: teamTime > 60
? "text-green-600"
: teamTime > 30
? "text-yellow-600"
: "text-red-600"
}`}
>
{formatTime(teamTime)}
</div>
)}
</div>
);
})}
</div>
{/* Draft Grid Rows */}
<div className="space-y-2">
{draftGrid.map((roundPicks, roundIndex) => {
const round = roundIndex + 1;
const isEvenRound = round % 2 === 0;
const displayPicks = isEvenRound
? [...roundPicks].reverse()
: roundPicks;
return (
<div key={roundIndex} className="flex gap-2">
{displayPicks.map((cell, index) => {
const actualIndex = isEvenRound
? roundPicks.length - 1 - index
: index;
const slot = draftSlots[actualIndex];
const pickNumber = roundIndex * totalTeams + actualIndex + 1;
const isCurrent = pickNumber === currentPick;
const isPicked = !!cell;
const cellContent = (
<div
className={`flex-1 min-w-0 h-20 border-2 rounded-lg p-2 transition-all ${
isCurrent
? "border-blue-500 bg-blue-50 dark:bg-blue-950 shadow-lg"
: isPicked
? "border-green-500 bg-green-50 dark:bg-green-950"
: "border-gray-300 bg-white dark:bg-gray-900"
}`}
title={`Overall Pick #${pickNumber}`}
>
<div className="text-xs font-mono text-muted-foreground mb-1">
{round}.{String(actualIndex + 1).padStart(2, "0")}
</div>
{isPicked ? (
<div className="text-xs">
<div className="font-semibold truncate">
{cell.participant.name}
</div>
<div className="text-muted-foreground truncate">
{cell.sport.name}
</div>
</div>
) : isCurrent ? (
<div className="text-xs font-semibold text-blue-600 dark:text-blue-400">
On Clock
</div>
) : null}
</div>
);
// Wrap with context menu if commissioner and current unpicked cell
if (isCommissioner && !isPicked && isCurrent && onForceAutopick && onForceManualPick) {
return (
<ContextMenu key={pickNumber}>
<ContextMenuTrigger asChild>
{cellContent}
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
onClick={() => onForceAutopick(pickNumber, slot.team.id)}
>
Force Auto Pick
</ContextMenuItem>
<ContextMenuItem
onClick={() => onForceManualPick(pickNumber, slot.team.id)}
>
Force Manual Pick
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
}
return cellContent;
})}
</div>
);
})}
</div>
</div>
</Card>
);
}

View file

@ -10,6 +10,10 @@ export default [
"leagues/:leagueId/draft/:seasonId", "leagues/:leagueId/draft/:seasonId",
"routes/leagues/$leagueId.draft.$seasonId.tsx" "routes/leagues/$leagueId.draft.$seasonId.tsx"
), ),
route(
"leagues/:leagueId/draft-board/:seasonId",
"routes/leagues/$leagueId.draft-board.$seasonId.tsx"
),
route("teams/:teamId/settings", "routes/teams/$teamId.settings.tsx"), route("teams/:teamId/settings", "routes/teams/$teamId.settings.tsx"),
route("api/webhooks/clerk", "routes/api/webhooks/clerk.ts"), route("api/webhooks/clerk", "routes/api/webhooks/clerk.ts"),
route("api/queue/add", "routes/api/queue.add.ts"), route("api/queue/add", "routes/api/queue.add.ts"),

View file

@ -1,4 +1,4 @@
import { getAuth } from "@clerk/react-router/ssr.server"; import { getAuth } from "@clerk/react-router/server";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { database } from "~/database/context"; import { database } from "~/database/context";
import * as schema from "~/database/schema"; import * as schema from "~/database/schema";
@ -50,10 +50,7 @@ export async function action(args: any) {
// Check if draft is active // Check if draft is active
if (season.status !== "draft") { if (season.status !== "draft") {
return Response.json( return Response.json({ error: "Draft is not active" }, { status: 400 });
{ error: "Draft is not active" },
{ status: 400 }
);
} }
// Pause the draft // Pause the draft

View file

@ -1,4 +1,4 @@
import { getAuth } from "@clerk/react-router/ssr.server"; import { getAuth } from "@clerk/react-router/server";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { database } from "~/database/context"; import { database } from "~/database/context";
import * as schema from "~/database/schema"; import * as schema from "~/database/schema";
@ -50,10 +50,7 @@ export async function action(args: any) {
// Check if draft is active // Check if draft is active
if (season.status !== "draft") { if (season.status !== "draft") {
return Response.json( return Response.json({ error: "Draft is not active" }, { status: 400 });
{ error: "Draft is not active" },
{ status: 400 }
);
} }
// Resume the draft // Resume the draft

View file

@ -0,0 +1,186 @@
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";
export async function loader(args: any) {
const { params } = args;
const { seasonId } = params;
const auth = await getAuth(args);
const userId = (auth as any).userId as string | null;
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 });
}
// Check access: allow if public OR user is a league member/commissioner
let hasAccess = season.league.isPublicDraftBoard;
if (!hasAccess && userId) {
// 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)
),
});
hasAccess = !!(isCommissioner || hasTeam);
}
if (!hasAccess) {
throw new Response("This draft board is not public", { 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));
return {
season,
draftSlots,
draftPicks,
};
}
export default function DraftBoard() {
const { season, draftSlots, draftPicks: initialPicks } = 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: {Math.ceil(currentPick / totalTeams)}</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-green-500" : "bg-red-500"
}`}
/>
<span className="text-sm font-medium">
{isConnected ? "Live" : "Disconnected"}
</span>
</div>
)}
</div>
</div>
</div>
{/* Draft Grid */}
<div className="w-full px-4 py-4">
<DraftGrid
draftSlots={draftSlots}
draftGrid={draftGrid}
currentPick={currentPick}
/>
</div>
</div>
);
}

View file

@ -184,11 +184,22 @@ export default function DraftRoom() {
const [isPaused, setIsPaused] = useState(season.draftPaused || false); const [isPaused, setIsPaused] = useState(season.draftPaused || false);
const [isDraftComplete, setIsDraftComplete] = useState(season.status === "active"); const [isDraftComplete, setIsDraftComplete] = useState(season.status === "active");
const [teamTimers, setTeamTimers] = useState<Record<string, number>>(() => { const [teamTimers, setTeamTimers] = useState<Record<string, number>>(() => {
// Initialize with timers from loader data // Initialize with timers from loader data, or use initial time if draft hasn't started
const timersMap: Record<string, number> = {}; const timersMap: Record<string, number> = {};
const initialTime = season.draftInitialTime || 120;
if (timers.length > 0) {
// Draft has started, use actual timers
timers.forEach((timer: any) => { timers.forEach((timer: any) => {
timersMap[timer.teamId] = timer.timeRemaining; timersMap[timer.teamId] = timer.timeRemaining;
}); });
} else {
// Draft hasn't started, show initial time for all teams
draftSlots.forEach((slot) => {
timersMap[slot.team.id] = initialTime;
});
}
return timersMap; return timersMap;
}); });
const [forcePickDialogOpen, setForcePickDialogOpen] = useState(false); const [forcePickDialogOpen, setForcePickDialogOpen] = useState(false);
@ -518,6 +529,17 @@ export default function DraftRoom() {
{isDraftComplete && ( {isDraftComplete && (
<div className="bg-green-500 text-white px-4 py-3 text-center font-semibold"> <div className="bg-green-500 text-white px-4 py-3 text-center font-semibold">
🎉 Draft Complete! The season is now active. 🎉 Draft Complete! The season is now active.
{season.league.isPublicDraftBoard && (
<>
{" "}
<a
href={`/leagues/${season.leagueId}/draft-board/${season.id}`}
className="underline hover:text-green-100"
>
View Draft Board
</a>
</>
)}
</div> </div>
)} )}

View file

@ -212,8 +212,34 @@ export async function action(args: Route.ActionArgs) {
const teamCount = formData.get("teamCount"); const teamCount = formData.get("teamCount");
const draftDateTime = formData.get("draftDateTime"); const draftDateTime = formData.get("draftDateTime");
const draftRounds = formData.get("draftRounds"); const draftRounds = formData.get("draftRounds");
const draftInitialTime = formData.get("draftInitialTime"); const draftSpeed = formData.get("draftSpeed");
const draftIncrementTime = formData.get("draftIncrementTime"); const isPublicDraftBoard = formData.get("isPublicDraftBoard") === "on";
// Map draft speed to time values
let draftInitialTime: number;
let draftIncrementTime: number;
switch (draftSpeed) {
case "fast":
draftInitialTime = 60;
draftIncrementTime = 10;
break;
case "standard":
draftInitialTime = 120;
draftIncrementTime = 15;
break;
case "slow":
draftInitialTime = 28800; // 8 hours
draftIncrementTime = 3600; // 1 hour
break;
case "very-slow":
draftInitialTime = 43200; // 12 hours
draftIncrementTime = 3600; // 1 hour
break;
default:
draftInitialTime = 120;
draftIncrementTime = 15;
}
// Validation // Validation
if (typeof name !== "string" || !name.trim()) { if (typeof name !== "string" || !name.trim()) {
@ -227,6 +253,7 @@ export async function action(args: Route.ActionArgs) {
try { try {
await updateLeague(leagueId, { await updateLeague(leagueId, {
name: name.trim(), name: name.trim(),
isPublicDraftBoard,
}); });
// Update season settings // Update season settings
@ -252,27 +279,9 @@ export async function action(args: Route.ActionArgs) {
seasonUpdates.draftDateTime = draftDateTime ? new Date(draftDateTime) : null; seasonUpdates.draftDateTime = draftDateTime ? new Date(draftDateTime) : null;
} }
// Handle draft initial time // Set draft times from speed preset
if (typeof draftInitialTime === "string") { seasonUpdates.draftInitialTime = draftInitialTime;
const initialTime = parseInt(draftInitialTime, 10); seasonUpdates.draftIncrementTime = draftIncrementTime;
if (!isNaN(initialTime)) {
if (initialTime < 30 || initialTime > 86400) {
return { error: "Initial draft time must be between 30 seconds and 24 hours (86400 seconds)" };
}
seasonUpdates.draftInitialTime = initialTime;
}
}
// Handle draft increment time
if (typeof draftIncrementTime === "string") {
const incrementTime = parseInt(draftIncrementTime, 10);
if (!isNaN(incrementTime)) {
if (incrementTime < 0 || incrementTime > 3600) {
return { error: "Draft increment time must be between 0 and 1 hour (3600 seconds)" };
}
seasonUpdates.draftIncrementTime = incrementTime;
}
}
// Update season if there are changes // Update season if there are changes
if (Object.keys(seasonUpdates).length > 0) { if (Object.keys(seasonUpdates).length > 0) {
@ -438,6 +447,19 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
/> />
</div> </div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="isPublicDraftBoard"
name="isPublicDraftBoard"
defaultChecked={league.isPublicDraftBoard}
className="h-4 w-4 rounded border-gray-300"
/>
<Label htmlFor="isPublicDraftBoard" className="font-normal cursor-pointer">
Make draft board publicly accessible (no login required)
</Label>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="teamCount">Number of Teams</Label> <Label htmlFor="teamCount">Number of Teams</Label>
<Select <Select
@ -578,37 +600,35 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="draftInitialTime">Initial Draft Time (seconds)</Label> <Label htmlFor="draftSpeed">Draft Speed</Label>
<Input <select
id="draftInitialTime" id="draftSpeed"
name="draftInitialTime" name="draftSpeed"
type="number" className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
min="30" defaultValue={
max="86400" season?.draftInitialTime === 60 && season?.draftIncrementTime === 10
defaultValue={season?.draftInitialTime || 120} ? "fast"
: season?.draftInitialTime === 120 && season?.draftIncrementTime === 15
? "standard"
: season?.draftInitialTime === 28800 && season?.draftIncrementTime === 3600
? "slow"
: season?.draftInitialTime === 43200 && season?.draftIncrementTime === 3600
? "very-slow"
: "standard"
}
disabled={!canEditDraftRounds} disabled={!canEditDraftRounds}
required required
/> >
<option value="fast">Fast (1 min + 10 sec)</option>
<option value="standard">Standard (2 min + 15 sec)</option>
<option value="slow">Slow (8 hours + 1 hour)</option>
<option value="very-slow">Very Slow (12 hours + 1 hour)</option>
</select>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Time each team starts with (30 seconds to 24 hours) Initial time + increment per pick
</p>
</div>
<div className="space-y-2">
<Label htmlFor="draftIncrementTime">Time Increment (seconds)</Label>
<Input
id="draftIncrementTime"
name="draftIncrementTime"
type="number"
min="0"
max="3600"
defaultValue={season?.draftIncrementTime || 30}
disabled={!canEditDraftRounds}
required
/>
<p className="text-sm text-muted-foreground">
Time added after each pick (0 to 1 hour)
</p> </p>
<input type="hidden" name="draftInitialTime" id="draftInitialTime" />
<input type="hidden" name="draftIncrementTime" id="draftIncrementTime" />
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View file

@ -364,6 +364,17 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
<p className="text-sm text-muted-foreground">Sports Selected</p> <p className="text-sm text-muted-foreground">Sports Selected</p>
<p className="font-medium">{sportsCount}</p> <p className="font-medium">{sportsCount}</p>
</div> </div>
{(season.status === "active" || season.status === "completed") && (
<div>
<p className="text-sm text-muted-foreground">Draft Board</p>
<Link
to={`/leagues/${league.id}/draft-board/${season.id}`}
className="text-blue-600 hover:underline font-medium"
>
View Draft Board
</Link>
</div>
)}
<div> <div>
<p className="text-sm text-muted-foreground">Flex Spots</p> <p className="text-sm text-muted-foreground">Flex Spots</p>
<p className="font-medium text-primary"> <p className="font-medium text-primary">

View file

@ -76,8 +76,33 @@ export async function action(args: Route.ActionArgs) {
const templateId = formData.get("templateId"); const templateId = formData.get("templateId");
const draftRounds = formData.get("draftRounds"); const draftRounds = formData.get("draftRounds");
const draftDateTime = formData.get("draftDateTime"); const draftDateTime = formData.get("draftDateTime");
const draftInitialTime = formData.get("draftInitialTime"); const draftSpeed = formData.get("draftSpeed");
const draftIncrementTime = formData.get("draftIncrementTime");
// Map draft speed to time values
let draftInitialTimeNum: number;
let draftIncrementTimeNum: number;
switch (draftSpeed) {
case "fast":
draftInitialTimeNum = 60;
draftIncrementTimeNum = 10;
break;
case "standard":
draftInitialTimeNum = 120;
draftIncrementTimeNum = 15;
break;
case "slow":
draftInitialTimeNum = 28800; // 8 hours
draftIncrementTimeNum = 3600; // 1 hour
break;
case "very-slow":
draftInitialTimeNum = 43200; // 12 hours
draftIncrementTimeNum = 3600; // 1 hour
break;
default:
draftInitialTimeNum = 120;
draftIncrementTimeNum = 15;
}
// Validation // Validation
if (typeof name !== "string" || !name.trim()) { if (typeof name !== "string" || !name.trim()) {
@ -98,16 +123,6 @@ export async function action(args: Route.ActionArgs) {
return { error: "Draft rounds must be between 1 and 50" }; return { error: "Draft rounds must be between 1 and 50" };
} }
const draftInitialTimeNum = typeof draftInitialTime === "string" ? parseInt(draftInitialTime, 10) : 120;
if (isNaN(draftInitialTimeNum) || draftInitialTimeNum < 30 || draftInitialTimeNum > 86400) {
return { error: "Initial draft time must be between 30 seconds and 24 hours" };
}
const draftIncrementTimeNum = typeof draftIncrementTime === "string" ? parseInt(draftIncrementTime, 10) : 30;
if (isNaN(draftIncrementTimeNum) || draftIncrementTimeNum < 0 || draftIncrementTimeNum > 3600) {
return { error: "Draft increment time must be between 0 and 1 hour" };
}
try { try {
// Create league // Create league
const league = await createLeague({ const league = await createLeague({
@ -341,34 +356,21 @@ export default function NewLeague({ loaderData, actionData }: Route.ComponentPro
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="draftInitialTime">Initial Draft Time (seconds)</Label> <Label htmlFor="draftSpeed">Draft Speed</Label>
<Input <select
id="draftInitialTime" id="draftSpeed"
name="draftInitialTime" name="draftSpeed"
type="number" className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
min="30" defaultValue="standard"
max="86400"
defaultValue="120"
required required
/> >
<option value="fast">Fast (1 min + 10 sec)</option>
<option value="standard">Standard (2 min + 15 sec)</option>
<option value="slow">Slow (8 hours + 1 hour)</option>
<option value="very-slow">Very Slow (12 hours + 1 hour)</option>
</select>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Time each team starts with (30 seconds to 24 hours). Default: 120 seconds (2 minutes) Initial time + increment per pick
</p>
</div>
<div className="space-y-2">
<Label htmlFor="draftIncrementTime">Time Increment (seconds)</Label>
<Input
id="draftIncrementTime"
name="draftIncrementTime"
type="number"
min="0"
max="3600"
defaultValue="30"
required
/>
<p className="text-sm text-muted-foreground">
Time added after each pick (0 to 1 hour). Default: 30 seconds
</p> </p>
</div> </div>

View file

@ -50,6 +50,7 @@ export const leagues = pgTable("leagues", {
name: varchar("name", { length: 255 }).notNull(), name: varchar("name", { length: 255 }).notNull(),
createdBy: varchar("created_by", { length: 255 }).notNull(), // Clerk user ID createdBy: varchar("created_by", { length: 255 }).notNull(), // Clerk user ID
currentSeasonId: uuid("current_season_id"), // References the active season currentSeasonId: uuid("current_season_id"), // References the active season
isPublicDraftBoard: boolean("is_public_draft_board").notNull().default(false),
createdAt: timestamp("created_at").defaultNow().notNull(), createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(),
}); });

View file

@ -0,0 +1 @@
ALTER TABLE "leagues" ADD COLUMN "is_public_draft_board" boolean DEFAULT false NOT NULL;

File diff suppressed because it is too large Load diff

View file

@ -127,6 +127,13 @@
"when": 1760599350154, "when": 1760599350154,
"tag": "0017_calm_zarda", "tag": "0017_calm_zarda",
"breakpoints": true "breakpoints": true
},
{
"idx": 18,
"version": "7",
"when": 1760996053371,
"tag": "0018_numerous_stardust",
"breakpoints": true
} }
] ]
} }