Add complete tournament management interface including: - Tournament card component with status badges and info display - Tournament form for creating/editing tournaments - Bracket view for single elimination visualization - Inscriptions list with payment tracking - Match score dialog for entering results - Tournaments list page with filtering and search - Tournament detail page with tabs (Overview, Inscriptions, Bracket) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
285 lines
8.5 KiB
TypeScript
285 lines
8.5 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useCallback } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { TournamentCard } from "@/components/tournaments/tournament-card";
|
|
import { TournamentForm } from "@/components/tournaments/tournament-form";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface Tournament {
|
|
id: string;
|
|
name: string;
|
|
description: string | null;
|
|
type: string;
|
|
status: string;
|
|
startDate: string;
|
|
endDate: string | null;
|
|
maxPlayers: number | null;
|
|
entryFee: number | string;
|
|
settings: {
|
|
tournamentFormat?: string;
|
|
category?: string | null;
|
|
} | null;
|
|
site: {
|
|
id: string;
|
|
name: string;
|
|
};
|
|
_count: {
|
|
inscriptions: number;
|
|
matches: number;
|
|
};
|
|
}
|
|
|
|
const statusFilters = [
|
|
{ value: "", label: "Todos" },
|
|
{ value: "DRAFT", label: "Borrador" },
|
|
{ value: "REGISTRATION_OPEN", label: "Inscripciones Abiertas" },
|
|
{ value: "IN_PROGRESS", label: "En Progreso" },
|
|
{ value: "COMPLETED", label: "Finalizados" },
|
|
{ value: "CANCELLED", label: "Cancelados" },
|
|
];
|
|
|
|
export default function TournamentsPage() {
|
|
const router = useRouter();
|
|
const [tournaments, setTournaments] = useState<Tournament[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [statusFilter, setStatusFilter] = useState("");
|
|
const [showForm, setShowForm] = useState(false);
|
|
const [formLoading, setFormLoading] = useState(false);
|
|
|
|
const fetchTournaments = useCallback(async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const params = new URLSearchParams();
|
|
if (statusFilter) {
|
|
params.append("status", statusFilter);
|
|
}
|
|
|
|
const response = await fetch(`/api/tournaments?${params.toString()}`);
|
|
if (!response.ok) {
|
|
throw new Error("Error al cargar torneos");
|
|
}
|
|
const data = await response.json();
|
|
setTournaments(data);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Error desconocido");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [statusFilter]);
|
|
|
|
useEffect(() => {
|
|
fetchTournaments();
|
|
}, [fetchTournaments]);
|
|
|
|
// Filter tournaments by search query (client-side)
|
|
const filteredTournaments = tournaments.filter((tournament) => {
|
|
if (!searchQuery) return true;
|
|
const query = searchQuery.toLowerCase();
|
|
return (
|
|
tournament.name.toLowerCase().includes(query) ||
|
|
tournament.description?.toLowerCase().includes(query) ||
|
|
tournament.site.name.toLowerCase().includes(query)
|
|
);
|
|
});
|
|
|
|
const handleCreateTournament = async (data: {
|
|
name: string;
|
|
description: string;
|
|
date: string;
|
|
endDate: string;
|
|
type: string;
|
|
category: string;
|
|
maxTeams: number;
|
|
price: number;
|
|
siteId: string;
|
|
}) => {
|
|
setFormLoading(true);
|
|
try {
|
|
const response = await fetch("/api/tournaments", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
...data,
|
|
date: new Date(data.date).toISOString(),
|
|
endDate: data.endDate ? new Date(data.endDate).toISOString() : undefined,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || "Error al crear torneo");
|
|
}
|
|
|
|
const newTournament = await response.json();
|
|
setShowForm(false);
|
|
router.push(`/tournaments/${newTournament.id}`);
|
|
} catch (err) {
|
|
throw err;
|
|
} finally {
|
|
setFormLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleTournamentClick = (tournamentId: string) => {
|
|
router.push(`/tournaments/${tournamentId}`);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-primary-800">Torneos</h1>
|
|
<p className="mt-1 text-primary-600">
|
|
Gestiona torneos y competiciones de tu club
|
|
</p>
|
|
</div>
|
|
<Button onClick={() => setShowForm(true)}>
|
|
<svg
|
|
className="w-5 h-5 mr-2"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M12 4v16m8-8H4"
|
|
/>
|
|
</svg>
|
|
Nuevo Torneo
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<div className="flex flex-col sm:flex-row gap-4">
|
|
{/* Search */}
|
|
<div className="flex-1">
|
|
<Input
|
|
type="text"
|
|
placeholder="Buscar por nombre..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Status Filter */}
|
|
<div className="flex gap-2 overflow-x-auto pb-2 sm:pb-0">
|
|
{statusFilters.map((filter) => (
|
|
<Button
|
|
key={filter.value}
|
|
variant={statusFilter === filter.value ? "default" : "outline"}
|
|
size="sm"
|
|
onClick={() => setStatusFilter(filter.value)}
|
|
className="whitespace-nowrap"
|
|
>
|
|
{filter.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Create Form Modal */}
|
|
{showForm && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 overflow-y-auto">
|
|
<div className="my-8">
|
|
<TournamentForm
|
|
onSubmit={handleCreateTournament}
|
|
onCancel={() => setShowForm(false)}
|
|
isLoading={formLoading}
|
|
mode="create"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Loading State */}
|
|
{loading && (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-2"></div>
|
|
<p className="text-primary-500">Cargando torneos...</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Error State */}
|
|
{error && !loading && (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="text-center text-red-500">
|
|
<p>{error}</p>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="mt-2"
|
|
onClick={fetchTournaments}
|
|
>
|
|
Reintentar
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Empty State */}
|
|
{!loading && !error && filteredTournaments.length === 0 && (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="text-center text-primary-500">
|
|
<svg
|
|
className="w-12 h-12 mx-auto mb-3 text-primary-300"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
<p className="font-medium">No hay torneos</p>
|
|
<p className="text-sm mt-1">
|
|
{searchQuery || statusFilter
|
|
? "Intenta con otros filtros"
|
|
: "Crea tu primer torneo para comenzar"}
|
|
</p>
|
|
{!searchQuery && !statusFilter && (
|
|
<Button className="mt-4" onClick={() => setShowForm(true)}>
|
|
Crear Torneo
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Tournaments Grid */}
|
|
{!loading && !error && filteredTournaments.length > 0 && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{filteredTournaments.map((tournament) => (
|
|
<TournamentCard
|
|
key={tournament.id}
|
|
tournament={tournament}
|
|
onClick={() => handleTournamentClick(tournament.id)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Results count */}
|
|
{!loading && !error && filteredTournaments.length > 0 && (
|
|
<div className="text-center text-sm text-primary-500">
|
|
Mostrando {filteredTournaments.length} de {tournaments.length} torneos
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|