import { useState } from "react"; import { Form, redirect, Link } from "react-router"; import { getAuth } from "@clerk/react-router/server"; import type { Route } from "./+types/new"; import { createLeague, setCurrentSeason } from "~/models/league"; import { createCommissioner } from "~/models/commissioner"; import { createSeason } from "~/models/season"; import { linkMultipleSportsToSeason } from "~/models/season-sport"; import { createManyTeams } from "~/models/team"; import { findActiveSeasonTemplates, findSeasonTemplateWithSportsSeasons } from "~/models/season-template"; import { findAllSportsSeasons } from "~/models/sports-season"; import { generateUniqueTeamNames } from "~/utils/team-names"; import { format } from "date-fns"; import { CalendarIcon, Check } from "lucide-react"; import { Button } from "~/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "~/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "~/components/ui/select"; import { Badge } from "~/components/ui/badge"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { Checkbox } from "~/components/ui/checkbox"; import { Calendar } from "~/components/ui/calendar"; import { Popover, PopoverContent, PopoverTrigger, } from "~/components/ui/popover"; import { cn } from "~/lib/utils"; import { ScoringRulesEditor } from "~/components/scoring/ScoringRulesEditor"; export async function loader() { const templates = await findActiveSeasonTemplates(); const allSportsSeasons = await findAllSportsSeasons(); // Fetch templates with their sports seasons for mapping const templatesWithSports = await Promise.all( templates.map(async (template) => { const templateWithSports = await findSeasonTemplateWithSportsSeasons(template.id); return { ...template, sportsSeasonIds: templateWithSports?.seasonTemplateSports?.map( (ts: any) => ts.sportsSeason.id ) || [] }; }) ); return { templates: templatesWithSports, allSportsSeasons: allSportsSeasons as Array }; } export async function action(args: Route.ActionArgs) { const { userId } = await getAuth(args); const { request } = args; if (!userId) { throw new Response("Unauthorized", { status: 401 }); } const formData = await request.formData(); const name = formData.get("name"); const teamCount = formData.get("teamCount"); const templateId = formData.get("templateId"); const draftRounds = formData.get("draftRounds"); const draftDateTime = formData.get("draftDateTime"); 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()) { return { error: "League name is required" }; } if (typeof teamCount !== "string") { return { error: "Number of teams is required" }; } const teamCountNum = parseInt(teamCount, 10); if (isNaN(teamCountNum) || teamCountNum < 6 || teamCountNum > 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" }; } // Extract and validate scoring rules const scoringFields = [ "pointsFor1st", "pointsFor2nd", "pointsFor3rd", "pointsFor4th", "pointsFor5th", "pointsFor6th", "pointsFor7th", "pointsFor8th" ]; const scoringRules: Record = {}; for (const field of scoringFields) { const value = formData.get(field); if (typeof value === "string") { const points = parseInt(value, 10); if (!isNaN(points)) { if (points < 0 || points > 1000) { return { error: `${field} must be between 0 and 1000 points` }; } scoringRules[field] = points; } } } try { // Create league const league = await createLeague({ name: name.trim(), createdBy: userId, }); // Make creator a commissioner await createCommissioner({ leagueId: league.id, userId: userId, }); // Create first season const currentYear = new Date().getFullYear(); const season = await createSeason({ leagueId: league.id, year: currentYear, status: "pre_draft", templateId: typeof templateId === "string" && templateId !== "" ? templateId : null, draftRounds: draftRoundsNum, draftDateTime: typeof draftDateTime === "string" && draftDateTime ? new Date(draftDateTime) : null, draftInitialTime: draftInitialTimeNum, draftIncrementTime: draftIncrementTimeNum, ...scoringRules, }); // Set this as the current season for the league await setCurrentSeason(league.id, season.id); // Add selected sports to the season 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) { await linkMultipleSportsToSeason( selectedSports.map((id) => ({ seasonId: season.id, sportsSeasonId: id as string, })) ); } // Create teams with fun random names const joinAsPlayer = formData.get("joinAsPlayer") === "on"; const teamNames = generateUniqueTeamNames(teamCountNum); const teams = teamNames.map((name, i) => ({ seasonId: season.id, name, ownerId: joinAsPlayer && i === 0 ? userId : null, })); await createManyTeams(teams); // Redirect to league homepage return redirect(`/leagues/${league.id}`); } catch (error) { console.error("Error creating league:", error); return { error: "Failed to create league. Please try again." }; } } export default function NewLeague({ loaderData, actionData }: Route.ComponentProps) { const { templates, allSportsSeasons } = loaderData; const [selectedTemplate, setSelectedTemplate] = useState(""); const [selectedSports, setSelectedSports] = useState>(new Set()); const [draftRounds, setDraftRounds] = useState(20); const [draftDate, setDraftDate] = useState(); const [draftTime, setDraftTime] = useState(""); const handleSportToggle = (sportId: string) => { const newSelected = new Set(selectedSports); if (newSelected.has(sportId)) { newSelected.delete(sportId); } else { newSelected.add(sportId); } setSelectedSports(newSelected); }; const handleTemplateClick = (templateId: string, sportsSeasonIds: string[]) => { setSelectedTemplate(templateId); 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 (
Start a New League Create your fantasy league and invite your friends to compete.

Choose between 6 and 16 teams

Minimum: {minRounds} (number of sports selected) {recommendedRounds > minRounds && ` • Recommended: ${recommendedRounds}`}

{sportsCount > 0 && draftRounds < sportsCount && (

⚠️ You need at least {sportsCount} rounds for the {sportsCount} sports selected

)} {sportsCount > 0 && draftRounds >= sportsCount && (

Flex Spots: {flexSpots}

)}
date < new Date(new Date().setHours(0, 0, 0, 0))} /> setDraftTime(e.target.value)} placeholder="Select time" />
{draftDate && draftTime && ( )}

You must set a draft date and time before starting the draft

Initial time + increment per pick

Choose a template to auto-select sports, or manually select your own

{/* Template Cards */} {templates.length > 0 && (
{templates.map((template) => ( handleTemplateClick(template.id, template.sportsSeasonIds)} >
{template.name} {template.year}
{selectedTemplate === template.id && (
)}
{template.description && (

{template.description}

)}
))}
)} {/* Sports Checkboxes */}

Recommendation: Select 16-20 sports seasons for optimal league balance and variety.

{allSportsSeasons.length > 0 ? ( allSportsSeasons.map((season) => (
handleSportToggle(season.id)} />
)) ) : (

No sports seasons available

)}
{actionData?.error && (
{actionData.error}
)}
); }