import { Form, Link, redirect } from "react-router"; import { auth } from "~/lib/auth.server"; import type { Route } from "./+types/admin.sports-seasons.$id.clone"; import { logger } from "~/lib/logger"; import { findSportsSeasonById, cloneSportsSeason, shiftDateByYears, type NewSportsSeason } from "~/models/sports-season"; import { isUserAdmin } from "~/models/user"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; 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 { Copy } from "lucide-react"; import { useState } from "react"; export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { return [{ title: `Clone ${data?.sourceSeason?.name ?? "Sports Season"} - Brackt Admin` }]; } export async function loader({ params }: Route.LoaderArgs) { const sourceSeason = await findSportsSeasonById(params.id); if (!sourceSeason) { throw new Response("Sports season not found", { status: 404 }); } const delta = 1; const newYear = sourceSeason.year + delta; const defaults = { name: sourceSeason.name.replace(/\b(20\d{2})\b/g, (_, y) => String(parseInt(y, 10) + delta)), year: newYear, startDate: sourceSeason.startDate ? shiftDateByYears(sourceSeason.startDate, delta) : "", endDate: sourceSeason.endDate ? shiftDateByYears(sourceSeason.endDate, delta) : "", draftOn: shiftDateByYears(sourceSeason.draftOn, delta), draftOff: shiftDateByYears(sourceSeason.draftOff, delta), scoringType: sourceSeason.scoringType, scoringPattern: sourceSeason.scoringPattern ?? "", totalMajors: sourceSeason.totalMajors ?? 4, }; return { sourceSeason, defaults }; } export async function action(args: Route.ActionArgs) { const { request, params } = args; const session = await auth.api.getSession({ headers: args.request.headers }); const userId = session?.user.id ?? null; const isAdmin = userId ? await isUserAdmin(userId) : false; if (!isAdmin) { throw new Response("Forbidden", { status: 403 }); } const formData = await request.formData(); const sportId = formData.get("sportId"); const name = formData.get("name"); const year = formData.get("year"); const startDate = formData.get("startDate"); const endDate = formData.get("endDate"); const scoringType = formData.get("scoringType"); const scoringPattern = formData.get("scoringPattern"); const totalMajors = formData.get("totalMajors"); const draftOn = formData.get("draftOn"); const draftOff = formData.get("draftOff"); // Validation if (typeof sportId !== "string" || !sportId) { return { error: "Sport ID is missing" }; } // Cross-validate sportId against source season to prevent tampering const sourceSeason = await findSportsSeasonById(params.id); if (!sourceSeason) { throw new Response("Sports season not found", { status: 404 }); } if (sportId !== sourceSeason.sportId) { return { error: "Sport ID does not match source season" }; } if (typeof name !== "string" || !name.trim()) { return { error: "Season name is required" }; } if (typeof year !== "string") { return { error: "Year is required" }; } const yearNum = parseInt(year, 10); if (isNaN(yearNum) || yearNum < 2000 || yearNum > 2100) { return { error: "Year must be between 2000 and 2100" }; } if (scoringType !== "playoffs" && scoringType !== "regular_season" && scoringType !== "majors") { return { error: "Invalid scoring type" }; } const validScoringPatterns = ["playoff_bracket", "season_standings", "qualifying_points"]; if (scoringPattern && typeof scoringPattern === "string" && !validScoringPatterns.includes(scoringPattern)) { return { error: "Invalid scoring pattern" }; } if (typeof draftOn !== "string" || !draftOn) { return { error: "Draft open date is required" }; } if (typeof draftOff !== "string" || !draftOff) { return { error: "Draft close date is required" }; } if (draftOff < draftOn) { return { error: "Draft close date must be on or after draft open date" }; } const newSeasonData: Partial = { sportId, name: name.trim(), year: yearNum, startDate: typeof startDate === "string" && startDate ? startDate : null, endDate: typeof endDate === "string" && endDate ? endDate : null, status: "upcoming", simulationStatus: "idle", majorsCompleted: 0, qualifyingPointsFinalized: false, scoringType, draftOn, draftOff, }; if (scoringPattern && typeof scoringPattern === "string") { newSeasonData.scoringPattern = scoringPattern as "playoff_bracket" | "season_standings" | "qualifying_points"; } if (totalMajors && typeof totalMajors === "string") { const totalMajorsNum = parseInt(totalMajors, 10); if (!isNaN(totalMajorsNum) && totalMajorsNum > 0) { newSeasonData.totalMajors = totalMajorsNum; } } let newSeason; try { newSeason = await cloneSportsSeason(params.id, newSeasonData as NewSportsSeason); } catch (error) { logger.error("Error cloning sports season:", error); return { error: "Failed to clone sports season. Please try again." }; } return redirect(`/admin/sports-seasons/${newSeason.id}`); } export default function CloneSportsSeason({ loaderData, actionData }: Route.ComponentProps) { const { sourceSeason, defaults } = loaderData; const [scoringPattern, setScoringPattern] = useState(defaults.scoringPattern); return (

Clone Sports Season

Clone

Cloning from: {sourceSeason.name}

New Sports Season Details Review and adjust the pre-filled settings. Participants, events, futures odds, and Elo ratings will be copied automatically.
{sourceSeason.sport.name}

Sport is locked to the source season.

Playoffs: Team sports playoffs. Regular Season: Full season standings. Majors: Individual sport majors.

{scoringPattern === "qualifying_points" && (

How many major tournaments will be tracked?

)}

This season appears in league creation and pre-draft settings only between these two dates (inclusive).

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