diff --git a/tournaments/models/match.py b/tournaments/models/match.py index 71571c4..0414ae1 100644 --- a/tournaments/models/match.py +++ b/tournaments/models/match.py @@ -268,6 +268,16 @@ class Match(models.Model): elif len(team_scores) == 2: # Both team scores present teams.extend([team_score.live_team(self) for team_score in team_scores]) + + if self.round is not None and self.round.parent is None: + pos1 = team_scores[0].team_registration.bracket_position if hasattr(team_scores[0], 'team_registration') and team_scores[0].team_registration else None + pos2 = team_scores[1].team_registration.bracket_position if hasattr(team_scores[1], 'team_registration') and team_scores[1].team_registration else None + if pos1 is not None and pos2 is not None and pos1 // 2 == self.index and pos2 // 2 == self.index: + if pos1 > pos2: + teams = [team_scores[1].live_team(self), team_scores[0].live_team(self)] + else: + teams = [team_scores[0].live_team(self), team_scores[1].live_team(self)] + else: teams.extend([team_score.live_team(self) for team_score in team_scores if team_score.walk_out != 1]) @@ -402,7 +412,7 @@ class Match(models.Model): ended = self.end_date is not None live_format = "Format " + FederalMatchCategory(self.format).format_label_short - livematch = LiveMatch(title, date, time_indication, court, self.started(), ended, group_stage_name, live_format, self.start_date, self.court_index, self.disabled) + livematch = LiveMatch(self.index, title, date, time_indication, court, self.started(), ended, group_stage_name, live_format, self.start_date, self.court_index, self.disabled) for team in self.live_teams(): livematch.add_team(team) @@ -463,7 +473,8 @@ class Team: } class LiveMatch: - def __init__(self, title, date, time_indication, court, started, ended, group_stage_name, format, start_date, court_index, disabled): + def __init__(self, index, title, date, time_indication, court, started, ended, group_stage_name, format, start_date, court_index, disabled): + self.index = index self.title = title self.date = date self.teams = [] @@ -485,6 +496,7 @@ class LiveMatch: def to_dict(self): return { + "index": self.index, "title": self.title, "date": self.date, "teams": [team.to_dict() for team in self.teams], diff --git a/tournaments/models/round.py b/tournaments/models/round.py index 7d740bc..1e23535 100644 --- a/tournaments/models/round.py +++ b/tournaments/models/round.py @@ -95,7 +95,7 @@ class Round(models.Model): return True - def prepare_match_group(self, next_round, parent_round, loser_final, double_butterfly_mode): + def prepare_match_group(self, next_round, parent_round, loser_final, double_butterfly_mode, secondHalf): matches = self.match_set.filter(disabled=False).order_by('index') if len(matches) == 0: return None @@ -118,7 +118,9 @@ class Round(models.Model): # 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) + if next_round_matches.filter(index=current_match.index // 2).exists(): + filtered_matches.append(current_match) + filtered_matches.append(pair_match) pass else: # Keep the current match @@ -138,6 +140,8 @@ class Round(models.Model): if len(matches) > 1 and double_butterfly_mode: midpoint = int(len(matches) / 2) first_half_matches = matches[:midpoint] + if secondHalf: + first_half_matches = matches[midpoint:] else: first_half_matches = list(matches) # Convert QuerySet to a list if self.index == 0 and loser_final: diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index 270530a..b30a4f6 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -661,8 +661,8 @@ class Tournament(models.Model): matches.extend(first_round.get_matches_recursive(True)) else: current_round = self.round_to_show() - # print(f'current_round = {current_round.index} / parent = {current_round.parent}') if current_round: + print(f'current_round = {current_round.index} / parent = {current_round.parent}') all_upper_matches_are_over = current_round.all_matches_are_over() if all_upper_matches_are_over is False: matches.extend(current_round.get_matches_recursive(True)) @@ -689,8 +689,12 @@ class Tournament(models.Model): 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') + print('group_stages') group_stages = [gs.live_group_stages() for gs in self.last_group_stage_step()] + else: + first_round = self.first_round() + if first_round: + matches.extend(first_round.get_matches_recursive(True)) return matches, group_stages @@ -1362,7 +1366,7 @@ class Tournament(models.Model): # 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) + match_group = round.prepare_match_group(next_round, parent_round, loser_final, double_butterfly_mode, False) if match_group: serializable_match_groups.append(match_group) @@ -1370,10 +1374,11 @@ class Tournament(models.Model): 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) + if round.index > 0: + next_round = main_rounds.filter(index=round.index - 1).first() + match_group = round.prepare_match_group(next_round, parent_round, None, double_butterfly_mode, True) + if match_group: + serializable_match_groups.append(match_group) return serializable_match_groups @@ -1390,6 +1395,13 @@ class MatchGroup: def add_matches(self, matches): self.matches = matches + def to_dict(self): + return { + 'name': self.name, + 'round_id': self.round_id, + 'matches': [match.to_dict() for match in self.matches] + } + class TeamSummon: def __init__(self, id, names, date, weight, stage, court, image, day_duration): self.id = str(id) diff --git a/tournaments/static/tournaments/css/style.css b/tournaments/static/tournaments/css/style.css index 30c0c3d..8498c51 100644 --- a/tournaments/static/tournaments/css/style.css +++ b/tournaments/static/tournaments/css/style.css @@ -339,10 +339,13 @@ tr { /* For single player teams */ .player.single-player .semibold { - line-height: 1.4em; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; overflow: hidden; - position: relative; + line-height: 1.4em; text-overflow: ellipsis; + max-height: 2.8em; /* 2 lines × 1.4em line-height */ } /* For two player teams */ @@ -661,8 +664,9 @@ h-margin { /* Summons */ .table-row-3-colums-summons { - display: flex; + grid-template-columns: 50% 25% 25%; align-items: center; + display: grid; } .summons-left, @@ -869,3 +873,11 @@ h-margin { transform: translate(-50%, -50%); white-space: nowrap; } + +.even-row { + background-color: #ffffff; /* White */ +} + +.odd-row { + background-color: #e6f2ff; /* Light blue */ +} diff --git a/tournaments/static/tournaments/css/tournament_bracket.css b/tournaments/static/tournaments/css/tournament_bracket.css new file mode 100644 index 0000000..1be8f50 --- /dev/null +++ b/tournaments/static/tournaments/css/tournament_bracket.css @@ -0,0 +1,240 @@ +.round-logo img { + width: 50px; + height: auto; + display: block; + margin: 0 auto; + position: relative; + top: -100px; /* Increased negative value to move it higher up */ +} + +.round-logo img { + width: 100px; /* Adjust size as needed */ + height: auto; + display: block; + margin: 0 auto; +} +.butterfly-match.same-level::before { + display: none; +} + +/* Adjust styling for matches with single parent */ +.match-content.disabled { + visibility: hidden; +} + +.incoming-line.disabled, +.outgoing-line.disabled, +.butterfly-match:has(.match-content.disabled)::after, +.butterfly-match:has(.match-content.disabled)::before { + visibility: hidden; +} + +.butterfly-bracket { + display: flex; + gap: 40px; /* Increased to account for horizontal lines (20px on each side) */ + position: relative; + margin-bottom: 80px; +} + +.round-title { + position: absolute; + top: 0px; /* Adjust this value to position the title where you want it */ + left: 50%; /* Center horizontally */ + transform: translateX(-50%); /* Center it exactly */ + text-align: center; + font-weight: bold; + width: auto; /* Change from 100% to auto */ + padding: 5px 10px; + + white-space: nowrap; /* Prevent text from wrapping */ + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.round-name { + color: #707070; + font-size: 1.5em; + padding: 8px 12px; + white-space: nowrap; /* Prevent text wrapping */ + display: block; /* Ensure proper centering */ +} + +.round-format { + font-size: 0.9em; + color: #707070; + margin-top: -5px; /* Reduced from -10px to bring it closer */ + white-space: nowrap; /* Prevent text wrapping */ + display: block; /* Ensure proper centering */ +} + +.round-name.button { + border-radius: 16px; + width: 100%; + display: inline-block; + background-color: #fae7ce; +} + +.button:hover { + color: white; + background-color: #f39200; +} + +.matches-container { + position: relative; + width: 100%; + flex-grow: 1; +} + +.butterfly-round { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; /* Space between title and matches */ + position: relative; + width: var(--match-width); + flex-shrink: 0; + margin-top: 100px; /* Add padding to account for absolute positioned title */ +} + +.butterfly-match { + position: absolute; + width: 100%; + padding: 10px 10px; +} + +/* Horizontal line after match */ +.outgoing-line { + content: ""; + position: absolute; + left: 100%; /* Start from end of match cell */ + top: 50%; + width: 20px; + height: 2px; + background: orange; +} + +.butterfly-match::before { + content: ""; + position: absolute; + left: calc(0% - 20px); + width: 2px; + top: calc(50% - (var(--next-match-distance) / 2)); + height: calc(var(--next-match-distance) + 2px); + background: pink; +} + +/* Vertical line connecting pair of matches */ +.butterfly-match.reverse-bracket::before { + content: ""; + position: absolute; + left: calc(100% + 20px); + width: 2px; + top: calc(50% - (var(--next-match-distance) / 2)); + height: calc(var(--next-match-distance) + 2px); + background: red; +} + +/* Horizontal line to next round match */ +.incoming-line { + position: absolute; + left: -20px; + top: 50%; + width: 20px; + height: 2px; + background: blue; +} + +/* Horizontal line to next round match */ +.butterfly-match .outgoing-line-downward { + position: absolute; + right: -20px; + top: 50%; /* Start from middle of match */ + width: 2px; + height: var(--semi-final-distance); + background: black; +} + +/* Horizontal line to next round match */ +.butterfly-match .outgoing-line-upward { + position: absolute; + right: -20px; + bottom: 50%; + width: 2px; + height: var(--semi-final-distance); + background: black; +} + +/* Horizontal line to next round match */ +.butterfly-match.reverse-bracket .outgoing-line-downward { + position: absolute; + left: -20px; + top: 50%; /* Start from middle of match */ + width: 2px; + height: var(--semi-final-distance); + background: black; +} + +/* Horizontal line to next round match */ +.butterfly-match.reverse-bracket .outgoing-line-upward { + position: absolute; + left: -20px; + bottom: 50%; + width: 2px; + height: var(--semi-final-distance); + background: black; +} + +.butterfly-round:last-child .butterfly-match.reverse-bracket::before, +.butterfly-round:last-child .outgoing-line, +.butterfly-round:first-child .incoming-line { + display: none; +} + +.broadcast-mode .round-name, +.broadcast-mode .round-format { + padding: 0px; + color: #707070; +} + +.broadcast-mode .round-title { + padding: 8px 20px; /* Slightly more horizontal padding */ + background-color: white; + align-content: center; + border-radius: 24px; +} + +.butterfly-match::before, +.butterfly-match.reverse-bracket::before, +.incoming-line, +.outgoing-line, +.outgoing-line-upward, +.outgoing-line-downward { + background-color: #707070 !important; /* Bright yellow - change to your preferred color */ +} + +/* Broadcast mode styling for all lines */ +.broadcast-mode .butterfly-match::before, +.broadcast-mode .butterfly-match.reverse-bracket::before, +.broadcast-mode .incoming-line, +.broadcast-mode .outgoing-line, +.broadcast-mode .outgoing-line-upward, +.broadcast-mode .outgoing-line-downward { + background-color: black !important; /* Bright yellow - change to your preferred color */ +} + +.bubble.match-running { + position: relative; +} + +.bubble.match-running::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 20px; /* Height of the green indicator */ + background-color: #90ee90; /* Light green color */ + border-radius: 0 0 24px 24px; /* Match the bubble's bottom corners */ +} diff --git a/tournaments/static/tournaments/js/tournament_bracket.js b/tournaments/static/tournaments/js/tournament_bracket.js new file mode 100644 index 0000000..3f610f1 --- /dev/null +++ b/tournaments/static/tournaments/js/tournament_bracket.js @@ -0,0 +1,424 @@ +function renderBracket(options) { + const bracket = document.getElementById("bracket"); + const matchTemplates = document.getElementById("match-templates").children; + const rounds = []; + const matchPositions = []; + const matchDisabled = []; + const doubleButterflyMode = options.doubleButterflyMode; + const displayLoserFinal = options.displayLoserFinal; + const tournamentId = options.tournamentId; + const isBroadcast = options.isBroadcast; + // Group matches by round + Array.from(matchTemplates).forEach((template) => { + const roundIndex = parseInt(template.dataset.matchRound); + if (!rounds[roundIndex]) { + rounds[roundIndex] = []; + } + rounds[roundIndex].push(template); + }); + + // First create a test match to get natural height + const firstMatch = document.createElement("div"); + firstMatch.className = "butterfly-match"; + firstMatch.innerHTML = `
${rounds[0][0].innerHTML}
`; + bracket.appendChild(firstMatch); + const matchHeight = firstMatch.offsetHeight; + const matchSpacing = 10; + const baseDistance = matchHeight + matchSpacing; + bracket.innerHTML = ""; + const roundCount = rounds.length; + let finalRoundIndex = roundCount - 1; + if (doubleButterflyMode == true) { + finalRoundIndex = finalRoundIndex / 2; + } + let nextMatchDistance = baseDistance; + let minimumMatchDistance = 1; + + const totalRounds = document.querySelectorAll(".butterfly-round").length; + const screenWidth = window.innerWidth; + let roundTotalCount = roundCount; + if (doubleButterflyMode == true && roundCount > 1) { + roundTotalCount = roundCount - 1; + } + const padding = 50 * roundTotalCount; // Account for some padding/margin + const availableWidth = screenWidth - padding; + responsiveMatchWidth = Math.min( + 365, + Math.max(365, Math.floor(availableWidth / roundTotalCount)), + ); + + if (isBroadcast) { + responsiveMatchWidth = Math.min( + 365, + Math.floor(availableWidth / roundTotalCount), + ); + } + + rounds.forEach((roundMatches, roundIndex) => { + if (rounds[0].length <= 2 && doubleButterflyMode) { + minimumMatchDistance = 2; + nextMatchDistance = baseDistance * 2; + } + const roundDiv = document.createElement("div"); + roundDiv.className = "butterfly-round"; + roundDiv.style.setProperty("--match-width", `${responsiveMatchWidth}px`); + + // Create title + const titleDiv = document.createElement("div"); + titleDiv.className = "round-title"; + + // Get the match group name and format + const firstMatchTemplate = roundMatches[0].closest(".match-template"); + const matchGroupName = firstMatchTemplate.dataset.matchGroupName; + const matchFormat = firstMatchTemplate.dataset.matchFormat; + const roundId = firstMatchTemplate.dataset.roundId; // Add this line + + let nameSpan = document.createElement("div"); + nameSpan.className = "round-name"; + nameSpan.textContent = matchGroupName; + if ( + roundIndex == finalRoundIndex || + (roundIndex == finalRoundIndex - 1 && displayLoserFinal) || + (roundIndex == finalRoundIndex + 1 && displayLoserFinal) || + isBroadcast + ) { + } else { + nameSpan = document.createElement("a"); + nameSpan.className = "round-name"; + nameSpan.classList.add("button"); + nameSpan.textContent = matchGroupName; + if (roundId) { + nameSpan.href = `/tournament/${tournamentId}/round/${roundId}/bracket/`; + nameSpan.style.cursor = "pointer"; + } + } + + const formatSpan = document.createElement("div"); + formatSpan.className = "round-format"; + formatSpan.textContent = matchFormat; + + titleDiv.appendChild(nameSpan); + titleDiv.appendChild(formatSpan); + + // Create matches container + const matchesContainer = document.createElement("div"); + matchesContainer.className = "matches-container"; + if (roundCount > 5 && doubleButterflyMode == true) { + if (roundIndex >= finalRoundIndex - 1) { + matchesContainer.style.transform = `translateX(-50%)`; + if (roundIndex >= finalRoundIndex + 2) { + matchesContainer.style.transform = `translateX(-100%)`; + } + } + } + roundDiv.appendChild(matchesContainer); + if (matchPositions[roundIndex] == undefined) { + matchPositions[roundIndex] = {}; + } + matchDisabled[roundIndex] = []; // Initialize array for this round + roundMatches.forEach((matchTemplate, matchIndex) => { + const matchTitle = matchTemplate.dataset.matchTitle; + const matchRealIndex = matchTemplate.dataset.matchRealIndex; + const matchDiv = document.createElement("div"); + matchDiv.className = "butterfly-match"; + + matchDiv.style.position = "absolute"; + const isDisabled = matchTemplate.dataset.disabled === "true"; + matchDisabled[roundIndex][matchRealIndex] = isDisabled; + let isIncomingLineIsDisabled = isDisabled; + let isOutgoingLineIsDisabled = isDisabled; + let top; + const currentMatchesCount = roundMatches.length; + if (roundIndex > finalRoundIndex) { + matchDiv.classList.add("reverse-bracket"); + + if (roundIndex <= finalRoundIndex + 2) { + const values = Object.values( + matchPositions[roundCount - roundIndex - 1], + ); + top = values[matchIndex]; + } else { + top = matchPositions[roundIndex][matchRealIndex]; + console.log(matchTitle, top); + } + } + + if (roundIndex === 0) { + nextMatchDistance = 0; + if (roundCount > 1) { + const nextMatchesCount = rounds[roundIndex + 1].length; + + if (currentMatchesCount == nextMatchesCount && roundCount > 2) { + nextMatchDistance = 0; + } + } else { + nextMatchDistance = 0; + } + + top = matchIndex * (matchHeight + matchSpacing) * minimumMatchDistance; + + if (roundCount == 3 && doubleButterflyMode) { + top = top + (matchHeight + matchSpacing) / 2; + } + } else if (roundIndex === roundCount - 1 && doubleButterflyMode == true) { + nextMatchDistance = 0; + } else if (roundIndex == finalRoundIndex) { + const values = Object.values(matchPositions[roundIndex - 1]); + const parentPos1 = values[0]; + const parentPos2 = values[1]; + + if (doubleButterflyMode == true) { + let lgth = matchPositions[0].length / 2; + let index = lgth + matchIndex - 1; + // If index goes negative, use 0 instead + if (displayLoserFinal == true) { + if (matchIndex == 0) { + top = parentPos1 - baseDistance / 2; + } else { + top = parentPos1 + baseDistance / 2; + } + nextMatchDistance = 0; + } else { + top = parentPos1; + nextMatchDistance = 0; + } + } else { + top = (parentPos1 + parentPos2) / 2; + if (matchIndex == 0) { + nextMatchDistance = parentPos2 - parentPos1; + } else { + nextMatchDistance = 0; + } + + if (displayLoserFinal == true) { + if (matchIndex == 1) { + top = matchPositions[roundIndex][0] + baseDistance + 80; + isIncomingLineIsDisabled = true; + } + } + } + } else if (roundIndex < finalRoundIndex) { + const parentIndex1 = matchRealIndex * 2 + 1; + const parentIndex2 = matchRealIndex * 2 + 2; + const parentPos1 = matchPositions[roundIndex - 1][parentIndex1]; + const parentPos2 = matchPositions[roundIndex - 1][parentIndex2]; + const parent1Disable = matchDisabled[roundIndex - 1][parentIndex1]; + const parent2Disable = matchDisabled[roundIndex - 1][parentIndex2]; + if ( + (parent1Disable == undefined || parent1Disable == true) && + (parent2Disable == undefined || parent2Disable == true) + ) { + isIncomingLineIsDisabled = true; + } + if ( + matchPositions[roundIndex - 1][parentIndex1] != undefined && + matchPositions[roundIndex - 1][parentIndex2] != undefined + ) { + top = (parentPos1 + parentPos2) / 2; + if (parent1Disable && parent2Disable) { + nextMatchDistance = 0; + } else { + nextMatchDistance = parentPos2 - parentPos1; + } + } else if (matchPositions[roundIndex - 1][parentIndex1] != undefined) { + nextMatchDistance = 0; + top = matchPositions[roundIndex - 1][parentIndex1]; + } else if (matchPositions[roundIndex - 1][parentIndex2] != undefined) { + nextMatchDistance = 0; + top = matchPositions[roundIndex - 1][parentIndex2]; + } else { + nextMatchDistance = 0; + top = 0; + } + } else if (roundIndex < roundCount) { + const parentIndex1 = matchRealIndex * 2 + 1; + const parentIndex2 = matchRealIndex * 2 + 2; + const parentMatch1 = rounds[roundIndex + 1].find( + (match) => parseInt(match.dataset.matchRealIndex) === parentIndex1, + ); + const parentMatch2 = rounds[roundIndex + 1].find( + (match) => parseInt(match.dataset.matchRealIndex) === parentIndex2, + ); + + if (matchPositions[roundIndex + 1] == undefined) { + matchPositions[roundIndex + 1] = {}; + } + if ( + parentMatch1 != undefined && + parentMatch2 != undefined && + parentMatch1.dataset.disabled == "false" && + parentMatch2.dataset.disabled == "false" + ) { + console.log( + roundIndex, + matchTitle, + parentMatch1.dataset.matchTitle, + parentMatch2.dataset.matchTitle, + parentMatch1.dataset.disabled, + parentMatch2.dataset.disabled, + top, + ); + nextMatchDistance = baseDistance; + matchPositions[roundIndex + 1][parentIndex1] = top - baseDistance / 2; + matchPositions[roundIndex + 1][parentIndex2] = top + baseDistance / 2; + console.log(matchPositions[roundIndex + 1]); + // } else if (parentMatch1 != undefined) { + // matchPositions[roundIndex + 1][parentIndex1] = top; + // nextMatchDistance = 0; + // } else if (parentMatch2 != undefined) { + // matchPositions[roundIndex + 1][parentIndex1] = top; + // nextMatchDistance = 0; + } else if ( + parentMatch2 != undefined && + parentMatch2.dataset.disabled == "false" + ) { + nextMatchDistance = 0; + matchPositions[roundIndex + 1][parentIndex2] = top; + } else if ( + parentMatch1 != undefined && + parentMatch1.dataset.disabled == "false" + ) { + nextMatchDistance = 0; + matchPositions[roundIndex + 1][parentIndex1] = top; + } else { + isOutgoingLineIsDisabled = true; + } + } + + if (roundCount > 5 && doubleButterflyMode == true) { + if (roundIndex >= finalRoundIndex - 2) { + if (roundIndex == finalRoundIndex - 1) { + matchDiv.classList.add("reverse-bracket"); + isIncomingLineIsDisabled = true; + nextMatchDistance = nextMatchDistance / 2; + } + if (roundIndex == finalRoundIndex + 1) { + matchDiv.classList.remove("reverse-bracket"); + isOutgoingLineIsDisabled = true; + nextMatchDistance = nextMatchDistance; + } + } + } + matchDiv.style.setProperty( + "--semi-final-distance", + `${baseDistance / 2}px`, + ); + + matchDiv.style.setProperty( + "--next-match-distance", + `${nextMatchDistance}px`, + ); + matchDiv.style.top = `${top}px`; + matchPositions[roundIndex][matchRealIndex] = top; + + if (matchIndex === 0) { + // // Add logo for final round + // if (roundIndex == finalRoundIndex) { + // const logoDiv = document.createElement('div'); + // logoDiv.className = 'round-logo'; + // const logoImg = document.createElement('img'); + // logoImg.src = '/static/tournaments/images/PadelClub_logo_512.png'; + // logoImg.alt = 'PadelClub Logo'; + // logoDiv.appendChild(logoImg); + // logoDiv.style.transform = `translateX(-50%)`; + // matchesContainer.appendChild(logoDiv); + // } + + // Position title above the first match + if ( + roundIndex < finalRoundIndex - 1 || + roundIndex > finalRoundIndex + 1 + ) { + titleDiv.style.top = `${-80}px`; // Adjust the 60px offset as needed + } else { + titleDiv.style.top = `${top - 80}px`; // Adjust the 60px offset as needed + } + titleDiv.style.position = "absolute"; + if (roundCount >= 5 && doubleButterflyMode == true) { + if (roundIndex == finalRoundIndex - 1) { + titleDiv.style.marginLeft = "60px"; + } else if (roundIndex == finalRoundIndex + 1) { + titleDiv.style.marginLeft = "-60px"; + } + } + matchesContainer.appendChild(titleDiv); + } + + if ( + roundIndex == finalRoundIndex && + matchIndex === 1 && + matchDisabled[roundIndex][matchRealIndex] == false && + displayLoserFinal == true && + doubleButterflyMode == false + ) { + let nameSpan = document.createElement("div"); + nameSpan.className = "round-name"; + nameSpan.textContent = matchTitle; + const formatSpan = document.createElement("div"); + formatSpan.className = "round-format"; + formatSpan.textContent = matchFormat; + const titleDiv = document.createElement("div"); + titleDiv.className = "round-title"; + titleDiv.appendChild(nameSpan); + titleDiv.appendChild(formatSpan); + titleDiv.style.top = `${top - 80}px`; // Adjust the 60px offset as needed + titleDiv.style.position = "absolute"; + matchesContainer.appendChild(titleDiv); + } + + matchDiv.innerHTML = ` +
+
${matchTemplate.innerHTML}
+
+ `; + + if (roundCount > 5 && doubleButterflyMode == true) { + if (roundIndex >= finalRoundIndex - 2) { + if (roundIndex === finalRoundIndex - 2) { + if (matchIndex === 0) { + const outgoingLine = document.createElement("div"); + outgoingLine.className = "outgoing-line-downward"; + matchDiv.appendChild(outgoingLine); + } else { + const outgoingLine = document.createElement("div"); + outgoingLine.className = "outgoing-line-upward"; + matchDiv.appendChild(outgoingLine); + } + } + if (roundIndex === finalRoundIndex + 2) { + if (matchIndex === 0) { + const outgoingLine = document.createElement("div"); + outgoingLine.className = "outgoing-line-downward"; + matchDiv.appendChild(outgoingLine); + } else { + const outgoingLine = document.createElement("div"); + outgoingLine.className = "outgoing-line-upward"; + matchDiv.appendChild(outgoingLine); + } + } + } + } + + // if ( + // roundIndex == finalRoundIndex - 1 && + // displayLoserFinal == true && + // doubleButterflyMode == true + // ) { + // const matchDiv2 = document.createElement("div"); + // matchDiv2.className = "butterfly-match"; + // matchDiv2.classList.add("inward"); + // matchDiv2.classList.add("semi-final"); + // matchDiv2.style.setProperty( + // "--next-match-distance", + // `${baseDistance}px`, + // ); + // matchDiv2.style.top = `${top}px`; + // matchDiv2.innerHTML = `
${rounds[0][0].innerHTML}
`; + // matchesContainer.appendChild(matchDiv2); // Append to matchesContainer instead of roundDiv + // } + matchesContainer.appendChild(matchDiv); // Append to matchesContainer instead of roundDiv + }); + + bracket.appendChild(roundDiv); + }); +} diff --git a/tournaments/templates/tournaments/base.html b/tournaments/templates/tournaments/base.html index ee1dec2..a7d2b24 100644 --- a/tournaments/templates/tournaments/base.html +++ b/tournaments/templates/tournaments/base.html @@ -13,6 +13,7 @@ /> + {% block extra_css %}{% endblock %} {% for team in match.teams %}
-
+
{% if team.id %} {% endif %} diff --git a/tournaments/templates/tournaments/broadcast/broadcast.html b/tournaments/templates/tournaments/broadcast/broadcast.html index 61b7dea..e090ec9 100644 --- a/tournaments/templates/tournaments/broadcast/broadcast.html +++ b/tournaments/templates/tournaments/broadcast/broadcast.html @@ -12,11 +12,12 @@ diff --git a/tournaments/templates/tournaments/broadcast/broadcast_club.html b/tournaments/templates/tournaments/broadcast/broadcast_club.html index 6b3d617..77196a4 100644 --- a/tournaments/templates/tournaments/broadcast/broadcast_club.html +++ b/tournaments/templates/tournaments/broadcast/broadcast_club.html @@ -13,7 +13,7 @@ {% for tournament in tournaments %} -
+
{{ tournament.formatted_start_date }}
@@ -31,7 +31,8 @@
Automatic | - Programmation | + (beta) Tableau | + (beta) Programmation | Matchs | Poules | Convocations | diff --git a/tournaments/templates/tournaments/broadcast/broadcasted_bracket.html b/tournaments/templates/tournaments/broadcast/broadcasted_bracket.html new file mode 100644 index 0000000..4c6e842 --- /dev/null +++ b/tournaments/templates/tournaments/broadcast/broadcasted_bracket.html @@ -0,0 +1,161 @@ + +{% load static %} +{% load qr_code %} + + + + + + + + + + + + Tableau + + + + + + + + +
+ +
+ +
+
+
+
+
+
+
+ + + + + diff --git a/tournaments/templates/tournaments/broadcast/broadcasted_summon.html b/tournaments/templates/tournaments/broadcast/broadcasted_summon.html index f3272fa..de2d2d8 100644 --- a/tournaments/templates/tournaments/broadcast/broadcasted_summon.html +++ b/tournaments/templates/tournaments/broadcast/broadcasted_summon.html @@ -9,13 +9,11 @@
-
-
-
-
-
-
+
+
+
+
diff --git a/tournaments/templates/tournaments/navigation_tournament.html b/tournaments/templates/tournaments/navigation_tournament.html index e144d9a..fd5995f 100644 --- a/tournaments/templates/tournaments/navigation_tournament.html +++ b/tournaments/templates/tournaments/navigation_tournament.html @@ -2,9 +2,9 @@ Informations - + {% endif %} {% if tournament.display_matches or tournament.display_group_stages %} Matches diff --git a/tournaments/templates/tournaments/tournament_bracket.html b/tournaments/templates/tournaments/tournament_bracket.html index b334bfd..daf44df 100644 --- a/tournaments/templates/tournaments/tournament_bracket.html +++ b/tournaments/templates/tournaments/tournament_bracket.html @@ -1,4 +1,9 @@ {% extends 'tournaments/base.html' %} +{% load static %} + +{% block extra_css %} + +{% endblock %} {% block head_title %}Matchs du {{ tournament.display_name }}{% endblock %} {% block first_title %}{{ tournament.event.display_name }}{% endblock %} @@ -21,6 +26,7 @@ data-match-group-name="{{ match_group.name }}" data-match-format="{{ match.format }}" data-match-title="{{ match.title }}" + data-match-real-index="{{ match.index }}" data-round-id="{{ match_group.round_id }}" class="match-template"> {% include 'tournaments/bracket_match_cell.html' %} @@ -30,479 +36,15 @@ {% endfor %}
+ - - {% endif %} {% endblock %} diff --git a/tournaments/urls.py b/tournaments/urls.py index 2bede6a..4f315c2 100644 --- a/tournaments/urls.py +++ b/tournaments/urls.py @@ -31,6 +31,8 @@ urlpatterns = [ path('broadcast/json/', views.broadcast_json, name='broadcast-json'), path('broadcast/group-stages/', views.tournament_broadcasted_group_stages, name='broadcasted-group-stages'), path('broadcast/prog/', views.tournament_broadcasted_prog, name='broadcasted-prog'), + path('broadcast/bracket/', views.tournament_broadcasted_bracket, name='broadcasted-bracket'), + path('bracket/json/', views.tournament_bracket_json, name='tournament-bracket-json'), path('group-stages/', views.tournament_group_stages, name='group-stages'), path('group-stages/json/', views.tournament_live_group_stage_json, name='group-stages-json'), path('rankings/', views.tournament_rankings, name='tournament-rankings'), diff --git a/tournaments/views.py b/tournaments/views.py index 3de6532..1a5df8a 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -449,6 +449,31 @@ def tournament_broadcasted_prog(request, tournament_id): 'qr_code_options': qr_code_options(), }) +def tournament_broadcasted_bracket(request, tournament_id): + tournament = get_object_or_404(Tournament, pk=tournament_id) + return render(request, 'tournaments/broadcast/broadcasted_bracket.html', { + 'tournament': tournament, + 'qr_code_url': qr_code_url(request, tournament_id), + 'qr_code_options': qr_code_options(), + }) + + +def tournament_bracket_json(request, tournament_id): + """ + JSON endpoint for tournament bracket data. + """ + tournament = get_object_or_404(Tournament, pk=tournament_id) + context = get_butterfly_bracket_view_context(tournament, parent_round=None, double_butterfly_mode=True, display_loser_final=True) + + data = { + 'match_groups': [match_group.to_dict() for match_group in context['match_groups']], + 'double_butterfly_mode': context['double_butterfly_mode'], + 'display_loser_final': context['display_loser_final'] + } + + return JsonResponse(data) + + def tournament_live_group_stage_json(request, tournament_id): tournament = get_object_or_404(Tournament, pk=tournament_id) @@ -578,8 +603,20 @@ def xls_to_csv(request): file_path = os.path.join(directory, uploaded_file.name) file_name = default_storage.save(file_path, ContentFile(uploaded_file.read())) - # convert to csv and save - data_xls = pd.read_excel(file_name, sheet_name=0, index_col=None) + # Check available sheets and look for 'inscriptions' + xls = pd.ExcelFile(file_name) + sheet_names = xls.sheet_names + + # Determine which sheet to use + target_sheet = 0 # Default to first sheet + if 'inscriptions' in [name.lower() for name in sheet_names]: + for i, name in enumerate(sheet_names): + if name.lower() == 'inscriptions': + target_sheet = i # or use the name directly: target_sheet = name + break + + # Convert to csv and save + data_xls = pd.read_excel(file_name, sheet_name=target_sheet, index_col=None) csv_file_name = create_random_filename('players', 'csv') output_path = os.path.join(directory, csv_file_name) data_xls.to_csv(output_path, sep=';', index=False, encoding='utf-8') @@ -1018,7 +1055,7 @@ def round_bracket(request, tournament_id, round_id): context = get_butterfly_bracket_view_context(tournament, round, double_butterfly_mode=False, display_loser_final=False) return render(request, 'tournaments/tournament_bracket.html', context) -def get_butterfly_bracket_view_context(tournament, parent_round=None, double_butterfly_mode=False, display_loser_final=False): +def get_butterfly_bracket_view_context(tournament, parent_round=None, double_butterfly_mode=False, display_loser_final=True): if parent_round: double_butterfly_mode = False display_loser_final = True