import { useState, useRef, useEffect } from "react"; import { useFetcher, useRevalidator } from "react-router"; import { Button } from "~/components/ui/button"; import { Textarea } from "~/components/ui/textarea"; import { Input } from "~/components/ui/input"; import { Badge } from "~/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "~/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "~/components/ui/table"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "~/components/ui/card"; import { CheckCircle2, Trophy } from "lucide-react"; import { parseResultsText } from "~/lib/parse-results-text"; interface Participant { id: string; name: string; } interface ResolvedRow { placement: number; rawName: string; participantId: string | null; // null = unresolved } type Stage = "idle" | "preview" | "saving"; interface BatchResultEntryProps { participants: Participant[]; sportsSeasonId: string; existingResultParticipantIds: Set; } export function BatchResultEntry({ participants, sportsSeasonId, existingResultParticipantIds, }: BatchResultEntryProps) { const [stage, setStage] = useState("idle"); const [pasteText, setPasteText] = useState(""); const [rows, setRows] = useState([]); const [parseError, setParseError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); const [addingNewFor, setAddingNewFor] = useState(null); const [newParticipantName, setNewParticipantName] = useState(""); const { revalidate } = useRevalidator(); const saveFetcher = useFetcher(); const createParticipantFetcher = useFetcher(); // Mutable lookup built from the prop — updated when new participants are created const participantByNameRef = useRef( new Map(participants.map((p) => [p.name.toLowerCase(), p])) ); // Mutable list — grows when new participants are created inline const participantsRef = useRef([...participants]); const pendingNewParticipantRowRef = useRef(null); const createData = createParticipantFetcher.data as | { newParticipant?: { id: string; name: string }; error?: string } | undefined; const saveData = saveFetcher.data as | { success?: string; error?: string } | undefined; // When createParticipantFetcher returns a new participant, auto-select it useEffect(() => { if ( createData?.newParticipant && pendingNewParticipantRowRef.current !== null ) { const newP = createData.newParticipant; const rowIndex = pendingNewParticipantRowRef.current; pendingNewParticipantRowRef.current = null; // Update lookup and list participantByNameRef.current.set(newP.name.toLowerCase(), newP); participantsRef.current.push(newP); // Auto-select for the row setRows((prev) => prev.map((row, i) => i === rowIndex ? { ...row, participantId: newP.id } : row ) ); } }, [createData]); // Watch save fetcher for completion useEffect(() => { if (stage === "saving" && saveFetcher.state === "idle" && saveData) { if (saveData.success) { setSuccessMessage(saveData.success); setStage("idle"); setPasteText(""); setRows([]); revalidate(); } else if (saveData.error) { setStage("preview"); } } }, [saveFetcher.state, saveData, stage, revalidate]); function handleParse() { setParseError(null); const parsed = parseResultsText(pasteText); if (parsed.length === 0) { setParseError("No results found. Check the format and try again."); return; } const resolved: ResolvedRow[] = parsed.map((line) => { const match = participantByNameRef.current.get(line.rawName.toLowerCase()); return { placement: line.placement, rawName: line.rawName, participantId: match ? match.id : null, }; }); setRows(resolved); setStage("preview"); } function setRowParticipant(index: number, participantId: string) { setRows((prev) => prev.map((row, i) => (i === index ? { ...row, participantId } : row)) ); } function handleAddNewParticipant(index: number) { setAddingNewFor(index); setNewParticipantName(rows[index].rawName); } function handleCreateParticipant(index: number) { if (!newParticipantName.trim()) return; const formData = new FormData(); formData.set("intent", "create-participant"); formData.set("name", newParticipantName.trim()); formData.set("sportsSeasonId", sportsSeasonId); createParticipantFetcher.submit(formData, { method: "post" }); pendingNewParticipantRowRef.current = index; setAddingNewFor(null); setNewParticipantName(""); } function handleConfirm() { if (!allResolved || rows.length === 0) return; setStage("saving"); const results = rows .filter((r) => r.participantId !== null) .map((r) => ({ participantId: r.participantId as string, placement: r.placement, })); const formData = new FormData(); formData.set("intent", "batch-add-results"); formData.set("results", JSON.stringify(results)); saveFetcher.submit(formData, { method: "post" }); } const allResolved = rows.every((r) => r.participantId !== null); if (successMessage) { return (
{successMessage}
); } // Participants available for mapping — exclude those already in saved results // (but include any that are currently resolved in this batch, to allow re-use) const resolvedIds = new Set(rows.map((r) => r.participantId).filter(Boolean) as string[]); const availableParticipants = participantsRef.current.filter( (p) => !existingResultParticipantIds.has(p.id) || resolvedIds.has(p.id) ); return ( Batch Paste Results Paste a ranked results list to import all placements at once. Format:{" "} 1. Player Name {" "} — use{" "} T3 or repeat a rank for ties. {stage === "idle" && (