diff --git a/tournaments/admin.py b/tournaments/admin.py index cf03512..2335c25 100644 --- a/tournaments/admin.py +++ b/tournaments/admin.py @@ -59,23 +59,47 @@ class TeamScoreAdmin(SyncedObjectAdmin): list_display = ['team_registration', 'score', 'walk_out', 'match'] list_filter = [TeamScoreTournamentListFilter] search_fields = ['id'] + raw_id_fields = ['team_registration', 'match'] # Add this line + list_per_page = 50 # Controls pagination on the list view + + def get_queryset(self, request): + qs = super().get_queryset(request) + return qs.select_related('team_registration', 'match') class RoundAdmin(SyncedObjectAdmin): list_display = ['tournament', 'name', 'parent', 'index'] list_filter = [SimpleTournamentListFilter, SimpleIndexListFilter] search_fields = ['id'] ordering = ['parent', 'index'] + raw_id_fields = ['parent'] # Add this line + list_per_page = 50 # Controls pagination on the list view + + def get_queryset(self, request): + qs = super().get_queryset(request) + return qs.select_related('parent') class PlayerRegistrationAdmin(SyncedObjectAdmin): list_display = ['first_name', 'last_name', 'licence_id', 'rank'] search_fields = ('id', 'first_name', 'last_name', 'licence_id__icontains') list_filter = ['registered_online', TeamScoreTournamentListFilter] ordering = ['last_name', 'first_name'] + raw_id_fields = ['team_registration'] # Add this line + list_per_page = 50 # Controls pagination on the list view + + def get_queryset(self, request): + qs = super().get_queryset(request) + return qs.select_related('team_registration') class MatchAdmin(SyncedObjectAdmin): list_display = ['__str__', 'round', 'group_stage', 'start_date', 'end_date', 'index'] list_filter = [MatchTypeListFilter, MatchTournamentListFilter, SimpleIndexListFilter] ordering = ['-group_stage', 'round', 'index'] + raw_id_fields = ['round', 'group_stage'] # Add this line + list_per_page = 50 # Controls pagination on the list view + + def get_queryset(self, request): + qs = super().get_queryset(request) + return qs.select_related('round', 'group_stage') class GroupStageAdmin(SyncedObjectAdmin): list_display = ['tournament', 'start_date', 'index'] diff --git a/tournaments/models/group_stage.py b/tournaments/models/group_stage.py index 6829404..a47eedf 100644 --- a/tournaments/models/group_stage.py +++ b/tournaments/models/group_stage.py @@ -6,6 +6,7 @@ import uuid from ..utils.extensions import format_seconds # from datetime import datetime, timedelta from django.utils import timezone +from django.db.models import Count, Q class GroupStage(SideStoreModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) @@ -151,12 +152,14 @@ class GroupStage(SideStoreModel): def has_at_least_one_started_match(self): for match in self.matches.all(): - if match.start_date is not None and match.start_date <= timezone.now(): + if match.start_date is not None and match.start_date <= timezone.now() and match.confirmed is True: return True return False def is_completed(self): - return not self.matches.filter(end_date__isnull=True).exists() and self.matches.count() > 0 + # This will use the prefetched matches if available + matches = list(self.matches.all()) + return len(matches) > 0 and all(match.end_date is not None for match in matches) class LiveGroupStage: def __init__(self, title, step, index): @@ -190,7 +193,8 @@ class LiveGroupStage: return { "title": self.title, "teams": [team.to_dict() for team in self.teams], - "duration": self.formatted_duration() + "duration": self.formatted_duration(), + "started": self.started() } def started(self): diff --git a/tournaments/models/match.py b/tournaments/models/match.py index ac9ee29..110bd84 100644 --- a/tournaments/models/match.py +++ b/tournaments/models/match.py @@ -191,38 +191,51 @@ class Match(SideStoreModel): loser_bottom_match = self.loser_precedent_match(False) if len(team_scores) == 0: + if (self.round and self.round.parent is None and self.round.tournament.rounds.filter(parent__isnull=True, group_stage_loser_bracket=False).count() - 1 == self.round.index): - names = ["Qualifié", ''] + names = ["Qualifié"] team = self.default_live_team(names) teams.append(team) - names = ["Qualifié", ''] + names = ["Qualifié"] team = self.default_live_team(names) teams.append(team) return teams if (self.group_stage): - names = ["Équipe de poule", ''] + names = ["Équipe de poule"] team = self.default_live_team(names) teams.append(team) - names = ["Équipe de poule", ''] + names = ["Équipe de poule"] team = self.default_live_team(names) teams.append(team) return teams elif self.round and self.round.parent: if loser_top_match: - names = [f"Perdant {loser_top_match.computed_name()}", ''] + names = [f"Perdant {loser_top_match.computed_name()}"] team = self.default_live_team(names) teams.append(team) if loser_bottom_match: - names = [f"Perdant {loser_bottom_match.computed_name()}", ''] + names = [f"Perdant {loser_bottom_match.computed_name()}"] + team = self.default_live_team(names) + teams.append(team) + if previous_top_match: + names = [f"Gagnant {previous_top_match.computed_name()}"] + if previous_top_match.disabled: + names = ["Perdant tableau principal"] + team = self.default_live_team(names) + teams.append(team) + if previous_bottom_match: + names = [f"Gagnant {previous_bottom_match.computed_name()}"] + if previous_bottom_match.disabled: + names = ["Perdant tableau principal"] team = self.default_live_team(names) teams.append(team) elif self.round and self.round.parent is None: if previous_top_match: - names = [f"Gagnant {previous_top_match.computed_name()}", ''] + names = [f"Gagnant {previous_top_match.computed_name()}"] team = self.default_live_team(names) teams.append(team) if previous_bottom_match: - names = [f"Gagnant {previous_bottom_match.computed_name()}", ''] + names = [f"Gagnant {previous_bottom_match.computed_name()}"] team = self.default_live_team(names) teams.append(team) elif len(team_scores) == 1: @@ -230,33 +243,33 @@ class Match(SideStoreModel): existing_team = team_scores[0].live_team(self) if (self.group_stage): teams.append(existing_team) - names = ["Équipe de poule", ''] + names = ["Équipe de poule"] team = self.default_live_team(names) teams.append(team) elif self.round: if loser_top_match and loser_top_match.disabled == False and loser_top_match.end_date is None: - names = [f"Perdant {loser_top_match.computed_name()}", ''] + names = [f"Perdant {loser_top_match.computed_name()}"] team = self.default_live_team(names) teams.append(team) teams.append(existing_team) elif loser_bottom_match: - names = [f"Perdant {loser_bottom_match.computed_name()}", ''] + names = [f"Perdant {loser_bottom_match.computed_name()}"] team = self.default_live_team(names) teams.append(existing_team) teams.append(team) elif previous_top_match and previous_top_match.disabled == False and previous_top_match.end_date is None: - names = [f"Gagnant {previous_top_match.computed_name()}", ''] + names = [f"Gagnant {previous_top_match.computed_name()}"] team = self.default_live_team(names) teams.append(team) teams.append(existing_team) elif previous_bottom_match: - names = [f"Gagnant {previous_bottom_match.computed_name()}", ''] + names = [f"Gagnant {previous_bottom_match.computed_name()}"] team = self.default_live_team(names) teams.append(existing_team) teams.append(team) elif (self.round.parent is None and self.round.tournament.rounds.filter(parent__isnull=True, group_stage_loser_bracket=False).count() - 1 == self.round.index): match_index_within_round = self.index - (int(2 ** self.round.index) - 1) - names = ["Qualifié", ''] + names = ["Qualifié"] team = self.default_live_team(names) if match_index_within_round < int(2 ** self.round.index) / 2: teams.append(existing_team) @@ -434,7 +447,10 @@ class Match(SideStoreModel): class Team: def __init__(self, id, image, names, scores, weight, is_winner, walk_out, is_lucky_loser): # print(f"image = {image}, names= {names}, scores ={scores}, weight={weight}, win={is_winner}") - self.id = str(id) + if id is not None: + self.id = str(id) + else: + self.id = None self.image = image self.names = names self.scores = scores @@ -455,7 +471,8 @@ class Team: "is_winner": self.is_winner, "walk_out": self.walk_out, "is_walk_out": self.is_walk_out(), - "is_lucky_loser": self.is_lucky_loser + "is_lucky_loser": self.is_lucky_loser, + "id": self.id } class LiveMatch: diff --git a/tournaments/models/round.py b/tournaments/models/round.py index 8214bc3..54d1c62 100644 --- a/tournaments/models/round.py +++ b/tournaments/models/round.py @@ -34,10 +34,12 @@ class Round(SideStoreModel): return self.tournament.id def name(self): - if self.parent: - return "Matchs de classement" + if self.parent and self.parent.parent is None: + return f"Classement {self.parent.name()}" + elif self.parent and self.parent.parent is not None: + return f"{self.parent.name()}" elif self.group_stage_loser_bracket is True: - return "Matchs de classement de poule" + return "Classement de poule" else: if self.index == 0: return "Finale" @@ -103,3 +105,67 @@ class Round(SideStoreModel): return False return True + + def prepare_match_group(self, next_round, parent_round, loser_final, double_butterfly_mode): + matches = self.match_set.filter(disabled=False).order_by('index') + if len(matches) == 0: + return None + if next_round: + next_round_matches = next_round.match_set.filter(disabled=False).order_by('index') + else: + next_round_matches = [] + + if len(matches) < len(next_round_matches): + all_matches = self.match_set.order_by('index') + filtered_matches = [] + + # Process matches in pairs + i = 0 + while i < len(all_matches): + # Get the current match and its pair (if available) + current_match = all_matches[i] + pair_match = all_matches[i+1] if i+1 < len(all_matches) else None + + # Only filter out the pair if both matches are disabled + if current_match.disabled and pair_match and pair_match.disabled: + # Skip one of the matches in the pair + filtered_matches.append(current_match) + pass + else: + # Keep the current match + if current_match.disabled == False: + filtered_matches.append(current_match) + # If there's a pair match, keep it too + if pair_match and pair_match.disabled == False: + filtered_matches.append(pair_match) + + # Move to the next pair + i += 2 + + # Replace the matches list with our filtered list + matches = filtered_matches + + if matches: + if len(matches) > 1 and double_butterfly_mode: + midpoint = int(len(matches) / 2) + first_half_matches = matches[:midpoint] + else: + first_half_matches = list(matches) # Convert QuerySet to a list + if self.index == 0 and loser_final: + loser_match = loser_final.match_set.first() + if loser_match: + first_half_matches.append(loser_match) + + + if first_half_matches: + name = self.name() + if parent_round and first_half_matches[0].name is not None: + name = first_half_matches[0].name + match_group = self.tournament.create_match_group( + name=name, + matches=first_half_matches, + round_id=self.id + ) + return match_group + + return None diff --git a/tournaments/models/team_registration.py b/tournaments/models/team_registration.py index e4ef14e..4364767 100644 --- a/tournaments/models/team_registration.py +++ b/tournaments/models/team_registration.py @@ -55,27 +55,27 @@ class TeamRegistration(SideStoreModel): def player_names_as_list(self): players = list(self.player_registrations.all()) if len(players) == 0: - return ['', ''] + return [] elif len(players) == 1: - return [players[0].name(), ''] + return [players[0].name()] else: return [pr.name() for pr in players] def team_names(self): if self.name: - return [self.name, ''] #add an empty line if it's a team name + return [self.name] #add an empty line if it's a team name else: return self.player_names_as_list() def shortened_team_names(self): if self.name: - return [self.name, ''] #add an empty line if it's a team name + return [self.name] #add an empty line if it's a team name else: players = list(self.player_registrations.all()) if len(players) == 0: - return ['', ''] + return [] elif len(players) == 1: - return [players[0].shortened_name(), ''] + return [players[0].shortened_name()] else: return [pr.shortened_name() for pr in players] diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index 4e72216..67e3a05 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -184,7 +184,7 @@ class Tournament(BaseModel): return self.get_federal_age_category_display() def formatted_start_date(self): - return self.start_date.strftime("%d/%m/%y") + return self.local_start_date().strftime("%d/%m/%y") def in_progress(self): return self.end_date is None @@ -549,7 +549,7 @@ class Tournament(BaseModel): return groups - def create_match_group(self, name, matches): + def create_match_group(self, name, matches, round_id=None): matches = list(matches) live_matches = [match.live_match() for match in matches] # Filter out matches that have a start_date of None @@ -566,7 +566,7 @@ class Tournament(BaseModel): time_format = 'l d M' formatted_schedule = f" - {formats.date_format(local_start, format=time_format)}" - return MatchGroup(name, live_matches, formatted_schedule) + return MatchGroup(name, live_matches, formatted_schedule, round_id) def live_group_stages(self): group_stages = self.sorted_group_stages() @@ -617,7 +617,11 @@ class Tournament(BaseModel): # if now is before the first match, we want to show the summons + group stage or first matches # change timezone to datetime to avoid the bug RuntimeWarning: DateTimeField Tournament.start_date received a naive datetime (2024-05-16 00:00:00) while time zone support is active. - if timezone.now() < self.start_date: + current_time = timezone.now() + tournament_start = self.local_start_date() + one_hour_before_start = tournament_start - timedelta(hours=1) + + if current_time < one_hour_before_start: team_summons_dicts = [summon.to_dict() for summon in self.team_summons()] if group_stages: return { @@ -693,6 +697,12 @@ class Tournament(BaseModel): if previous_round: # print('previous_round') matches.extend(previous_round.get_matches_recursive(True)) + + previous_previous_round = self.round_for_index(current_round.index + 2) + if previous_previous_round: + previous_previous_matches = previous_previous_round.get_matches_recursive(True) + previous_previous_matches = [m for m in previous_previous_matches if m.end_date is None] + matches.extend(previous_previous_matches) else: # print('group_stages') group_stages = [gs.live_group_stages() for gs in self.last_group_stage_step()] @@ -800,8 +810,18 @@ class Tournament(BaseModel): group_stages = self.elected_broadcast_group_stages() group_stages.sort(key=lambda gs: (gs.index, gs.start_date is None, gs.start_date)) for group_stage in group_stages: + matches.extend(group_stage.matches.all()) - matches = [m for m in matches if m.should_appear()] + + if len(matches) > 16: + # if more than 16 groupstage matches + now = timezone.now() + future_threshold = now + timedelta(hours=1) + past_threshold = now - timedelta(hours=1) + matches = [m for m in matches if m.should_appear() and + (m.start_date is None or m.start_date <= future_threshold) and # Not starting in more than 1h + (m.end_date is None or m.end_date >= past_threshold)] # Not finished for more than 1h + matches = matches[:16] matches.sort(key=lambda m: (m.start_date is None, m.end_date is not None, m.start_date, m.index)) group_stage_loser_bracket = list(self.rounds.filter(parent=None, group_stage_loser_bracket=True).all()) @@ -836,7 +856,7 @@ class Tournament(BaseModel): if self.end_date is not None: return is_build_and_not_empty - if timezone.now() >= timezone.localtime(self.start_date): + if timezone.now() >= self.local_start_date(): return is_build_and_not_empty minimum_publish_date = self.creation_date.replace(hour=9, minute=0) + timedelta(days=1) return timezone.now() >= timezone.localtime(minimum_publish_date) @@ -846,7 +866,7 @@ class Tournament(BaseModel): return self.has_team_registrations() if self.publish_teams: return self.has_team_registrations() - if timezone.now().date() >= self.start_date.date(): + if timezone.now() >= self.local_start_date(): return self.has_team_registrations() return False @@ -858,7 +878,7 @@ class Tournament(BaseModel): return False if self.publish_summons: return self.has_summons() - if timezone.now() >= self.start_date: + if timezone.now() >= self.local_start_date(): return self.has_summons() return False @@ -872,7 +892,7 @@ class Tournament(BaseModel): first_group_stage_start_date = self.group_stage_start_date() if first_group_stage_start_date is None: - return timezone.now() >= self.start_date + return timezone.now() >= self.local_start_date() else: return timezone.now() >= first_group_stage_start_date @@ -881,7 +901,8 @@ class Tournament(BaseModel): if len(group_stages) == 0: return None - return min(group_stages, key=lambda gs: gs.start_date).start_date + timezone = self.timezone() + return min(group_stages, key=lambda gs: gs.start_date).start_date.astimezone(timezone) def display_matches(self): if self.end_date is not None: @@ -894,12 +915,12 @@ class Tournament(BaseModel): first_match_start_date = self.first_match_start_date(bracket_matches) if first_match_start_date is None: - return timezone.now() >= self.start_date + return timezone.now() >= self.local_start_date() bracket_start_date = self.getEightAm(first_match_start_date) - if bracket_start_date < self.start_date: - bracket_start_date = self.start_date + if bracket_start_date < self.local_start_date(): + bracket_start_date = self.local_start_date() group_stage_start_date = self.group_stage_start_date() if group_stage_start_date is not None: @@ -922,8 +943,7 @@ class Tournament(BaseModel): matches = [m for m in bracket_matches if m.start_date is not None] if len(matches) == 0: return None - - return min(matches, key=lambda m: m.start_date).start_date + return min(matches, key=lambda m: m.start_date).local_start_date() def getEightAm(self, date): return date.replace(hour=8, minute=0, second=0, microsecond=0, tzinfo=date.tzinfo) @@ -932,7 +952,7 @@ class Tournament(BaseModel): # end = self.start_date + timedelta(days=self.day_duration + 1) # return self.start_date.replace(hour=0, minute=0) <= timezone.now() <= end - timezoned_datetime = timezone.localtime(self.start_date) + timezoned_datetime = self.local_start_date() end = timezoned_datetime + timedelta(days=self.day_duration + 1) now = timezone.now() @@ -946,33 +966,41 @@ class Tournament(BaseModel): return start <= now <= end + def starts_in_the_future(self): + # tomorrow = datetime.now().date() + timedelta(days=1) + + timezoned_datetime = self.local_start_date() + start = timezoned_datetime.replace(hour=0, minute=0) + now = timezone.now() + + return start >= now + def should_be_over(self): if self.end_date is not None: return True - timezoned_datetime = timezone.localtime(self.start_date) + timezoned_datetime = self.local_start_date() end = timezoned_datetime + timedelta(days=self.day_duration + 1) now = timezone.now() return now >= end and self.is_build_and_not_empty() and self.nearly_over() def nearly_over(self): # First check group stages if they exist - if self.group_stages.count() > 0: + group_stages = list(self.group_stages.all()) # Use prefetched data + if group_stages: # Check if all group stages are completed - for group_stage in self.group_stages.all(): + for group_stage in group_stages: + # Use the is_completed method if group_stage.is_completed(): return True # If no group stages, check semi-finals - if self.rounds.count() > 0: - # Get round with index 1 (semi-finals) and no parent - semifinals = self.rounds.filter(index=1, parent=None).first() - if semifinals: - # Check if any match in semi-finals has started - for match in semifinals.matches.all(): - if match.start_date is not None and match.is_ready(): - return True - return False + semifinals = self.rounds.filter(index=1, parent=None).first() # Use prefetched data + if semifinals: + # Check if any match in semi-finals has started + for match in semifinals.matches.all(): # Use prefetched data + if match.start_date is not None and match.is_ready(): + return True return False @@ -983,7 +1011,18 @@ class Tournament(BaseModel): return self.hide_teams_weight def is_build_and_not_empty(self): - return self.group_stages.count() > 0 or self.rounds.count() > 0 and self.team_registrations.count() >= 4 + if hasattr(self, '_prefetched_objects_cache'): + # Use prefetched data if available + has_group_stages = 'groupstage_set' in self._prefetched_objects_cache and len(self.group_stages.all()) > 0 + has_rounds = 'round_set' in self._prefetched_objects_cache and len(self.rounds.all()) > 0 + has_team_registrations = 'teamregistration_set' in self._prefetched_objects_cache and len(self.team_registrations.all()) >= 4 + else: + # Fall back to database queries if not prefetched + has_group_stages = self.group_stages.count() > 0 + has_rounds = self.rounds.count() > 0 + has_team_registrations = self.team_registrations.count() >= 4 + + return (has_group_stages or has_rounds) and has_team_registrations def day_duration_formatted(self): return plural_format("jour", self.day_duration) @@ -1315,12 +1354,51 @@ class Tournament(BaseModel): return ordered_matches + def get_butterfly_bracket_match_group(self, parent_round=None, double_butterfly_mode=False, display_loser_final=False): + loser_final = None + main_rounds_reversed = [] + + # Get main bracket rounds (excluding children/ranking matches) + main_rounds = self.rounds.filter( + parent=parent_round, + group_stage_loser_bracket=False + ).order_by('-index') + + count = main_rounds.count() + if display_loser_final and count > 1: + semi = main_rounds[count - 2] + loser_final = self.rounds.filter( + parent=semi, + group_stage_loser_bracket=False + ).order_by('index').first() + + # Create serializable match groups data + serializable_match_groups = [] + + # Add first half of each round (from last to semi-finals) + for round in main_rounds: + next_round = main_rounds.filter(index=round.index - 1).first() + match_group = round.prepare_match_group(next_round, parent_round, loser_final, double_butterfly_mode) + if match_group: + serializable_match_groups.append(match_group) + + if double_butterfly_mode: + main_rounds_reversed = list(main_rounds) + main_rounds_reversed.reverse() + for round in main_rounds_reversed: + next_round = main_rounds.filter(index=round.index - 1).first() + match_group = round.prepare_match_group(next_round, parent_round, None, double_butterfly_mode) + if match_group: + serializable_match_groups.append(match_group) + + return serializable_match_groups class MatchGroup: - def __init__(self, name, matches, formatted_schedule): + def __init__(self, name, matches, formatted_schedule, round_id=None): self.name = name self.matches = matches self.formatted_schedule = formatted_schedule + self.round_id = round_id def add_match(self, match): self.matches.append(match) diff --git a/tournaments/static/tournaments/css/broadcast.css b/tournaments/static/tournaments/css/broadcast.css index ffd1f78..2975558 100644 --- a/tournaments/static/tournaments/css/broadcast.css +++ b/tournaments/static/tournaments/css/broadcast.css @@ -25,3 +25,57 @@ body { .bold { font-family: "Montserrat-Bold"; } + +.player { + position: relative; + flex: 1; + display: flex; + flex-direction: column; + min-height: 3.2em; /* This ensures minimum height for 2 lines */ + justify-content: center; + overflow: hidden; +} + +/* Add this if you want empty lines to take up space */ +.player div { + min-height: 1.4em; /* Height for single line */ +} + +/* For single player teams */ +.player.single-player .bold { + line-height: 1.4em; + overflow: hidden; + position: relative; + text-overflow: ellipsis; +} + +/* For two player teams */ +.player.two-players .bold { + line-height: 1.4em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.flex { + display: flex; + align-items: center; +} + +.flex-left { + flex: 1; + text-align: left; + justify-content: center; + min-height: 4em; + padding-right: 5px; +} + +.flex-right { + flex: initial; + text-align: right; + justify-content: center; +} + +.center { + align-items: center; +} diff --git a/tournaments/static/tournaments/css/style.css b/tournaments/static/tournaments/css/style.css index 5ea7cbb..e3073b3 100644 --- a/tournaments/static/tournaments/css/style.css +++ b/tournaments/static/tournaments/css/style.css @@ -323,9 +323,34 @@ tr { } .player { + position: relative; flex: 1; display: flex; flex-direction: column; + min-height: 3.2em; /* This ensures minimum height for 2 lines */ + justify-content: center; + overflow: hidden; +} + +/* Add this if you want empty lines to take up space */ +.player div { + min-height: 1.4em; /* Height for single line */ +} + +/* For single player teams */ +.player.single-player .semibold { + line-height: 1.4em; + overflow: hidden; + position: relative; + text-overflow: ellipsis; +} + +/* For two player teams */ +.player.two-players .semibold { + line-height: 1.4em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .scores { @@ -344,7 +369,6 @@ tr { font-size: 1.3em; vertical-align: middle; text-align: center; - padding: 0px 5px; /* width: 30px; */ } @@ -718,14 +742,15 @@ h-margin { .flex-left { flex: 1; text-align: left; - padding: 5px 0px; + justify-content: center; + min-height: 4em; + padding-right: 5px; } .flex-right { flex: initial; text-align: right; - vertical-align: middle; - padding: 5px 0px; + justify-content: center; } #header { @@ -833,7 +858,6 @@ h-margin { text-decoration: none; color: inherit; display: block; - padding: 2px 8px; border-radius: 6px; } @@ -878,10 +902,6 @@ h-margin { background-color: #90ee90; /* Light green color */ } -.player { - position: relative; /* Ensures the overlay is positioned within this block */ -} - .overlay-text { position: absolute; top: 50%; diff --git a/tournaments/templates/tournaments/broadcast/broadcasted_group_stage.html b/tournaments/templates/tournaments/broadcast/broadcasted_group_stage.html index 48d7c68..6371829 100644 --- a/tournaments/templates/tournaments/broadcast/broadcasted_group_stage.html +++ b/tournaments/templates/tournaments/broadcast/broadcasted_group_stage.html @@ -8,7 +8,10 @@