Files
euchre_camp/src/app/admin/tournaments/new/page.tsx
T

392 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 TournamentFormData {
name: string
description: string
eventDate: string
format: string
maxParticipants: string
participants: number[] // Array of player IDs
}
export default function NewTournamentPage() {
const router = useRouter()
const [step, setStep] = useState(1)
const [formData, setFormData] = useState<TournamentFormData>({
name: "",
description: "",
eventDate: "",
format: "round_robin",
maxParticipants: "",
participants: [],
})
const [error, setError] = useState("")
const [isLoading, setIsLoading] = useState(false)
// Player search state
const [searchQuery, setSearchQuery] = useState("")
const [searchResults, setSearchResults] = useState<Player[]>([])
const [selectedPlayers, setSelectedPlayers] = useState<Player[]>([])
const [isSearching, setIsSearching] = useState(false)
// Search for players as user types
useEffect(() => {
const searchPlayers = async () => {
if (searchQuery.length < 2) {
setSearchResults([])
return
}
setIsSearching(true)
try {
const response = await fetch(`/api/players/search?q=${encodeURIComponent(searchQuery)}`)
const data = await response.json()
if (response.ok) {
// Filter out already selected players
const availablePlayers = data.players.filter(
(p: Player) => !selectedPlayers.find(sp => sp.id === p.id)
)
setSearchResults(availablePlayers)
}
} catch (err) {
console.error("Search failed:", err)
} finally {
setIsSearching(false)
}
}
const debounceTimer = setTimeout(searchPlayers, 300)
return () => clearTimeout(debounceTimer)
}, [searchQuery, selectedPlayers])
const addPlayer = (player: Player) => {
setSelectedPlayers([...selectedPlayers, player])
setSearchQuery("")
setSearchResults([])
}
const removePlayer = (playerId: number) => {
setSelectedPlayers(selectedPlayers.filter(p => p.id !== playerId))
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
})
}
const nextStep = () => {
if (step === 1) {
if (!formData.name.trim()) {
setError("Tournament name is required")
return
}
if (selectedPlayers.length < 2) {
setError("At least 2 participants are required")
return
}
}
setError("")
setStep(step + 1)
}
const prevStep = () => {
setStep(step - 1)
setError("")
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError("")
setIsLoading(true)
try {
// First create the tournament
const tournamentResponse = await fetch("/api/tournaments", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: formData.name,
description: formData.description,
eventDate: formData.eventDate || null,
format: formData.format,
maxParticipants: formData.maxParticipants ? parseInt(formData.maxParticipants) : null,
}),
})
const tournamentData = await tournamentResponse.json()
if (!tournamentResponse.ok) {
throw new Error(tournamentData.error || "Failed to create tournament")
}
const tournamentId = tournamentData.tournament.id
// Add participants to the tournament
if (selectedPlayers.length > 0) {
await fetch(`/api/tournaments/${tournamentId}/participants`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
playerIds: selectedPlayers.map(p => p.id),
}),
})
}
// Redirect to game entry page
router.push(`/admin/tournaments/${tournamentId}/entry`)
} catch (err: any) {
setError(err.message)
} finally {
setIsLoading(false)
}
}
return (
<div className="min-h-screen bg-gray-50">
<Navigation />
<main className="max-w-4xl mx-auto py-6 sm:px-6 lg:px-8">
<div className="px-4 py-6 sm:px-0">
<h1 className="text-3xl font-bold text-gray-900 mb-6">
Create New Tournament
</h1>
{/* Step Indicator */}
<div className="mb-8">
<div className="flex items-center">
<div className={`flex items-center justify-center w-8 h-8 rounded-full ${step >= 1 ? 'bg-green-600 text-white' : 'bg-gray-300 text-gray-600'}`}>
1
</div>
<div className={`flex-1 h-1 ${step >= 2 ? 'bg-green-600' : 'bg-gray-300'}`} />
<div className={`flex items-center justify-center w-8 h-8 rounded-full ${step >= 2 ? 'bg-green-600 text-white' : 'bg-gray-300 text-gray-600'}`}>
2
</div>
</div>
<div className="flex justify-between mt-2 text-sm text-gray-600">
<span>Tournament Details</span>
<span>Participants</span>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="rounded-md bg-red-50 p-4">
<div className="text-sm text-red-700">{error}</div>
</div>
)}
{/* Step 1: Tournament Details */}
{step === 1 && (
<>
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
Tournament Name *
</label>
<input
type="text"
name="name"
id="name"
required
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm"
value={formData.name}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
Description
</label>
<textarea
name="description"
id="description"
rows={3}
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm"
value={formData.description}
onChange={handleChange}
/>
</div>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
<label htmlFor="eventDate" className="block text-sm font-medium text-gray-700">
Event Date
</label>
<input
type="date"
name="eventDate"
id="eventDate"
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm"
value={formData.eventDate}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="format" className="block text-sm font-medium text-gray-700">
Format *
</label>
<select
name="format"
id="format"
required
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm"
value={formData.format}
onChange={handleChange}
>
<option value="round_robin">Round Robin</option>
<option value="single_elimination">Single Elimination</option>
<option value="double_elimination">Double Elimination</option>
<option value="swiss">Swiss</option>
</select>
</div>
</div>
<div>
<label htmlFor="maxParticipants" className="block text-sm font-medium text-gray-700">
Max Participants
</label>
<input
type="number"
name="maxParticipants"
id="maxParticipants"
min="2"
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm"
value={formData.maxParticipants}
onChange={handleChange}
/>
</div>
</>
)}
{/* Step 2: Participants */}
{step === 2 && (
<>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Add Participants
</label>
<div className="relative">
<input
type="text"
placeholder="Search for players..."
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
{isSearching && (
<div className="absolute right-3 top-3">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-green-600"></div>
</div>
)}
</div>
{searchResults.length > 0 && (
<div className="mt-2 border border-gray-300 rounded-md shadow-sm max-h-48 overflow-y-auto">
{searchResults.map(player => (
<div
key={player.id}
className="px-3 py-2 hover:bg-gray-100 cursor-pointer flex justify-between items-center"
onClick={() => addPlayer(player)}
>
<span>{player.name}</span>
<span className="text-sm text-gray-500">Elo: {player.currentElo}</span>
</div>
))}
</div>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Selected Participants ({selectedPlayers.length})
</label>
{selectedPlayers.length > 0 ? (
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
{selectedPlayers.map(player => (
<div
key={player.id}
className="bg-green-50 border border-green-200 rounded-md px-3 py-2 flex justify-between items-center"
>
<span className="text-sm">{player.name}</span>
<button
type="button"
onClick={() => removePlayer(player.id)}
className="text-red-600 hover:text-red-800 ml-2"
>
×
</button>
</div>
))}
</div>
) : (
<p className="text-gray-500 text-sm">No participants added yet</p>
)}
</div>
</>
)}
{/* Navigation Buttons */}
<div className="flex justify-between pt-4">
<div>
{step > 1 && (
<button
type="button"
onClick={prevStep}
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
Back
</button>
)}
</div>
<div className="space-x-3">
{step < 2 ? (
<button
type="button"
onClick={nextStep}
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
Next
</button>
) : (
<>
<button
type="button"
onClick={() => router.back()}
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
Cancel
</button>
<button
type="submit"
disabled={isLoading}
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50"
>
{isLoading ? "Creating..." : "Create Tournament & Enter Games"}
</button>
</>
)}
</div>
</div>
</form>
</div>
</main>
</div>
)
}