import { useState } from "react"; import { useFetcher } from "react-router"; import type { Route } from "./+types/admin.data-sync"; import { logger } from "~/lib/logger"; import { findAllSports } from "~/models/sport"; import { findAllSportsSeasons } from "~/models/sports-season"; import { findAllSeasonTemplates } from "~/models/season-template"; import { countAllParticipants } from "~/models/participant"; import { countAllParticipantEVs } from "~/models/participant-expected-value"; import { Button } from "~/components/ui/button"; import { Label } from "~/components/ui/label"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "~/components/ui/card"; import { Download, Upload, AlertTriangle, Database } from "lucide-react"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "~/components/ui/alert-dialog"; export function meta(): Route.MetaDescriptors { return [{ title: "Data Sync - Brackt Admin" }]; } export async function loader() { const [sports, sportsSeasons, templates, participantCount, evCount] = await Promise.all([ findAllSports(), findAllSportsSeasons(), findAllSeasonTemplates(), countAllParticipants(), countAllParticipantEVs(), ]); return { stats: { sportsCount: sports.length, sportsSeasonsCount: sportsSeasons.length, templatesCount: templates.length, participantCount, evCount, }, }; } export async function action({ request }: Route.ActionArgs) { const formData = await request.formData(); const intent = formData.get("intent"); if (intent === "import") { const mode = formData.get("mode") as "merge" | "replace"; const fileData = formData.get("fileData") as string; if (!fileData) { return { success: false, error: "No file data provided" }; } try { const { importSportsDataFromJSON } = await import( "~/utils/sports-data-sync.server" ); const result = await importSportsDataFromJSON(fileData, mode); return { success: true, error: null, message: `Import complete! Created: ${result.created}, Updated: ${result.updated}, Skipped: ${result.skipped}`, }; } catch (error) { logger.error("Import error:", error); return { success: false, error: error instanceof Error ? error.message : "Import failed", message: null, }; } } return { success: false, error: "Invalid intent", message: null }; } export default function DataSync({ loaderData }: Route.ComponentProps) { const { stats } = loaderData; const [importMode, setImportMode] = useState<"merge" | "replace">("merge"); const [selectedFile, setSelectedFile] = useState(null); const [isExporting, setIsExporting] = useState(false); const fetcher = useFetcher(); const isImporting = fetcher.state !== "idle"; const handleExport = async () => { setIsExporting(true); try { const response = await fetch("/api/admin/export-sports-data"); const data = await response.json(); const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json", }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `sports-data-export-${new Date().toISOString().split("T")[0]}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (error) { logger.error("Export failed:", error); alert("Export failed. Please try again."); } finally { setIsExporting(false); } }; const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { setSelectedFile(file); } }; const handleImport = (e: React.FormEvent) => { e.preventDefault(); if (!selectedFile) { alert("Please select a file first"); return; } const reader = new FileReader(); reader.addEventListener("load", (event) => { const fileData = event.target?.result as string; const formData = new FormData(); formData.append("intent", "import"); formData.append("mode", importMode); formData.append("fileData", fileData); fetcher.submit(formData, { method: "POST" }); }); reader.readAsText(selectedFile); }; return (

Data Sync

Import and export sports data between environments

{fetcher.data?.success && (
{fetcher.data.message || "Operation completed successfully!"}
)} {fetcher.data?.error && (
{fetcher.data.error}
)}
Sports
{stats.sportsCount}
Sports Seasons
{stats.sportsSeasonsCount}
Participants
{stats.participantCount}
EV Records
{stats.evCount}
Templates
{stats.templatesCount}
Export Data Download all sports data as a JSON file

This will export all sports, sports seasons, participants, participant EV/futures data, participant results, and season templates.

Import Data Upload a JSON file to import sports data

{importMode === "merge" ? "Updates existing records and adds new ones (safe)" : "Deletes all existing data and recreates from file (destructive)"}

{importMode === "replace" ? ( Are you absolutely sure? This will{" "} delete all existing sports data and replace it with the data from the file. This action cannot be undone.

This includes:
  • All sports
  • All sports seasons
  • All participants
  • All participant EV/futures data
  • All participant results
  • All season templates
Cancel Yes, Replace All Data
) : ( )}
How It Works
Export: Downloads all sports data as a JSON file that you can save, version control, or transfer to another environment.
Import (Merge): Updates existing records and adds new ones. Safe to use — won't delete anything.
Import (Replace): Deletes all existing sports data and recreates it from the file. Use this for a clean slate.
Typical workflow:
  1. Export data from development
  2. Download the JSON file
  3. Upload it to production
  4. Import using merge mode
); }