import { Form, Link, redirect } from "react-router"; import type { Route } from "./+types/admin.sports.$id"; import { logger } from "~/lib/logger"; import { findSportById, updateSport } from "~/models/sport"; import { deleteCloudinaryImageByUrl } from "~/lib/cloudinary.server"; import { parseSportIconUrlInput } from "~/lib/sport-icon-url"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { Textarea } from "~/components/ui/textarea"; import { SportIconUploader } from "~/components/ui/SportIconUploader"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "~/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "~/components/ui/select"; import { SIMULATOR_TYPES, getSimulatorInfo } from "~/services/simulations/registry"; export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { return [{ title: `${data?.sport?.name ?? "Sport"} - Brackt Admin` }]; } export async function loader({ params }: Route.LoaderArgs) { const sport = await findSportById(params.id); if (!sport) { throw new Response("Sport not found", { status: 404 }); } return { sport }; } export async function action({ request, params }: Route.ActionArgs) { const formData = await request.formData(); const name = formData.get("name"); const type = formData.get("type"); const slug = formData.get("slug"); const description = formData.get("description"); const simulatorType = formData.get("simulatorType"); const iconUrlValue = formData.get("iconUrl"); // Validation if (typeof name !== "string" || !name.trim()) { return { error: "Sport name is required" }; } if (type !== "team" && type !== "individual") { return { error: "Sport type must be either 'team' or 'individual'" }; } if (typeof slug !== "string" || !slug.trim()) { return { error: "Slug is required" }; } // Validate slug format (lowercase, hyphens only) if (!/^[a-z0-9-]+$/.test(slug)) { return { error: "Slug must contain only lowercase letters, numbers, and hyphens" }; } const existingSport = await findSportById(params.id); const iconUrl = parseSportIconUrlInput(iconUrlValue); if (iconUrl === undefined) { return { error: "Sport icon must be an uploaded SVG icon." }; } type ValidSimulatorType = typeof SIMULATOR_TYPES[number]; const parsedSimulatorType: ValidSimulatorType | null = typeof simulatorType === "string" && (SIMULATOR_TYPES as readonly string[]).includes(simulatorType) ? (simulatorType as ValidSimulatorType) : null; try { const updatedSport = await updateSport(params.id, { name: name.trim(), type, slug: slug.trim(), description: typeof description === "string" ? description.trim() : null, iconUrl, simulatorType: parsedSimulatorType, }); if (existingSport?.iconUrl && existingSport.iconUrl !== updatedSport.iconUrl) { deleteCloudinaryImageByUrl(existingSport.iconUrl).catch((error) => { logger.error("Failed to delete replaced sport icon from Cloudinary:", error); }); } return redirect("/admin/sports"); } catch (error) { logger.error("Error updating sport:", error); if (iconUrl && iconUrl !== existingSport?.iconUrl) { deleteCloudinaryImageByUrl(iconUrl).catch((deleteError) => { logger.error("Failed to delete unused sport icon from Cloudinary:", deleteError); }); } const message = error instanceof Error ? error.message : String(error); const isUniqueViolation = message.includes("unique") || message.includes("duplicate"); return { error: isUniqueViolation ? "A sport with that slug already exists. Please choose a different slug." : `Failed to update sport: ${message}`, }; } } export default function EditSport({ loaderData, actionData }: Route.ComponentProps) { const { sport } = loaderData; return (

Edit Sport

Update the sport information

Sport Details Modify the information for this sport

Team sports have teams, individual sports have players

URL-friendly identifier (lowercase, hyphens only)

Algorithm used when running EV simulations for this sport