bb6be245b7
## Summary This PR implements the tournament schedule tab functionality and fixes all remaining E2E test failures. ### Changes Included 1. **Tournament Schedule Feature** - Added tournament schedule page at `/admin/tournaments/[id]/schedule` - Implemented "Generate Schedule" button functionality - Added schedule generation logic for round-robin tournaments 2. **E2E Test Fixes** - Fixed database connection issues in production builds - Improved test reliability with better error handling and debugging - Updated test infrastructure to use environment variables instead of hardcoded values 3. **CI/CD Updates** - Added E2E test job to PR workflow - Configured tests to run against development database - Moved database password to Gitea secrets 4. **Code Quality** - Removed hardcoded passwords from codebase - Improved Prisma client configuration - Enhanced authentication and navigation components ### Test Results All 16 E2E test scenarios are now passing: - Authentication tests: ✅ - Registration tests: ✅ - Tournament schedule tests: ✅ - Player schedule tests: ✅ - Admin navigation tests: ✅ ### Database Configuration - Tests run against `euchre_camp_dev` database - Production builds use environment variables for database configuration - Database password stored in Gitea secrets as `DB_PASSWORD` ### CI Pipeline The PR workflow now includes: 1. Unit tests 2. E2E tests (using production build) 3. Version bump analysis E2E tests must pass before PR can be merged. Reviewed-on: #27 Co-authored-by: David Gwilliam <dhgwilliam@gmail.com> Co-committed-by: David Gwilliam <dhgwilliam@gmail.com>
661 lines
23 KiB
TypeScript
661 lines
23 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import { useRouter } from "next/navigation"
|
|
|
|
interface Player {
|
|
id: number
|
|
name: string
|
|
currentElo: number
|
|
}
|
|
|
|
interface Team {
|
|
id: number
|
|
teamName: string | null
|
|
player1: Player
|
|
player2: Player
|
|
}
|
|
|
|
interface TeamsSectionProps {
|
|
tournamentId: number
|
|
participants: Player[]
|
|
teamDurability: string
|
|
partnerRotation: string
|
|
allowByes: boolean
|
|
}
|
|
|
|
type TeamDurabilityOption = 'permanent' | 'variable' | 'per_round'
|
|
type PartnerRotationOption = 'none' | 'minimize_repeat' | 'maximize_even' | 'elo_based'
|
|
|
|
export default function TeamsSection({
|
|
tournamentId,
|
|
participants,
|
|
teamDurability: initialTeamDurability,
|
|
partnerRotation: initialPartnerRotation,
|
|
allowByes: initialAllowByes,
|
|
}: TeamsSectionProps) {
|
|
const router = useRouter()
|
|
const [teams, setTeams] = useState<Team[]>([])
|
|
const [teamDurability, setTeamDurability] = useState<TeamDurabilityOption>(initialTeamDurability as TeamDurabilityOption)
|
|
const [partnerRotation, setPartnerRotation] = useState<PartnerRotationOption>(initialPartnerRotation as PartnerRotationOption)
|
|
const [allowByes, setAllowByes] = useState(initialAllowByes)
|
|
const [isGenerating, setIsGenerating] = useState(false)
|
|
const [error, setError] = useState("")
|
|
const [success, setSuccess] = useState("")
|
|
|
|
// Manual team entry state
|
|
const [manualTeamMode, setManualTeamMode] = useState(false)
|
|
const [selectedPlayer1, setSelectedPlayer1] = useState<number | null>(null)
|
|
const [selectedPlayer2, setSelectedPlayer2] = useState<number | null>(null)
|
|
const [newTeamName, setNewTeamName] = useState("")
|
|
|
|
const handleSaveConfig = async () => {
|
|
setError("")
|
|
setSuccess("")
|
|
setIsGenerating(true)
|
|
|
|
try {
|
|
const response = await fetch(`/api/tournaments/${tournamentId}`, {
|
|
method: "PUT",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
teamDurability,
|
|
partnerRotation,
|
|
allowByes,
|
|
}),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
try {
|
|
const errorData = await response.json()
|
|
setError(errorData.error || "Failed to save configuration")
|
|
} catch {
|
|
setError(`Failed to save configuration: ${response.status} ${response.statusText}`)
|
|
}
|
|
return
|
|
}
|
|
|
|
setSuccess("Configuration saved successfully!")
|
|
router.refresh()
|
|
} catch (err) {
|
|
if (err instanceof Error) {
|
|
setError(err.message)
|
|
} else {
|
|
setError("An unknown error occurred")
|
|
}
|
|
} finally {
|
|
setIsGenerating(false)
|
|
}
|
|
}
|
|
|
|
const handleGenerateSchedule = async () => {
|
|
if (participants.length < 2) {
|
|
setError("At least 2 participants are required to generate a schedule")
|
|
return
|
|
}
|
|
|
|
if (participants.length % 2 !== 0 && !allowByes) {
|
|
setError("Odd number of participants. Enable 'Allow Byes' to proceed.")
|
|
return
|
|
}
|
|
|
|
setError("")
|
|
setSuccess("")
|
|
setIsGenerating(true)
|
|
|
|
try {
|
|
const response = await fetch(`/api/tournaments/${tournamentId}/schedule`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
})
|
|
|
|
if (!response.ok) {
|
|
try {
|
|
const errorData = await response.json()
|
|
setError(errorData.error || "Failed to generate schedule")
|
|
} catch {
|
|
setError(`Failed to generate schedule: ${response.status} ${response.statusText}`)
|
|
}
|
|
setIsGenerating(false)
|
|
return
|
|
}
|
|
|
|
const data = await response.json()
|
|
// Update teams from the generated schedule
|
|
// Extract teams from round matchups
|
|
const generatedTeams: Team[] = []
|
|
for (const round of data.rounds) {
|
|
for (const matchup of round.bracketMatchups) {
|
|
// Create unique keys based on player IDs
|
|
const team1Key = `${matchup.player1P1.id}-${matchup.player1P2.id}`
|
|
const team2Key = `${matchup.player2P1.id}-${matchup.player2P2.id}`
|
|
|
|
// Add unique teams
|
|
const team1 = {
|
|
id: matchup.player1P1.id * 10000 + matchup.player1P2.id,
|
|
teamName: `${matchup.player1P1.name} & ${matchup.player1P2.name}`,
|
|
player1: matchup.player1P1,
|
|
player2: matchup.player1P2,
|
|
}
|
|
const team2 = {
|
|
id: matchup.player2P1.id * 10000 + matchup.player2P2.id,
|
|
teamName: `${matchup.player2P1.name} & ${matchup.player2P2.name}`,
|
|
player1: matchup.player2P1,
|
|
player2: matchup.player2P2,
|
|
}
|
|
|
|
// Check if team already exists
|
|
const existingTeam1 = generatedTeams.find(
|
|
t => t.player1.id === team1.player1.id && t.player2.id === team1.player2.id
|
|
)
|
|
if (!existingTeam1) generatedTeams.push(team1)
|
|
|
|
const existingTeam2 = generatedTeams.find(
|
|
t => t.player1.id === team2.player1.id && t.player2.id === team2.player2.id
|
|
)
|
|
if (!existingTeam2) generatedTeams.push(team2)
|
|
}
|
|
}
|
|
setTeams(generatedTeams)
|
|
setSuccess(`Successfully generated ${data.roundsCreated} rounds with ${data.matchupsCreated} matchups!`)
|
|
router.refresh()
|
|
} catch (err) {
|
|
if (err instanceof Error) {
|
|
setError(err.message)
|
|
} else {
|
|
setError("An unknown error occurred")
|
|
}
|
|
} finally {
|
|
setIsGenerating(false)
|
|
}
|
|
}
|
|
|
|
const handleDeleteTeams = async () => {
|
|
if (!confirm("Are you sure you want to delete the schedule? This will remove all rounds and matchups.")) {
|
|
return
|
|
}
|
|
|
|
setError("")
|
|
setSuccess("")
|
|
setIsGenerating(true)
|
|
|
|
try {
|
|
// Delete schedule (which includes matchups/rounds)
|
|
const response = await fetch(`/api/tournaments/${tournamentId}/schedule`, {
|
|
method: "DELETE",
|
|
})
|
|
|
|
if (!response.ok) {
|
|
try {
|
|
const errorData = await response.json()
|
|
setError(errorData.error || "Failed to delete schedule")
|
|
} catch {
|
|
setError(`Failed to delete schedule: ${response.status} ${response.statusText}`)
|
|
}
|
|
return
|
|
}
|
|
|
|
setTeams([])
|
|
setSuccess("Schedule deleted successfully!")
|
|
router.refresh()
|
|
} catch (err) {
|
|
if (err instanceof Error) {
|
|
setError(err.message)
|
|
} else {
|
|
setError("An unknown error occurred")
|
|
}
|
|
} finally {
|
|
setIsGenerating(false)
|
|
}
|
|
}
|
|
|
|
// Handle adding a manual team
|
|
const handleAddManualTeam = () => {
|
|
if (!selectedPlayer1 || !selectedPlayer2) {
|
|
setError("Please select two players for the team")
|
|
return
|
|
}
|
|
|
|
if (selectedPlayer1 === selectedPlayer2) {
|
|
setError("A player cannot be on a team with themselves")
|
|
return
|
|
}
|
|
|
|
// Check if player is already in a team
|
|
const player1InTeam = teams.some(t => t.player1.id === selectedPlayer1 || t.player2.id === selectedPlayer1)
|
|
const player2InTeam = teams.some(t => t.player1.id === selectedPlayer2 || t.player2.id === selectedPlayer2)
|
|
|
|
if (player1InTeam || player2InTeam) {
|
|
setError("One or both players are already on a team")
|
|
return
|
|
}
|
|
|
|
const player1 = participants.find(p => p.id === selectedPlayer1)
|
|
const player2 = participants.find(p => p.id === selectedPlayer2)
|
|
|
|
if (!player1 || !player2) {
|
|
setError("Invalid player selection")
|
|
return
|
|
}
|
|
|
|
const newTeam: Team = {
|
|
id: player1.id * 10000 + player2.id,
|
|
teamName: newTeamName || `${player1.name} & ${player2.name}`,
|
|
player1,
|
|
player2,
|
|
}
|
|
|
|
setTeams([...teams, newTeam])
|
|
setSelectedPlayer1(null)
|
|
setSelectedPlayer2(null)
|
|
setNewTeamName("")
|
|
setError("")
|
|
setSuccess("Team added!")
|
|
|
|
// Clear success message after 2 seconds
|
|
setTimeout(() => setSuccess(""), 2000)
|
|
}
|
|
|
|
// Handle removing a manual team
|
|
const handleRemoveTeam = (teamId: number) => {
|
|
setTeams(teams.filter(t => t.id !== teamId))
|
|
setSuccess("Team removed!")
|
|
setTimeout(() => setSuccess(""), 2000)
|
|
}
|
|
|
|
// Handle saving manual teams
|
|
const handleSaveManualTeams = async () => {
|
|
if (teams.length === 0) {
|
|
setError("Please add at least one team")
|
|
return
|
|
}
|
|
|
|
setError("")
|
|
setSuccess("")
|
|
setIsGenerating(true)
|
|
|
|
try {
|
|
// For permanent teams with manual entry, we need to save the team list
|
|
// This would require a new API endpoint or extending the tournament update
|
|
const response = await fetch(`/api/tournaments/${tournamentId}`, {
|
|
method: "PUT",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
teamDurability: "permanent",
|
|
manualTeams: teams.map(t => ({
|
|
player1Id: t.player1.id,
|
|
player2Id: t.player2.id,
|
|
teamName: t.teamName,
|
|
})),
|
|
}),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json()
|
|
throw new Error(errorData.error || "Failed to save teams")
|
|
}
|
|
|
|
setSuccess("Teams saved successfully!")
|
|
router.refresh()
|
|
} catch (err) {
|
|
if (err instanceof Error) {
|
|
setError(err.message)
|
|
} else {
|
|
setError("An unknown error occurred")
|
|
}
|
|
} finally {
|
|
setIsGenerating(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="bg-white shadow rounded-lg p-6 mb-6">
|
|
<h2 className="text-lg font-medium text-gray-900 mb-4">
|
|
Matchups {teams.length > 0 && `(${teams.length})`}
|
|
</h2>
|
|
|
|
{/* Configuration Panel */}
|
|
<div className="bg-gray-50 rounded-lg p-4 mb-6">
|
|
<h3 className="text-sm font-medium text-gray-700 mb-3">Team Configuration</h3>
|
|
|
|
{/* Team Durability */}
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-600 mb-2">
|
|
Team Durability
|
|
</label>
|
|
<div className="flex gap-4">
|
|
<label className="flex items-center">
|
|
<input
|
|
type="radio"
|
|
name="teamDurability"
|
|
value="permanent"
|
|
checked={teamDurability === 'permanent'}
|
|
onChange={(e) => setTeamDurability(e.target.value as TeamDurabilityOption)}
|
|
className="mr-2"
|
|
/>
|
|
<span className="text-sm">Permanent Teams</span>
|
|
</label>
|
|
<label className="flex items-center">
|
|
<input
|
|
type="radio"
|
|
name="teamDurability"
|
|
value="variable"
|
|
checked={teamDurability === 'variable'}
|
|
onChange={(e) => setTeamDurability(e.target.value as TeamDurabilityOption)}
|
|
className="mr-2"
|
|
/>
|
|
<span className="text-sm">Pre-Planned Variable</span>
|
|
</label>
|
|
<label className="flex items-center">
|
|
<input
|
|
type="radio"
|
|
name="teamDurability"
|
|
value="per_round"
|
|
checked={teamDurability === 'per_round'}
|
|
onChange={(e) => setTeamDurability(e.target.value as TeamDurabilityOption)}
|
|
className="mr-2"
|
|
/>
|
|
<span className="text-sm">Dynamic/Progressive</span>
|
|
</label>
|
|
</div>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
{teamDurability === 'permanent'
|
|
? 'Teams formed once and stay fixed throughout the tournament.'
|
|
: teamDurability === 'variable'
|
|
? 'Fresh teams each round with partner rotation. Schedule is pre-planned.'
|
|
: 'Teams formed based on results. Schedule progresses as rounds complete.'}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Partner Rotation (for variable/per_round teams) */}
|
|
{(teamDurability === 'variable' || teamDurability === 'per_round') && (
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-600 mb-2">
|
|
Partner Rotation Strategy
|
|
</label>
|
|
<div className="flex gap-4 flex-wrap">
|
|
<label className="flex items-center">
|
|
<input
|
|
type="radio"
|
|
name="partnerRotation"
|
|
value="none"
|
|
checked={partnerRotation === 'none'}
|
|
onChange={(e) => setPartnerRotation(e.target.value as PartnerRotationOption)}
|
|
className="mr-2"
|
|
/>
|
|
<span className="text-sm">None (Random)</span>
|
|
</label>
|
|
<label className="flex items-center">
|
|
<input
|
|
type="radio"
|
|
name="partnerRotation"
|
|
value="minimize_repeat"
|
|
checked={partnerRotation === 'minimize_repeat'}
|
|
onChange={(e) => setPartnerRotation(e.target.value as PartnerRotationOption)}
|
|
className="mr-2"
|
|
/>
|
|
<span className="text-sm">Minimize Repeat</span>
|
|
</label>
|
|
<label className="flex items-center">
|
|
<input
|
|
type="radio"
|
|
name="partnerRotation"
|
|
value="maximize_even"
|
|
checked={partnerRotation === 'maximize_even'}
|
|
onChange={(e) => setPartnerRotation(e.target.value as PartnerRotationOption)}
|
|
className="mr-2"
|
|
/>
|
|
<span className="text-sm">Maximize Even</span>
|
|
</label>
|
|
<label className="flex items-center">
|
|
<input
|
|
type="radio"
|
|
name="partnerRotation"
|
|
value="elo_based"
|
|
checked={partnerRotation === 'elo_based'}
|
|
onChange={(e) => setPartnerRotation(e.target.value as PartnerRotationOption)}
|
|
className="mr-2"
|
|
/>
|
|
<span className="text-sm">ELO-Based</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Manual Team Entry Toggle (for permanent teams) */}
|
|
{teamDurability === 'permanent' && (
|
|
<div className="mb-4">
|
|
<label className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={manualTeamMode}
|
|
onChange={(e) => setManualTeamMode(e.target.checked)}
|
|
className="mr-2"
|
|
/>
|
|
<span className="text-sm font-medium text-gray-600">Enter teams manually</span>
|
|
</label>
|
|
<p className="text-xs text-gray-500 mt-1 ml-6">
|
|
Check this to manually create teams instead of auto-generating them.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Allow Byes (for odd participant counts) */}
|
|
<div className="mb-4">
|
|
<label className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={allowByes}
|
|
onChange={(e) => setAllowByes(e.target.checked)}
|
|
className="mr-2"
|
|
/>
|
|
<span className="text-sm font-medium text-gray-600">Allow Byes (for odd number of participants)</span>
|
|
</label>
|
|
<p className="text-xs text-gray-500 mt-1 ml-6">
|
|
When enabled, one player will have a bye each round if there's an odd number of participants.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex gap-3 mt-4">
|
|
<button
|
|
onClick={handleSaveConfig}
|
|
disabled={isGenerating}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 text-sm"
|
|
>
|
|
{isGenerating ? "Saving..." : "Save Configuration"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Error/Success Messages */}
|
|
{error && (
|
|
<div className="rounded-md bg-red-50 p-4 mb-4">
|
|
<div className="text-sm text-red-700">{error}</div>
|
|
</div>
|
|
)}
|
|
|
|
{success && (
|
|
<div className="rounded-md bg-green-50 p-4 mb-4">
|
|
<div className="text-sm text-green-700">{success}</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Manual Team Entry Form */}
|
|
{manualTeamMode && teamDurability === 'permanent' && (
|
|
<div className="bg-gray-50 rounded-lg p-4 mb-6 border-2 border-dashed border-gray-300">
|
|
<h3 className="text-sm font-medium text-gray-700 mb-3">Manual Team Entry</h3>
|
|
|
|
<div className="grid grid-cols-2 gap-4 mb-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-600 mb-2">
|
|
Player 1
|
|
</label>
|
|
<select
|
|
value={selectedPlayer1 || ""}
|
|
onChange={(e) => setSelectedPlayer1(e.target.value ? parseInt(e.target.value) : null)}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm"
|
|
>
|
|
<option value="">Select player...</option>
|
|
{participants
|
|
.filter(p => !teams.some(t => t.player1.id === p.id || t.player2.id === p.id))
|
|
.map(p => (
|
|
<option key={p.id} value={p.id}>{p.name} (Elo: {p.currentElo})</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-600 mb-2">
|
|
Player 2
|
|
</label>
|
|
<select
|
|
value={selectedPlayer2 || ""}
|
|
onChange={(e) => setSelectedPlayer2(e.target.value ? parseInt(e.target.value) : null)}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm"
|
|
>
|
|
<option value="">Select player...</option>
|
|
{participants
|
|
.filter(p => p.id !== selectedPlayer1 && !teams.some(t => t.player1.id === p.id || t.player2.id === p.id))
|
|
.map(p => (
|
|
<option key={p.id} value={p.id}>{p.name} (Elo: {p.currentElo})</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-600 mb-2">
|
|
Team Name (optional)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={newTeamName}
|
|
onChange={(e) => setNewTeamName(e.target.value)}
|
|
placeholder="e.g., The Aces"
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-3">
|
|
<button
|
|
onClick={handleAddManualTeam}
|
|
disabled={!selectedPlayer1 || !selectedPlayer2}
|
|
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:opacity-50 text-sm"
|
|
>
|
|
Add Team
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setSelectedPlayer1(null)
|
|
setSelectedPlayer2(null)
|
|
setNewTeamName("")
|
|
}}
|
|
className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400 text-sm"
|
|
>
|
|
Clear
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Team Generation Controls */}
|
|
<div className="flex gap-3 mb-4">
|
|
{!manualTeamMode && (
|
|
<>
|
|
<button
|
|
onClick={handleGenerateSchedule}
|
|
disabled={isGenerating || participants.length < 2}
|
|
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:opacity-50 text-sm"
|
|
>
|
|
{isGenerating ? "Generating..." : `Generate Schedule (${participants.length} participants)`}
|
|
</button>
|
|
|
|
{teams.length > 0 && (
|
|
<button
|
|
onClick={handleDeleteTeams}
|
|
disabled={isGenerating}
|
|
className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 disabled:opacity-50 text-sm"
|
|
>
|
|
Delete Schedule
|
|
</button>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{manualTeamMode && teams.length > 0 && (
|
|
<button
|
|
onClick={handleSaveManualTeams}
|
|
disabled={isGenerating}
|
|
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:opacity-50 text-sm"
|
|
>
|
|
{isGenerating ? "Saving..." : `Save ${teams.length} Team(s)`}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Teams Display */}
|
|
{teams.length > 0 ? (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
{teams.map((team) => (
|
|
<div
|
|
key={`${team.player1.id}-${team.player2.id}`}
|
|
className="bg-gray-50 rounded p-3 flex justify-between items-center"
|
|
>
|
|
<div>
|
|
<p className="font-medium text-gray-900">
|
|
{team.player1.name} + {team.player2.name}
|
|
</p>
|
|
<p className="text-sm text-gray-500">
|
|
{team.teamName || `Team ${team.id}`}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<div className="text-right">
|
|
<p className="text-sm text-gray-500">
|
|
ELO: {team.player1.currentElo} + {team.player2.currentElo} = {team.player1.currentElo + team.player2.currentElo}
|
|
</p>
|
|
</div>
|
|
{manualTeamMode && (
|
|
<button
|
|
onClick={() => handleRemoveTeam(team.id)}
|
|
className="text-red-600 hover:text-red-800 text-sm"
|
|
>
|
|
Remove
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-gray-500">
|
|
{manualTeamMode
|
|
? "No teams created yet. Use the form above to add teams manually."
|
|
: "No teams created yet. Configure options above and click Generate Schedule."}
|
|
</p>
|
|
)}
|
|
|
|
{/* Participants Summary */}
|
|
<div className="mt-6 pt-4 border-t border-gray-200">
|
|
<h3 className="text-sm font-medium text-gray-700 mb-2">
|
|
Available Participants ({participants.length})
|
|
</h3>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
|
{participants.map((player) => (
|
|
<div key={player.id} className="text-sm text-gray-600 bg-gray-100 rounded px-2 py-1">
|
|
{player.name} ({player.currentElo})
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|