feat: add draft rounds configuration to league settings
This commit is contained in:
parent
0e68bd2037
commit
501c5b183f
7 changed files with 1356 additions and 130 deletions
|
|
@ -3,7 +3,7 @@ import { Form, Link, redirect, useNavigation } from "react-router";
|
||||||
import { getAuth } from "@clerk/react-router/server";
|
import { getAuth } from "@clerk/react-router/server";
|
||||||
import { findLeagueById, updateLeague, deleteLeague } from "~/models/league";
|
import { findLeagueById, updateLeague, deleteLeague } from "~/models/league";
|
||||||
import { isCommissioner } from "~/models/commissioner";
|
import { isCommissioner } from "~/models/commissioner";
|
||||||
import { findCurrentSeasonWithSports } from "~/models/season";
|
import { findCurrentSeasonWithSports, updateSeason } from "~/models/season";
|
||||||
import { findTeamsBySeasonId, createManyTeams, deleteTeam } from "~/models/team";
|
import { findTeamsBySeasonId, createManyTeams, deleteTeam } from "~/models/team";
|
||||||
import { unlinkSportFromSeason, linkMultipleSportsToSeason } from "~/models/season-sport";
|
import { unlinkSportFromSeason, linkMultipleSportsToSeason } from "~/models/season-sport";
|
||||||
import { findAllSportsSeasons } from "~/models/sports-season";
|
import { findAllSportsSeasons } from "~/models/sports-season";
|
||||||
|
|
@ -117,6 +117,31 @@ export async function action(args: Route.ActionArgs) {
|
||||||
return { error: "No active season found" };
|
return { error: "No active season found" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (intent === "update-draft-rounds") {
|
||||||
|
if (season.status !== "pre_draft") {
|
||||||
|
return { error: "Cannot modify draft rounds after draft has started" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const draftRounds = formData.get("draftRounds");
|
||||||
|
const draftRoundsNum = typeof draftRounds === "string" ? parseInt(draftRounds, 10) : null;
|
||||||
|
|
||||||
|
if (draftRoundsNum === null || isNaN(draftRoundsNum)) {
|
||||||
|
return { error: "Draft rounds must be a valid number" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const sportsCount = season.seasonSports?.length || 0;
|
||||||
|
if (draftRoundsNum < sportsCount) {
|
||||||
|
return { error: `Draft rounds must be at least ${sportsCount} (number of sports selected)` };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draftRoundsNum < 1 || draftRoundsNum > 50) {
|
||||||
|
return { error: "Draft rounds must be between 1 and 50" };
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateSeason(season.id, { draftRounds: draftRoundsNum });
|
||||||
|
return { success: true, message: "Draft rounds updated successfully" };
|
||||||
|
}
|
||||||
|
|
||||||
if (intent === "update-sports") {
|
if (intent === "update-sports") {
|
||||||
if (season.status !== "pre_draft") {
|
if (season.status !== "pre_draft") {
|
||||||
return { error: "Cannot modify sports after draft has started" };
|
return { error: "Cannot modify sports after draft has started" };
|
||||||
|
|
@ -281,6 +306,7 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
|
||||||
? draftSlots.map((slot: any) => slot.teamId)
|
? draftSlots.map((slot: any) => slot.teamId)
|
||||||
: teams.map((team: any) => team.id)
|
: teams.map((team: any) => team.id)
|
||||||
);
|
);
|
||||||
|
const [draftRounds, setDraftRounds] = useState<number>(season?.draftRounds || 20);
|
||||||
|
|
||||||
// Update draft order when loader data changes (after randomization)
|
// Update draft order when loader data changes (after randomization)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -293,8 +319,15 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
|
||||||
const canEditSports = season && season.status === "pre_draft";
|
const canEditSports = season && season.status === "pre_draft";
|
||||||
const canEditTeamCount = season && season.status === "pre_draft";
|
const canEditTeamCount = season && season.status === "pre_draft";
|
||||||
const canEditDraftOrder = season && season.status === "pre_draft";
|
const canEditDraftOrder = season && season.status === "pre_draft";
|
||||||
|
const canEditDraftRounds = season && season.status === "pre_draft";
|
||||||
const minTeamCount = Math.max(6, teamsWithOwners);
|
const minTeamCount = Math.max(6, teamsWithOwners);
|
||||||
|
|
||||||
|
// Calculate flex spots and minimum rounds
|
||||||
|
const sportsCount = selectedSports.size;
|
||||||
|
const minRounds = sportsCount;
|
||||||
|
const recommendedRounds = Math.ceil(sportsCount * 1.25);
|
||||||
|
const flexSpots = Math.max(0, draftRounds - sportsCount);
|
||||||
|
|
||||||
const handleSportToggle = (sportId: string) => {
|
const handleSportToggle = (sportId: string) => {
|
||||||
const newSelected = new Set(selectedSports);
|
const newSelected = new Set(selectedSports);
|
||||||
if (newSelected.has(sportId)) {
|
if (newSelected.has(sportId)) {
|
||||||
|
|
@ -333,8 +366,10 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
|
||||||
<p className="text-muted-foreground">{league.name}</p>
|
<p className="text-muted-foreground">{league.name}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<Form method="post" className="space-y-6">
|
||||||
{/* Update League Form */}
|
<input type="hidden" name="intent" value="update" />
|
||||||
|
|
||||||
|
{/* General Settings */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>General Settings</CardTitle>
|
<CardTitle>General Settings</CardTitle>
|
||||||
|
|
@ -342,68 +377,111 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
|
||||||
Update your league's basic information
|
Update your league's basic information
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="space-y-4">
|
||||||
<Form method="post" className="space-y-4">
|
<div className="space-y-2">
|
||||||
<input type="hidden" name="intent" value="update" />
|
<Label htmlFor="name">League Name</Label>
|
||||||
|
<Input
|
||||||
<div className="space-y-2">
|
id="name"
|
||||||
<Label htmlFor="name">League Name</Label>
|
name="name"
|
||||||
<Input
|
type="text"
|
||||||
id="name"
|
defaultValue={league.name}
|
||||||
name="name"
|
placeholder="Enter league name"
|
||||||
type="text"
|
required
|
||||||
defaultValue={league.name}
|
minLength={3}
|
||||||
placeholder="Enter league name"
|
maxLength={50}
|
||||||
required
|
/>
|
||||||
minLength={3}
|
</div>
|
||||||
maxLength={50}
|
|
||||||
/>
|
|
||||||
</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
|
||||||
name="teamCount"
|
name="teamCount"
|
||||||
defaultValue={teamCount.toString()}
|
defaultValue={teamCount.toString()}
|
||||||
disabled={!canEditTeamCount}
|
disabled={!canEditTeamCount}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="teamCount">
|
<SelectTrigger id="teamCount">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{Array.from({ length: 11 }, (_, i) => i + 6).map((num) => (
|
{Array.from({ length: 11 }, (_, i) => i + 6).map((num) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={num}
|
key={num}
|
||||||
value={num.toString()}
|
value={num.toString()}
|
||||||
disabled={num < minTeamCount}
|
disabled={num < minTeamCount}
|
||||||
>
|
>
|
||||||
{num} Teams
|
{num} Teams
|
||||||
{num < minTeamCount && ` (${teamsWithOwners} teams have owners)`}
|
{num < minTeamCount && ` (${teamsWithOwners} teams have owners)`}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{canEditTeamCount
|
{canEditTeamCount
|
||||||
? `Can be changed before draft starts (minimum ${minTeamCount} based on registered teams)`
|
? `Can be changed before draft starts (minimum ${minTeamCount} based on registered teams)`
|
||||||
: "Team count cannot be changed after draft has started"}
|
: "Team count cannot be changed after draft has started"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{actionData?.error && (
|
|
||||||
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
|
|
||||||
{actionData.error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button type="submit" className="w-full">
|
|
||||||
Save Changes
|
|
||||||
</Button>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Sports Seasons Management */}
|
{/* Draft Rounds */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Draft Rounds</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{canEditDraftRounds
|
||||||
|
? "Set the number of draft rounds for your league"
|
||||||
|
: "Draft rounds cannot be modified after the draft has started"}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="draftRounds">Number of Draft Rounds</Label>
|
||||||
|
<Select
|
||||||
|
name="draftRounds"
|
||||||
|
value={draftRounds.toString()}
|
||||||
|
onValueChange={(value) => setDraftRounds(parseInt(value))}
|
||||||
|
disabled={!canEditDraftRounds}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<SelectTrigger id="draftRounds">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Array.from({ length: 50 }, (_, i) => i + 1)
|
||||||
|
.filter(num => num >= minRounds)
|
||||||
|
.map((num) => (
|
||||||
|
<SelectItem
|
||||||
|
key={num}
|
||||||
|
value={num.toString()}
|
||||||
|
>
|
||||||
|
{num} Round{num !== 1 ? 's' : ''}
|
||||||
|
{num === recommendedRounds && ' (recommended)'}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Minimum: {minRounds} (number of sports selected)
|
||||||
|
{recommendedRounds > minRounds && ` • Recommended: ${recommendedRounds}`}
|
||||||
|
</p>
|
||||||
|
{sportsCount > 0 && draftRounds < sportsCount && (
|
||||||
|
<p className="text-sm font-medium text-destructive">
|
||||||
|
⚠️ You need at least {sportsCount} rounds for the {sportsCount} sports selected
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{sportsCount > 0 && draftRounds >= sportsCount && (
|
||||||
|
<p className="text-sm font-medium text-primary">
|
||||||
|
Flex Spots: {flexSpots}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Sports Seasons */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Sports Seasons</CardTitle>
|
<CardTitle>Sports Seasons</CardTitle>
|
||||||
|
|
@ -413,74 +491,72 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
|
||||||
: "Sports cannot be modified after the draft has started"}
|
: "Sports cannot be modified after the draft has started"}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="space-y-4">
|
||||||
<Form method="post" className="space-y-4">
|
|
||||||
<input type="hidden" name="intent" value="update-sports" />
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="bg-muted/50 border border-muted-foreground/20 rounded-md p-3">
|
<div className="bg-muted/50 border border-muted-foreground/20 rounded-md p-3">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
<strong>Recommendation:</strong> Select 16-20 sports seasons for optimal league balance and variety.
|
<strong>Recommendation:</strong> Select 16-20 sports seasons for optimal league balance and variety.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-sm font-medium">
|
<Label className="text-sm font-medium">
|
||||||
Sports Seasons ({selectedSports.size} selected)
|
Sports Seasons ({selectedSports.size} selected)
|
||||||
</Label>
|
</Label>
|
||||||
<div className="max-h-96 overflow-y-auto border rounded-md p-3 space-y-2">
|
<div className="max-h-96 overflow-y-auto border rounded-md p-3 space-y-2">
|
||||||
{allSportsSeasons.length > 0 ? (
|
{allSportsSeasons.length > 0 ? (
|
||||||
allSportsSeasons.map((season) => (
|
allSportsSeasons.map((season) => (
|
||||||
<div key={season.id} className="flex items-center space-x-2">
|
<div key={season.id} className="flex items-center space-x-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id={season.id}
|
id={season.id}
|
||||||
name="sportsSeasons"
|
name="sportsSeasons"
|
||||||
value={season.id}
|
value={season.id}
|
||||||
checked={selectedSports.has(season.id)}
|
checked={selectedSports.has(season.id)}
|
||||||
onCheckedChange={() => handleSportToggle(season.id)}
|
onCheckedChange={() => handleSportToggle(season.id)}
|
||||||
disabled={!canEditSports}
|
disabled={!canEditSports}
|
||||||
/>
|
/>
|
||||||
<Label
|
<Label
|
||||||
htmlFor={season.id}
|
htmlFor={season.id}
|
||||||
className={`font-normal flex-1 ${canEditSports ? 'cursor-pointer' : 'cursor-not-allowed opacity-60'}`}
|
className={`font-normal flex-1 ${canEditSports ? 'cursor-pointer' : 'cursor-not-allowed opacity-60'}`}
|
||||||
>
|
>
|
||||||
<span className="font-medium">{season.sport.name}</span> - {season.name} ({season.year})
|
<span className="font-medium">{season.sport.name}</span> - {season.name} ({season.year})
|
||||||
<Badge variant="outline" className="ml-2 text-xs">
|
<Badge variant="outline" className="ml-2 text-xs">
|
||||||
{season.scoringType.replace("_", " ")}
|
{season.scoringType.replace("_", " ")}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
No sports seasons available
|
No sports seasons available
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{canEditSports && (
|
|
||||||
<Button type="submit" className="w-full">
|
|
||||||
Save Sports Selection
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{actionData?.success && (
|
|
||||||
<div className="bg-green-500/15 text-green-700 dark:text-green-400 px-4 py-3 rounded-md text-sm">
|
|
||||||
Sports updated successfully!
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{actionData?.error && (
|
|
||||||
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
|
|
||||||
{actionData.error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
{actionData?.error && (
|
||||||
|
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
|
||||||
|
{actionData.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{actionData?.success && (
|
||||||
|
<div className="bg-green-500/15 text-green-700 dark:text-green-400 px-4 py-3 rounded-md text-sm">
|
||||||
|
Settings updated successfully!
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button type="submit" size="lg" className="w-full">
|
||||||
|
Save All Settings
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
{/* Draft Order Management */}
|
{/* Draft Order Management */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
findUserByClerkId,
|
findUserByClerkId,
|
||||||
findDraftSlotsBySeasonId,
|
findDraftSlotsBySeasonId,
|
||||||
} from "~/models";
|
} from "~/models";
|
||||||
|
import { findSeasonWithSportsSeasons } from "~/models/season";
|
||||||
import type { Route } from "./+types/$leagueId";
|
import type { Route } from "./+types/$leagueId";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|
@ -34,8 +35,9 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
throw new Response("League not found", { status: 404 });
|
throw new Response("League not found", { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch current season
|
// Fetch current season with sports
|
||||||
const season = await findCurrentSeason(leagueId);
|
const season = await findCurrentSeason(leagueId);
|
||||||
|
const seasonWithSports = season ? await findSeasonWithSportsSeasons(season.id) : null;
|
||||||
|
|
||||||
// Fetch commissioners
|
// Fetch commissioners
|
||||||
const commissioners = await findCommissionersByLeagueId(leagueId);
|
const commissioners = await findCommissionersByLeagueId(leagueId);
|
||||||
|
|
@ -110,6 +112,9 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
// Count available teams
|
// Count available teams
|
||||||
const availableTeamCount = teams.filter((t) => !t.ownerId).length;
|
const availableTeamCount = teams.filter((t) => !t.ownerId).length;
|
||||||
|
|
||||||
|
// Count sports in season
|
||||||
|
const sportsCount = seasonWithSports?.seasonSports?.length || 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
league,
|
league,
|
||||||
season,
|
season,
|
||||||
|
|
@ -121,6 +126,7 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
commissionerMap: Object.fromEntries(commissionerMap),
|
commissionerMap: Object.fromEntries(commissionerMap),
|
||||||
availableTeamCount,
|
availableTeamCount,
|
||||||
draftSlots,
|
draftSlots,
|
||||||
|
sportsCount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,6 +142,7 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
commissionerMap,
|
commissionerMap,
|
||||||
availableTeamCount,
|
availableTeamCount,
|
||||||
draftSlots,
|
draftSlots,
|
||||||
|
sportsCount,
|
||||||
} = loaderData;
|
} = loaderData;
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
@ -332,12 +339,28 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{season && (
|
{season && (
|
||||||
<div>
|
<>
|
||||||
<p className="text-sm text-muted-foreground">Season Status</p>
|
<div>
|
||||||
<p className="font-medium capitalize">
|
<p className="text-sm text-muted-foreground">Season Status</p>
|
||||||
{season.status.replace("_", " ")}
|
<p className="font-medium capitalize">
|
||||||
</p>
|
{season.status.replace("_", " ")}
|
||||||
</div>
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Draft Rounds</p>
|
||||||
|
<p className="font-medium">{season.draftRounds}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Sports Selected</p>
|
||||||
|
<p className="font-medium">{sportsCount}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Flex Spots</p>
|
||||||
|
<p className="font-medium text-primary">
|
||||||
|
{Math.max(0, season.draftRounds - sportsCount)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ export async function action(args: Route.ActionArgs) {
|
||||||
const name = formData.get("name");
|
const name = formData.get("name");
|
||||||
const teamCount = formData.get("teamCount");
|
const teamCount = formData.get("teamCount");
|
||||||
const templateId = formData.get("templateId");
|
const templateId = formData.get("templateId");
|
||||||
|
const draftRounds = formData.get("draftRounds");
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
if (typeof name !== "string" || !name.trim()) {
|
if (typeof name !== "string" || !name.trim()) {
|
||||||
|
|
@ -81,6 +82,11 @@ export async function action(args: Route.ActionArgs) {
|
||||||
return { error: "Number of teams must be between 6 and 16" };
|
return { error: "Number of teams must be between 6 and 16" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const draftRoundsNum = typeof draftRounds === "string" ? parseInt(draftRounds, 10) : 20;
|
||||||
|
if (isNaN(draftRoundsNum) || draftRoundsNum < 1 || draftRoundsNum > 50) {
|
||||||
|
return { error: "Draft rounds must be between 1 and 50" };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create league
|
// Create league
|
||||||
const league = await createLeague({
|
const league = await createLeague({
|
||||||
|
|
@ -101,6 +107,7 @@ export async function action(args: Route.ActionArgs) {
|
||||||
year: currentYear,
|
year: currentYear,
|
||||||
status: "pre_draft",
|
status: "pre_draft",
|
||||||
templateId: typeof templateId === "string" && templateId !== "" ? templateId : null,
|
templateId: typeof templateId === "string" && templateId !== "" ? templateId : null,
|
||||||
|
draftRounds: draftRoundsNum,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set this as the current season for the league
|
// Set this as the current season for the league
|
||||||
|
|
@ -108,6 +115,14 @@ export async function action(args: Route.ActionArgs) {
|
||||||
|
|
||||||
// Add selected sports to the season
|
// Add selected sports to the season
|
||||||
const selectedSports = formData.getAll("sportsSeasons");
|
const selectedSports = formData.getAll("sportsSeasons");
|
||||||
|
|
||||||
|
// Validate that draft rounds is at least the number of sports
|
||||||
|
if (selectedSports.length > draftRoundsNum) {
|
||||||
|
return {
|
||||||
|
error: `You need at least ${selectedSports.length} draft rounds for the ${selectedSports.length} sports selected. Currently set to ${draftRoundsNum} rounds.`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedSports.length > 0) {
|
if (selectedSports.length > 0) {
|
||||||
await linkMultipleSportsToSeason(
|
await linkMultipleSportsToSeason(
|
||||||
selectedSports.map((id) => ({
|
selectedSports.map((id) => ({
|
||||||
|
|
@ -139,6 +154,7 @@ export default function NewLeague({ loaderData, actionData }: Route.ComponentPro
|
||||||
const { templates, allSportsSeasons } = loaderData;
|
const { templates, allSportsSeasons } = loaderData;
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<string>("");
|
const [selectedTemplate, setSelectedTemplate] = useState<string>("");
|
||||||
const [selectedSports, setSelectedSports] = useState<Set<string>>(new Set());
|
const [selectedSports, setSelectedSports] = useState<Set<string>>(new Set());
|
||||||
|
const [draftRounds, setDraftRounds] = useState<number>(20);
|
||||||
|
|
||||||
const handleSportToggle = (sportId: string) => {
|
const handleSportToggle = (sportId: string) => {
|
||||||
const newSelected = new Set(selectedSports);
|
const newSelected = new Set(selectedSports);
|
||||||
|
|
@ -154,6 +170,13 @@ export default function NewLeague({ loaderData, actionData }: Route.ComponentPro
|
||||||
setSelectedTemplate(templateId);
|
setSelectedTemplate(templateId);
|
||||||
setSelectedSports(new Set(sportsSeasonIds));
|
setSelectedSports(new Set(sportsSeasonIds));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Calculate flex spots and minimum rounds
|
||||||
|
const sportsCount = selectedSports.size;
|
||||||
|
const minRounds = sportsCount;
|
||||||
|
const recommendedRounds = Math.ceil(sportsCount * 1.25);
|
||||||
|
const flexSpots = Math.max(0, draftRounds - sportsCount);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container max-w-2xl mx-auto py-8 px-4">
|
<div className="container max-w-2xl mx-auto py-8 px-4">
|
||||||
<Card>
|
<Card>
|
||||||
|
|
@ -203,6 +226,49 @@ export default function NewLeague({ loaderData, actionData }: Route.ComponentPro
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="draftRounds">Draft Rounds</Label>
|
||||||
|
<Select
|
||||||
|
name="draftRounds"
|
||||||
|
value={draftRounds.toString()}
|
||||||
|
onValueChange={(value) => setDraftRounds(parseInt(value))}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<SelectTrigger id="draftRounds">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Array.from({ length: 50 }, (_, i) => i + 1)
|
||||||
|
.filter(num => num >= minRounds)
|
||||||
|
.map((num) => (
|
||||||
|
<SelectItem
|
||||||
|
key={num}
|
||||||
|
value={num.toString()}
|
||||||
|
>
|
||||||
|
{num} Round{num !== 1 ? 's' : ''}
|
||||||
|
{num === recommendedRounds && ' (recommended)'}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Minimum: {minRounds} (number of sports selected)
|
||||||
|
{recommendedRounds > minRounds && ` • Recommended: ${recommendedRounds}`}
|
||||||
|
</p>
|
||||||
|
{sportsCount > 0 && draftRounds < sportsCount && (
|
||||||
|
<p className="text-sm font-medium text-destructive">
|
||||||
|
⚠️ You need at least {sportsCount} rounds for the {sportsCount} sports selected
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{sportsCount > 0 && draftRounds >= sportsCount && (
|
||||||
|
<p className="text-sm font-medium text-primary">
|
||||||
|
Flex Spots: {flexSpots}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Label>Sports Selection</Label>
|
<Label>Sports Selection</Label>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ export const seasons = pgTable("seasons", {
|
||||||
status: seasonStatusEnum("status").notNull().default("pre_draft"),
|
status: seasonStatusEnum("status").notNull().default("pre_draft"),
|
||||||
templateId: uuid("template_id")
|
templateId: uuid("template_id")
|
||||||
.references(() => seasonTemplates.id, { onDelete: "set null" }),
|
.references(() => seasonTemplates.id, { onDelete: "set null" }),
|
||||||
|
draftRounds: integer("draft_rounds").notNull().default(20),
|
||||||
flexSpots: integer("flex_spots").notNull().default(0),
|
flexSpots: integer("flex_spots").notNull().default(0),
|
||||||
inviteCode: varchar("invite_code", { length: 20 }).notNull().unique(),
|
inviteCode: varchar("invite_code", { length: 20 }).notNull().unique(),
|
||||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
|
|
|
||||||
1
drizzle/0015_exotic_loners.sql
Normal file
1
drizzle/0015_exotic_loners.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE "seasons" ADD COLUMN "draft_rounds" integer DEFAULT 20 NOT NULL;
|
||||||
1052
drizzle/meta/0015_snapshot.json
Normal file
1052
drizzle/meta/0015_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -106,6 +106,13 @@
|
||||||
"when": 1760588333462,
|
"when": 1760588333462,
|
||||||
"tag": "0014_lowly_gateway",
|
"tag": "0014_lowly_gateway",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 15,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1760589195551,
|
||||||
|
"tag": "0015_exotic_loners",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Loading…
Add table
Reference in a new issue