feat: add public draft board and simplify draft speed settings
This commit is contained in:
parent
f86dfade38
commit
6c5c9f709b
13 changed files with 1908 additions and 102 deletions
157
app/components/DraftGrid.tsx
Normal file
157
app/components/DraftGrid.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -10,6 +10,10 @@ export default [
|
|||
"leagues/:leagueId/draft/:seasonId",
|
||||
"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("api/webhooks/clerk", "routes/api/webhooks/clerk.ts"),
|
||||
route("api/queue/add", "routes/api/queue.add.ts"),
|
||||
|
|
|
|||
|
|
@ -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 { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
|
|
@ -50,10 +50,7 @@ export async function action(args: any) {
|
|||
|
||||
// Check if draft is active
|
||||
if (season.status !== "draft") {
|
||||
return Response.json(
|
||||
{ error: "Draft is not active" },
|
||||
{ status: 400 }
|
||||
);
|
||||
return Response.json({ error: "Draft is not active" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Pause the draft
|
||||
|
|
|
|||
|
|
@ -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 { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
|
|
@ -50,10 +50,7 @@ export async function action(args: any) {
|
|||
|
||||
// Check if draft is active
|
||||
if (season.status !== "draft") {
|
||||
return Response.json(
|
||||
{ error: "Draft is not active" },
|
||||
{ status: 400 }
|
||||
);
|
||||
return Response.json({ error: "Draft is not active" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Resume the draft
|
||||
|
|
|
|||
186
app/routes/leagues/$leagueId.draft-board.$seasonId.tsx
Normal file
186
app/routes/leagues/$leagueId.draft-board.$seasonId.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -184,11 +184,22 @@ export default function DraftRoom() {
|
|||
const [isPaused, setIsPaused] = useState(season.draftPaused || false);
|
||||
const [isDraftComplete, setIsDraftComplete] = useState(season.status === "active");
|
||||
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> = {};
|
||||
timers.forEach((timer: any) => {
|
||||
timersMap[timer.teamId] = timer.timeRemaining;
|
||||
});
|
||||
const initialTime = season.draftInitialTime || 120;
|
||||
|
||||
if (timers.length > 0) {
|
||||
// Draft has started, use actual timers
|
||||
timers.forEach((timer: any) => {
|
||||
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;
|
||||
});
|
||||
const [forcePickDialogOpen, setForcePickDialogOpen] = useState(false);
|
||||
|
|
@ -518,6 +529,17 @@ export default function DraftRoom() {
|
|||
{isDraftComplete && (
|
||||
<div className="bg-green-500 text-white px-4 py-3 text-center font-semibold">
|
||||
🎉 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>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -212,8 +212,34 @@ export async function action(args: Route.ActionArgs) {
|
|||
const teamCount = formData.get("teamCount");
|
||||
const draftDateTime = formData.get("draftDateTime");
|
||||
const draftRounds = formData.get("draftRounds");
|
||||
const draftInitialTime = formData.get("draftInitialTime");
|
||||
const draftIncrementTime = formData.get("draftIncrementTime");
|
||||
const draftSpeed = formData.get("draftSpeed");
|
||||
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
|
||||
if (typeof name !== "string" || !name.trim()) {
|
||||
|
|
@ -227,6 +253,7 @@ export async function action(args: Route.ActionArgs) {
|
|||
try {
|
||||
await updateLeague(leagueId, {
|
||||
name: name.trim(),
|
||||
isPublicDraftBoard,
|
||||
});
|
||||
|
||||
// Update season settings
|
||||
|
|
@ -252,27 +279,9 @@ export async function action(args: Route.ActionArgs) {
|
|||
seasonUpdates.draftDateTime = draftDateTime ? new Date(draftDateTime) : null;
|
||||
}
|
||||
|
||||
// Handle draft initial time
|
||||
if (typeof draftInitialTime === "string") {
|
||||
const initialTime = parseInt(draftInitialTime, 10);
|
||||
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;
|
||||
}
|
||||
}
|
||||
// Set draft times from speed preset
|
||||
seasonUpdates.draftInitialTime = draftInitialTime;
|
||||
seasonUpdates.draftIncrementTime = draftIncrementTime;
|
||||
|
||||
// Update season if there are changes
|
||||
if (Object.keys(seasonUpdates).length > 0) {
|
||||
|
|
@ -438,6 +447,19 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
|
|||
/>
|
||||
</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">
|
||||
<Label htmlFor="teamCount">Number of Teams</Label>
|
||||
<Select
|
||||
|
|
@ -578,37 +600,35 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
|
|||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="draftInitialTime">Initial Draft Time (seconds)</Label>
|
||||
<Input
|
||||
id="draftInitialTime"
|
||||
name="draftInitialTime"
|
||||
type="number"
|
||||
min="30"
|
||||
max="86400"
|
||||
defaultValue={season?.draftInitialTime || 120}
|
||||
<Label htmlFor="draftSpeed">Draft Speed</Label>
|
||||
<select
|
||||
id="draftSpeed"
|
||||
name="draftSpeed"
|
||||
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"
|
||||
defaultValue={
|
||||
season?.draftInitialTime === 60 && season?.draftIncrementTime === 10
|
||||
? "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}
|
||||
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">
|
||||
Time each team starts with (30 seconds to 24 hours)
|
||||
</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)
|
||||
Initial time + increment per pick
|
||||
</p>
|
||||
<input type="hidden" name="draftInitialTime" id="draftInitialTime" />
|
||||
<input type="hidden" name="draftIncrementTime" id="draftIncrementTime" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -364,6 +364,17 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
|||
<p className="text-sm text-muted-foreground">Sports Selected</p>
|
||||
<p className="font-medium">{sportsCount}</p>
|
||||
</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>
|
||||
<p className="text-sm text-muted-foreground">Flex Spots</p>
|
||||
<p className="font-medium text-primary">
|
||||
|
|
|
|||
|
|
@ -76,8 +76,33 @@ export async function action(args: Route.ActionArgs) {
|
|||
const templateId = formData.get("templateId");
|
||||
const draftRounds = formData.get("draftRounds");
|
||||
const draftDateTime = formData.get("draftDateTime");
|
||||
const draftInitialTime = formData.get("draftInitialTime");
|
||||
const draftIncrementTime = formData.get("draftIncrementTime");
|
||||
const draftSpeed = formData.get("draftSpeed");
|
||||
|
||||
// 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
|
||||
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" };
|
||||
}
|
||||
|
||||
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 {
|
||||
// Create league
|
||||
const league = await createLeague({
|
||||
|
|
@ -341,34 +356,21 @@ export default function NewLeague({ loaderData, actionData }: Route.ComponentPro
|
|||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="draftInitialTime">Initial Draft Time (seconds)</Label>
|
||||
<Input
|
||||
id="draftInitialTime"
|
||||
name="draftInitialTime"
|
||||
type="number"
|
||||
min="30"
|
||||
max="86400"
|
||||
defaultValue="120"
|
||||
<Label htmlFor="draftSpeed">Draft Speed</Label>
|
||||
<select
|
||||
id="draftSpeed"
|
||||
name="draftSpeed"
|
||||
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"
|
||||
defaultValue="standard"
|
||||
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">
|
||||
Time each team starts with (30 seconds to 24 hours). Default: 120 seconds (2 minutes)
|
||||
</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
|
||||
Initial time + increment per pick
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ export const leagues = pgTable("leagues", {
|
|||
name: varchar("name", { length: 255 }).notNull(),
|
||||
createdBy: varchar("created_by", { length: 255 }).notNull(), // Clerk user ID
|
||||
currentSeasonId: uuid("current_season_id"), // References the active season
|
||||
isPublicDraftBoard: boolean("is_public_draft_board").notNull().default(false),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
});
|
||||
|
|
|
|||
1
drizzle/0018_numerous_stardust.sql
Normal file
1
drizzle/0018_numerous_stardust.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "leagues" ADD COLUMN "is_public_draft_board" boolean DEFAULT false NOT NULL;
|
||||
1401
drizzle/meta/0018_snapshot.json
Normal file
1401
drizzle/meta/0018_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -127,6 +127,13 @@
|
|||
"when": 1760599350154,
|
||||
"tag": "0017_calm_zarda",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 18,
|
||||
"version": "7",
|
||||
"when": 1760996053371,
|
||||
"tag": "0018_numerous_stardust",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue