import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "~/components/ui/card"; import { Badge } from "~/components/ui/badge"; import { Trophy, Users } from "lucide-react"; interface Match { id: string; round: string; matchNumber: number; participant1Id: string | null; participant2Id: string | null; winnerId: string | null; loserId: string | null; isComplete: boolean; participant1Score: string | null; participant2Score: string | null; participant1?: { id: string; name: string; } | null; participant2?: { id: string; name: string; } | null; winner?: { id: string; name: string; } | null; } interface TeamOwnership { participantId: string; teamName: string; teamId: string; ownerName?: string; // Optional: user's display name } interface PlayoffBracketProps { matches: Match[]; rounds: string[]; // Ordered list of round names teamOwnerships?: TeamOwnership[]; // Which teams own which participants showOwnership?: boolean; // Toggle ownership display title?: string; description?: string; } /** * PlayoffBracket component - Displays playoff bracket with optional ownership hints * * Features: * - Visual bracket display grouped by round * - Shows match winners and scores * - Ownership hints with team avatars and tooltips (Q15) * - Responsive layout */ export function PlayoffBracket({ matches, rounds, teamOwnerships = [], showOwnership = true, title = "Playoff Bracket", description, }: PlayoffBracketProps) { // Create a map of participant ID to ownership info for fast lookup const ownershipMap = new Map(); teamOwnerships.forEach((ownership) => { ownershipMap.set(ownership.participantId, ownership); }); // Group matches by round const matchesByRound = rounds.reduce((acc, round) => { acc[round] = matches.filter((m) => m.round === round); return acc; }, {} as Record); // Get ownership info for a participant const getOwnership = (participantId: string | null): TeamOwnership | null => { if (!participantId || !showOwnership) return null; return ownershipMap.get(participantId) || null; }; // Generate avatar from team name (first letter) const getTeamAvatar = (teamName: string) => { const initial = teamName.charAt(0).toUpperCase(); // Use a simple hash to generate a consistent color const hash = teamName.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0); const colors = [ "bg-blue-500", "bg-green-500", "bg-purple-500", "bg-pink-500", "bg-amber-500", "bg-red-500", "bg-indigo-500", "bg-teal-500", ]; const colorClass = colors[hash % colors.length]; return (
{initial}
); }; // Render a participant row with optional ownership indicator const renderParticipant = ( participant: { id: string; name: string } | null | undefined, participantId: string | null, isWinner: boolean, score: string | null ) => { const ownership = getOwnership(participantId); const participantName = participant?.name || "TBD"; return (
{isWinner && ( )} {participantName}
{score && ( {parseFloat(score)} )} {ownership && (
{getTeamAvatar(ownership.teamName)}
)}
); }; // Count how many matches have ownership info const matchesWithOwnership = matches.filter( (m) => (m.participant1Id && ownershipMap.has(m.participant1Id)) || (m.participant2Id && ownershipMap.has(m.participant2Id)) ).length; return ( {title} {description && {description}} {showOwnership && matchesWithOwnership > 0 && (
{matchesWithOwnership} match{matchesWithOwnership !== 1 ? "es" : ""} with team ownership
)}
{matches.length === 0 ? (

No bracket matches available yet.

Matches will appear here once the bracket is set up.

) : (
{rounds.map((round) => { const roundMatches = matchesByRound[round] || []; if (roundMatches.length === 0) return null; return (

{round}

{roundMatches.length} match{roundMatches.length !== 1 ? "es" : ""}
{roundMatches .sort((a, b) => a.matchNumber - b.matchNumber) .map((match) => { const isComplete = match.isComplete; const participant1IsWinner = match.winnerId === match.participant1Id; const participant2IsWinner = match.winnerId === match.participant2Id; return (
Match {match.matchNumber} {isComplete && ( Complete )}
{renderParticipant( match.participant1, match.participant1Id, participant1IsWinner, match.participant1Score )}
vs
{renderParticipant( match.participant2, match.participant2Id, participant2IsWinner, match.participant2Score )}
); })}
); })}
)}
); }