diff --git a/prisma/dev.db b/prisma/dev.db index 10470f4..9a3a28c 100644 Binary files a/prisma/dev.db and b/prisma/dev.db differ diff --git a/prisma/migrations/20260330200000_add_event_participant_unique/migration.sql b/prisma/migrations/20260330200000_add_event_participant_unique/migration.sql new file mode 100644 index 0000000..cf32a20 --- /dev/null +++ b/prisma/migrations/20260330200000_add_event_participant_unique/migration.sql @@ -0,0 +1,2 @@ +-- Add unique constraint to EventParticipant table +CREATE UNIQUE INDEX "event_participants_eventId_playerId_key" ON "event_participants"("eventId", "playerId"); diff --git a/prisma/prisma/dev.db b/prisma/prisma/dev.db index 79a41ab..6b21707 100644 Binary files a/prisma/prisma/dev.db and b/prisma/prisma/dev.db differ diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cec4d74..7ea9e25 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -100,6 +100,7 @@ model EventParticipant { player Player @relation(fields: [playerId], references: [id]) event Event @relation(fields: [eventId], references: [id]) + @@unique([eventId, playerId]) @@map("event_participants") } diff --git a/src/app/admin/tournaments/[id]/entry/page.tsx b/src/app/admin/tournaments/[id]/entry/page.tsx new file mode 100644 index 0000000..099cad6 --- /dev/null +++ b/src/app/admin/tournaments/[id]/entry/page.tsx @@ -0,0 +1,299 @@ +"use client" + +import { useState, useEffect } from "react" +import { useRouter } from "next/navigation" +import Navigation from "@/components/Navigation" + +interface Player { + id: number + name: string + currentElo: number +} + +interface Tournament { + id: number + name: string + eventDate: string | null + format: string + participants: { + player: Player + }[] +} + +interface GameEntry { + round: number + table: string + player1: string + player2: string + score1: number + player3: string + player4: string + score2: number +} + +export default function TournamentEntryPage({ params }: { params: { id: string } }) { + const router = useRouter() + const [tournament, setTournament] = useState(null) + const [gameText, setGameText] = useState("") + const [parsedGames, setParsedGames] = useState([]) + const [error, setError] = useState("") + const [success, setSuccess] = useState("") + const [isLoading, setIsLoading] = useState(false) + + useEffect(() => { + loadTournament() + }, []) + + const loadTournament = async () => { + try { + const response = await fetch(`/api/tournaments/${params.id}`) + const data = await response.json() + if (response.ok) { + setTournament(data.tournament) + } + } catch (err) { + console.error("Failed to load tournament:", err) + } + } + + const parseGameText = (text: string): GameEntry[] => { + const lines = text.trim().split("\n") + const games: GameEntry[] = [] + + for (const line of lines) { + // Skip empty lines and comments + if (!line.trim() || line.trim().startsWith("#")) continue + + // Parse tab-separated or comma-separated values + const parts = line.split(/[,\t]/).map(p => p.trim()) + + if (parts.length >= 7) { + games.push({ + round: parseInt(parts[0]) || 1, + table: parts[1] || "", + player1: parts[2], + player2: parts[3], + score1: parseInt(parts[4]) || 0, + player3: parts[5], + player4: parts[6], + score2: parseInt(parts[7]) || 0, + }) + } + } + + return games + } + + const handleTextChange = (e: React.ChangeEvent) => { + const text = e.target.value + setGameText(text) + const games = parseGameText(text) + setParsedGames(games) + } + + const submitGames = async () => { + if (parsedGames.length === 0) { + setError("No valid games to submit") + return + } + + setError("") + setSuccess("") + setIsLoading(true) + + try { + const response = await fetch(`/api/tournaments/${params.id}/games/bulk`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + games: parsedGames, + }), + }) + + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || "Failed to submit games") + } + + setSuccess(`Successfully imported ${data.importedCount} games`) + setGameText("") + setParsedGames([]) + } catch (err: any) { + setError(err.message) + } finally { + setIsLoading(false) + } + } + + if (!tournament) { + return ( +
+ +
+
+
+
+
+
+ ) + } + + return ( +
+ + +
+
+
+ +

+ Game Entry: {tournament.name} +

+ {tournament.eventDate && ( +

+ {new Date(tournament.eventDate).toLocaleDateString()} +

+ )} +
+ + {error && ( +
+
{error}
+
+ )} + + {success && ( +
+
{success}
+
+ )} + +
+ {/* Participants Panel */} +
+
+

+ Participants ({tournament.participants.length}) +

+
+ {tournament.participants.map(({ player }) => ( +
+ {player.name} +
+ ))} +
+
+
+ + {/* Game Entry Panel */} +
+
+

+ Enter Games +

+ +
+ +
+

Tab or comma-separated format:

+ + Round Table Player1 Player2 Score1 Player3 Player4 Score2 + +

+ Example: 1 1 John Smith Jane Doe 10 Mike Johnson Sarah Brown 5 +

+

+ Lines starting with # are treated as comments +

+
+
+ +
+ +