From 1402c16f21659d8c9b8264738ce427bf32d31752 Mon Sep 17 00:00:00 2001 From: tikaiz Date: Thu, 12 Mar 2026 14:33:52 +0100 Subject: [PATCH] added tournament_bracket_page --- docs/prompt.md | 5 + .../lib/pages/tournament_bracket_page.dart | 245 ++++++++++++++++++ .../lib/pages/tournament_detail_page.dart | 11 +- 3 files changed, 257 insertions(+), 4 deletions(-) create mode 100644 frontend_splatournament_manager/lib/pages/tournament_bracket_page.dart diff --git a/docs/prompt.md b/docs/prompt.md index fb83464..d56043f 100644 --- a/docs/prompt.md +++ b/docs/prompt.md @@ -95,3 +95,8 @@ Folgende Dateien wurden in diesem Prompt verändert: - Only allow entering a tournament if the registration period is open currently.

Folgende Dateien wurden in diesem Prompt verändert: - frontend_splatournament_manager/lib/pages/tournament_detail_page.dart + +- Add the ongoing view where teams are displayed in a bracket layout: single elimination, supporting 2/4/8 teams with horizontally scrollable rounds connected by bracket lines.

+Folgende Dateien wurden in diesem Prompt verändert: + - frontend_splatournament_manager/lib/pages/tournament_bracket_page.dart (neu erstellt) + - frontend_splatournament_manager/lib/pages/tournament_detail_page.dart diff --git a/frontend_splatournament_manager/lib/pages/tournament_bracket_page.dart b/frontend_splatournament_manager/lib/pages/tournament_bracket_page.dart new file mode 100644 index 0000000..11af7c4 --- /dev/null +++ b/frontend_splatournament_manager/lib/pages/tournament_bracket_page.dart @@ -0,0 +1,245 @@ +import 'package:flutter/material.dart'; +import 'package:frontend_splatournament_manager/models/team.dart'; +import 'package:frontend_splatournament_manager/models/tournament.dart'; +import 'package:frontend_splatournament_manager/providers/team_provider.dart'; +import 'package:provider/provider.dart'; + +class TournamentBracketPage extends StatelessWidget { + final Tournament tournament; + + const TournamentBracketPage({super.key, required this.tournament}); + + int _bracketSize(int n) { + if (n <= 2) return 2; + if (n <= 4) return 4; + return 8; + } + + int _roundCount(int bracketSize) { + if (bracketSize == 2) return 2; + if (bracketSize == 4) return 3; + return 4; + } + + @override + Widget build(BuildContext context) { + final teamProvider = Provider.of(context, listen: false); + + return Scaffold( + appBar: AppBar(title: Text(tournament.name)), + body: FutureBuilder>( + future: teamProvider.getTeamsByTournament(tournament.id), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } + + final teams = snapshot.data ?? []; + final bracketSize = _bracketSize(tournament.maxTeamAmount); + final roundCount = _roundCount(bracketSize); + + return Scrollbar( + thumbVisibility: true, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.all(24), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: _BracketBoard( + teams: teams, + bracketSize: bracketSize, + roundCount: roundCount, + ), + ), + ), + ); + }, + ), + ); + } +} + +class _BracketBoard extends StatelessWidget { + final List teams; + final int bracketSize; + final int roundCount; + + static const double _cardWidth = 112; + static const double _cardHeight = 96; + static const double _baseSlotHeight = 128; + static const double _connectorWidth = 48; + static const double _headerHeight = 28; + static const double _lineThickness = 2; + + const _BracketBoard({ + required this.teams, + required this.bracketSize, + required this.roundCount, + }); + + String _roundLabel(int round) { + if (round == roundCount - 1) return 'Winner'; + final teamsInRound = bracketSize ~/ (1 << round); + if (teamsInRound == 2) return 'Final'; + if (teamsInRound == 4) return 'Semi-finals'; + return 'Quarter-finals'; + } + + double _cardTop(int round, int index) { + final slotHeight = _baseSlotHeight * (1 << round).toDouble(); + return _headerHeight + index * slotHeight + (slotHeight - _cardHeight) / 2; + } + + double _cardCenterY(int round, int index) => + _cardTop(round, index) + _cardHeight / 2; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final lineColor = colorScheme.outlineVariant; + + final boardWidth = + roundCount * _cardWidth + (roundCount - 1) * _connectorWidth; + final boardHeight = _headerHeight + bracketSize * _baseSlotHeight; + + final children = []; + + for (int round = 0; round < roundCount; round++) { + final cardsInRound = bracketSize ~/ (1 << round); + final left = round * (_cardWidth + _connectorWidth); + + children.add( + Positioned( + left: left, + top: 0, + width: _cardWidth, + height: _headerHeight, + child: Center( + child: Text( + _roundLabel(round), + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: colorScheme.primary, + letterSpacing: 0.3, + ), + ), + ), + ), + ); + + for (int i = 0; i < cardsInRound; i++) { + final String? label = (round == 0 && i < teams.length) + ? teams[i].name + : null; + children.add( + Positioned( + left: left, + top: _cardTop(round, i), + width: _cardWidth, + height: _cardHeight, + child: _TeamCard(label: label), + ), + ); + } + + if (round == roundCount - 1) { + continue; + } + + final connectorLeft = left + _cardWidth; + final matches = cardsInRound ~/ 2; + final halfConnector = _connectorWidth / 2; + + for (int i = 0; i < matches; i++) { + final yTop = _cardCenterY(round, i * 2); + final yBottom = _cardCenterY(round, i * 2 + 1); + final yMiddle = (yTop + yBottom) / 2; + + children.add( + Positioned( + left: connectorLeft, + top: yTop - _lineThickness / 2, + width: halfConnector, + height: _lineThickness, + child: DecoratedBox(decoration: BoxDecoration(color: lineColor)), + ), + ); + + children.add( + Positioned( + left: connectorLeft, + top: yBottom - _lineThickness / 2, + width: halfConnector, + height: _lineThickness, + child: DecoratedBox(decoration: BoxDecoration(color: lineColor)), + ), + ); + + children.add( + Positioned( + left: connectorLeft + halfConnector - _lineThickness / 2, + top: yTop, + width: _lineThickness, + height: yBottom - yTop, + child: DecoratedBox(decoration: BoxDecoration(color: lineColor)), + ), + ); + + children.add( + Positioned( + left: connectorLeft + halfConnector, + top: yMiddle - _lineThickness / 2, + width: halfConnector, + height: _lineThickness, + child: DecoratedBox(decoration: BoxDecoration(color: lineColor)), + ), + ); + } + } + + return SizedBox( + width: boardWidth, + height: boardHeight, + child: Stack(children: children), + ); + } +} + +class _TeamCard extends StatelessWidget { + final String? label; + + const _TeamCard({this.label}); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final isTbd = label == null; + + return Card( + elevation: isTbd ? 1 : 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + isTbd ? '?' : label!, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13, + fontWeight: isTbd ? FontWeight.w400 : FontWeight.w600, + color: isTbd + ? colorScheme.onSurface.withValues(alpha: 0.38) + : colorScheme.onSurface, + ), + ), + ), + ), + ); + } +} diff --git a/frontend_splatournament_manager/lib/pages/tournament_detail_page.dart b/frontend_splatournament_manager/lib/pages/tournament_detail_page.dart index d65984a..0511535 100644 --- a/frontend_splatournament_manager/lib/pages/tournament_detail_page.dart +++ b/frontend_splatournament_manager/lib/pages/tournament_detail_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:frontend_splatournament_manager/models/team.dart'; import 'package:frontend_splatournament_manager/models/tournament.dart'; +import 'package:frontend_splatournament_manager/pages/tournament_bracket_page.dart'; import 'package:frontend_splatournament_manager/providers/team_provider.dart'; import 'package:provider/provider.dart'; @@ -322,10 +323,12 @@ class TournamentContentWidget extends StatelessWidget { width: double.infinity, child: ElevatedButton( onPressed: () { - //TODO: Redirect to Ongoing View - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("ongoing clicked")), + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + TournamentBracketPage(tournament: tournament), + ), ); }, child: Text("View ongoing"),