Files
Ivan 23ee47fe47 feat(tournaments): add tournament management UI
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>
2026-02-01 07:15:04 +00:00

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>
);
}