diff --git a/e2e/cucumber/features/bracket-visualization.feature b/e2e/cucumber/features/bracket-visualization.feature new file mode 100644 index 0000000..85b1e9e --- /dev/null +++ b/e2e/cucumber/features/bracket-visualization.feature @@ -0,0 +1,37 @@ +Feature: Bracket Visualization + As a tournament admin + I want to see a visual bracket of the tournament schedule + So that I can track tournament progress at a glance + + @happy-path @tournament @issue-8 + Scenario: Tournament admin views bracket with a generated schedule + Given I am logged in as a tournament admin + And a tournament exists with 4 teams + When I go to the tournament schedule page + And I click the "Generate Schedule" button + Then I should see "Generated" + When I go to the tournament detail page + And I click the "Bracket" tab + Then I should see "Tournament Bracket" + And I should see "Round 1" + And I should see "Round 2" + And I should see "Round 3" + And I should see bracket matchup cards + + @happy-path @tournament @issue-8 + Scenario: Bracket shows team names in matchup cards + Given I am logged in as a tournament admin + And a tournament exists with 4 teams + When I go to the tournament schedule page + And I click the "Generate Schedule" button + Then I should see "Generated" + When I go to the tournament detail page + And I click the "Bracket" tab + Then I should see bracket matchup cards with team names + + @happy-path @tournament @issue-8 + Scenario: Bracket tab is not visible without a schedule + Given I am logged in as a tournament admin + And a tournament exists with 4 teams + When I go to the tournament detail page + Then I should not see the "Bracket" tab diff --git a/e2e/cucumber/step-definitions/common-steps.ts b/e2e/cucumber/step-definitions/common-steps.ts index a27c840..2294c0a 100644 --- a/e2e/cucumber/step-definitions/common-steps.ts +++ b/e2e/cucumber/step-definitions/common-steps.ts @@ -748,3 +748,44 @@ Then('I should not see the viewing as banner', async function () { await expect(banner).not.toBeVisible({ timeout: 3000 }); console.log('🌍 Verified viewing as banner is not visible'); }); + +// Bracket Visualization Steps +When('I go to the tournament detail page', async function () { + const tournamentId = world.tournament?.id || 1; + await world.page.goto(`${world.baseURL}/admin/tournaments/${tournamentId}`); + await world.page.waitForLoadState('networkidle'); + await world.page.waitForTimeout(500); + console.log(`🌍 Navigated to tournament detail page: ${tournamentId}`); +}); + +When('I click the {string} tab', async function (tabName: string) { + const tab = world.page.locator(`button:has-text("${tabName}")`); + await tab.click(); + await world.page.waitForTimeout(500); + console.log(`🌍 Clicked "${tabName}" tab`); +}); + +Then('I should see bracket matchup cards', async function () { + const cards = world.page.locator('[data-testid="bracket-matchup"]'); + const count = await cards.count(); + expect(count).toBeGreaterThan(0); + console.log(`🌍 Found ${count} bracket matchup cards`); +}); + +Then('I should see bracket matchup cards with team names', async function () { + const cards = world.page.locator('[data-testid="bracket-matchup"]'); + const count = await cards.count(); + expect(count).toBeGreaterThan(0); + + const firstCard = cards.first(); + const text = await firstCard.textContent(); + expect(text).toBeTruthy(); + expect(text!.length).toBeGreaterThan(2); + console.log(`🌍 Verified bracket matchup cards have team names`); +}); + +Then('I should not see the {string} tab', async function (tabName: string) { + const tab = world.page.locator(`button:has-text("${tabName}")`); + await expect(tab).not.toBeVisible({ timeout: 3000 }); + console.log(`🌍 Verified "${tabName}" tab is not visible`); +}); diff --git a/src/app/admin/tournaments/[id]/page.tsx b/src/app/admin/tournaments/[id]/page.tsx index 3a936bb..77ff413 100644 --- a/src/app/admin/tournaments/[id]/page.tsx +++ b/src/app/admin/tournaments/[id]/page.tsx @@ -7,6 +7,7 @@ import Navigation from "@/components/Navigation" import TeamsSection from "@/components/TeamsSection" import { DeleteTournamentButton } from "@/components/DeleteTournamentButton" import { ScheduleGenerator } from "@/components/ScheduleGenerator" +import { BracketVisualization } from "@/components/BracketVisualization" import MatchEditor from "@/components/MatchEditor" interface PageProps { @@ -420,6 +421,11 @@ export default function TournamentDetailPage({ params }: PageProps) { ) + case "bracket": + return ( + + ) + default: return null } @@ -555,6 +561,18 @@ export default function TournamentDetailPage({ params }: PageProps) { > Results + {hasSchedule && ( + + )} Analytics diff --git a/src/components/BracketVisualization.tsx b/src/components/BracketVisualization.tsx new file mode 100644 index 0000000..f45664d --- /dev/null +++ b/src/components/BracketVisualization.tsx @@ -0,0 +1,168 @@ +"use client" + +interface Player { + id: number + name: string +} + +interface BracketMatchup { + id: number + roundId: number + player1P1: Player | null + player1P2: Player | null + player2P1: Player | null + player2P2: Player | null + match: { id: number; team1Score: number; team2Score: number } | null + bracketPosition: number | null + status: string +} + +interface TournamentRound { + id: number + roundNumber: number + status: string + bracketMatchups: BracketMatchup[] +} + +interface BracketVisualizationProps { + rounds: TournamentRound[] +} + +export function BracketVisualization({ rounds }: BracketVisualizationProps) { + if (rounds.length === 0) { + return ( +
+

No schedule generated yet. Generate a schedule to see the bracket.

+
+ ) + } + + const currentRoundIdx = rounds.findIndex(r => r.status === "in_progress") + const completedRounds = rounds.filter(r => r.status === "completed").length + + return ( +
+
+

Tournament Bracket

+
+ {rounds.length} rounds + {completedRounds} completed +
+
+ +
+
+ {rounds.map((round, roundIdx) => { + const isCurrent = roundIdx === currentRoundIdx + const isCompleted = round.status === "completed" + + return ( +
+ {/* Round Header */} +
+ Round {round.roundNumber} + {isCurrent && ( + (current) + )} +
+ + {/* Matchups */} +
+ {round.bracketMatchups + .sort((a, b) => (a.bracketPosition || 0) - (b.bracketPosition || 0)) + .map((matchup) => ( + + ))} +
+
+ ) + })} +
+
+
+ ) +} + +function MatchupCard({ matchup, isCurrentRound }: { matchup: BracketMatchup; isCurrentRound: boolean }) { + const team1Name = matchup.player1P1 && matchup.player1P2 + ? `${matchup.player1P1.name.split(" ").pop()} & ${matchup.player1P2.name.split(" ").pop()}` + : "TBD" + const team2Name = matchup.player2P1 && matchup.player2P2 + ? `${matchup.player2P1.name.split(" ").pop()} & ${matchup.player2P2.name.split(" ").pop()}` + : "TBD" + + const hasResult = matchup.match !== null + const team1Won = hasResult && matchup.match!.team1Score > matchup.match!.team2Score + const team2Won = hasResult && matchup.match!.team2Score > matchup.match!.team1Score + + const borderColor = isCurrentRound + ? "border-green-400" + : hasResult + ? "border-gray-300" + : "border-gray-200" + + return ( +
+ {/* Team 1 */} +
+ + {team1Name} + + {hasResult && ( + + {matchup.match!.team1Score} + + )} +
+ + {/* Divider */} +
+ + {/* Team 2 */} +
+ + {team2Name} + + {hasResult && ( + + {matchup.match!.team2Score} + + )} +
+ + {/* Status indicator */} + {!hasResult && matchup.status === "pending" && ( +
+ pending +
+ )} +
+ ) +}