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>
This commit is contained in:
175
apps/web/components/tournaments/tournament-card.tsx
Normal file
175
apps/web/components/tournaments/tournament-card.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { formatCurrency, formatDate, 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;
|
||||
};
|
||||
}
|
||||
|
||||
interface TournamentCardProps {
|
||||
tournament: Tournament;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const statusConfig: Record<string, { label: string; className: string }> = {
|
||||
DRAFT: {
|
||||
label: "Borrador",
|
||||
className: "bg-gray-100 text-gray-700 border-gray-300",
|
||||
},
|
||||
REGISTRATION_OPEN: {
|
||||
label: "Inscripciones Abiertas",
|
||||
className: "bg-green-100 text-green-700 border-green-300",
|
||||
},
|
||||
REGISTRATION_CLOSED: {
|
||||
label: "Inscripciones Cerradas",
|
||||
className: "bg-yellow-100 text-yellow-700 border-yellow-300",
|
||||
},
|
||||
IN_PROGRESS: {
|
||||
label: "En Progreso",
|
||||
className: "bg-blue-100 text-blue-700 border-blue-300",
|
||||
},
|
||||
COMPLETED: {
|
||||
label: "Finalizado",
|
||||
className: "bg-purple-100 text-purple-700 border-purple-300",
|
||||
},
|
||||
CANCELLED: {
|
||||
label: "Cancelado",
|
||||
className: "bg-red-100 text-red-700 border-red-300",
|
||||
},
|
||||
};
|
||||
|
||||
const typeLabels: Record<string, string> = {
|
||||
BRACKET: "Eliminacion Directa",
|
||||
AMERICANO: "Americano",
|
||||
MEXICANO: "Mexicano",
|
||||
ROUND_ROBIN: "Round Robin",
|
||||
LEAGUE: "Liga",
|
||||
};
|
||||
|
||||
export function TournamentCard({ tournament, onClick }: TournamentCardProps) {
|
||||
const status = statusConfig[tournament.status] || statusConfig.DRAFT;
|
||||
const typeLabel = typeLabels[tournament.type] || tournament.type;
|
||||
const category = tournament.settings?.category;
|
||||
const entryFee = typeof tournament.entryFee === 'string'
|
||||
? parseFloat(tournament.entryFee)
|
||||
: tournament.entryFee;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={cn(
|
||||
"cursor-pointer transition-all hover:shadow-lg hover:border-primary-400",
|
||||
"relative overflow-hidden"
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<CardTitle className="text-lg line-clamp-2">{tournament.name}</CardTitle>
|
||||
<span
|
||||
className={cn(
|
||||
"px-2 py-1 text-xs font-medium rounded-full border shrink-0",
|
||||
status.className
|
||||
)}
|
||||
>
|
||||
{status.label}
|
||||
</span>
|
||||
</div>
|
||||
{tournament.description && (
|
||||
<p className="text-sm text-primary-500 line-clamp-2 mt-1">
|
||||
{tournament.description}
|
||||
</p>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{/* Type and Category */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="px-2 py-1 text-xs bg-primary-100 text-primary-700 rounded">
|
||||
{typeLabel}
|
||||
</span>
|
||||
{category && (
|
||||
<span className="px-2 py-1 text-xs bg-accent-100 text-accent-700 rounded">
|
||||
{category}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Date */}
|
||||
<div className="flex items-center gap-2 text-sm text-primary-600">
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{formatDate(tournament.startDate)}</span>
|
||||
{tournament.endDate && (
|
||||
<>
|
||||
<span>-</span>
|
||||
<span>{formatDate(tournament.endDate)}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Inscriptions and Price */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<svg
|
||||
className="w-4 h-4 text-primary-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-primary-700">
|
||||
{tournament._count.inscriptions}
|
||||
{tournament.maxPlayers && ` / ${tournament.maxPlayers}`}
|
||||
{" equipos"}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-semibold text-primary-800">
|
||||
{entryFee > 0 ? formatCurrency(entryFee) : "Gratis"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Site name */}
|
||||
<div className="pt-2 border-t border-primary-100">
|
||||
<span className="text-xs text-primary-500">{tournament.site.name}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user