Laurent 8 months ago
commit 621a00f13d
  1. 16
      tournaments/models/match.py
  2. 8
      tournaments/models/round.py
  3. 26
      tournaments/models/tournament.py
  4. 18
      tournaments/static/tournaments/css/style.css
  5. 240
      tournaments/static/tournaments/css/tournament_bracket.css
  6. 424
      tournaments/static/tournaments/js/tournament_bracket.js
  7. 1
      tournaments/templates/tournaments/base.html
  8. 2
      tournaments/templates/tournaments/bracket_match_cell.html
  9. 3
      tournaments/templates/tournaments/broadcast/broadcast.html
  10. 5
      tournaments/templates/tournaments/broadcast/broadcast_club.html
  11. 161
      tournaments/templates/tournaments/broadcast/broadcasted_bracket.html
  12. 10
      tournaments/templates/tournaments/broadcast/broadcasted_summon.html
  13. 4
      tournaments/templates/tournaments/navigation_tournament.html
  14. 484
      tournaments/templates/tournaments/tournament_bracket.html
  15. 2
      tournaments/urls.py
  16. 43
      tournaments/views.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],

@ -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:

@ -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)

@ -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 */
}

@ -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 */
}

@ -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 = `<div class="match-content">${rounds[0][0].innerHTML}</div>`;
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 = `
<div class="incoming-line ${isIncomingLineIsDisabled ? "disabled" : ""}"></div>
<div class="match-content ${isDisabled ? "disabled" : ""}">${matchTemplate.innerHTML}</div>
<div class="outgoing-line ${isOutgoingLineIsDisabled ? "disabled" : ""}"></div>
`;
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 = `<div class="match-content">${rounds[0][0].innerHTML}</div>`;
// matchesContainer.appendChild(matchDiv2); // Append to matchesContainer instead of roundDiv
// }
matchesContainer.appendChild(matchDiv); // Append to matchesContainer instead of roundDiv
});
bracket.appendChild(roundDiv);
});
}

@ -13,6 +13,7 @@
/>
<link rel="stylesheet" href="{% static 'tournaments/css/style.css' %}" />
<link rel="stylesheet" href="{% static 'tournaments/css/basics.css' %}" />
{% block extra_css %}{% endblock %}
<link
rel="icon"

@ -5,7 +5,7 @@
<div>
{% for team in match.teams %}
<div class="match-result {% cycle 'bottom-border' '' %}">
<div class="player">
<div class="player {% if team.names|length == 1 %}single-player{% else %}two-players{% endif %}">
{% if team.id %}
<a href="{% url 'team-details' tournament.id team.id %}" class="player-link">
{% endif %}

@ -12,11 +12,12 @@
<div class="cell medium-6 large-6 topblock my-block">
<div class="bubble">
<div><a href="{% url 'automatic-broadcast' tournament.id %}">Automatique</a></div>
<div><a href="{% url 'broadcasted-prog' tournament.id %}">Programmation</a></div>
<div><a href="{% url 'broadcasted-matches' tournament.id %}">Matchs</a></div>
<div><a href="{% url 'broadcasted-group-stages' tournament.id %}">Poules</a></div>
<div><a href="{% url 'broadcasted-summons' tournament.id %}">Convocations</a></div>
<div><a href="{% url 'broadcasted-rankings' tournament.id %}">Classement</a></div>
<div><a href="{% url 'broadcasted-prog' tournament.id %}">(beta) Programmation</a></div>
<div><a href="{% url 'broadcasted-bracket' tournament.id %}">(beta) Tableau</a></div>
</div>
</div>

@ -13,7 +13,7 @@
{% for tournament in tournaments %}
<div class="table-row-6-colums-club-tournament">
<div class="table-row-6-colums-club-tournament {% cycle 'even-row' 'odd-row' %}">
<div class="table-cell mybox center">{{ tournament.formatted_start_date }}</div>
<div class="tight table-cell">
@ -31,7 +31,8 @@
</div>
<div class="table-cell">
<span><a href="{% url 'automatic-broadcast' tournament.id %}">Automatic</a></span> |
<span><a href="{% url 'broadcasted-prog' tournament.id %}">Programmation</a></span> |
<span><a href="{% url 'broadcasted-bracket' tournament.id %}">(beta) Tableau</a></span> |
<span><a href="{% url 'broadcasted-prog' tournament.id %}">(beta) Programmation</a></span> |
<span><a href="{% url 'broadcasted-matches' tournament.id %}">Matchs</a></span> |
<span><a href="{% url 'broadcasted-group-stages' tournament.id %}">Poules</a></span> |
<span><a href="{% url 'broadcasted-summons' tournament.id %}">Convocations</a></span> |

@ -0,0 +1,161 @@
<!DOCTYPE html>
{% load static %}
{% load qr_code %}
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="{% static 'tournaments/css/foundation.min.css' %}" />
<link rel="stylesheet" href="{% static 'tournaments/css/basics.css' %}" />
<link rel="stylesheet" href="{% static 'tournaments/css/style.css' %}" />
<link rel="stylesheet" href="{% static 'tournaments/css/broadcast.css' %}" />
<link rel="stylesheet" href="{% static 'tournaments/css/tournament_bracket.css' %}" />
<link rel="icon" type="image/png" href="{% static 'tournaments/images/favicon.png' %}" />
<title>Tableau</title>
<script src="{% static 'tournaments/js/alpine.min.js' %}"></script>
<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["setDocumentTitle", document.domain + "/" + document.title]);
_paq.push(["setDoNotTrack", true]);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//matomo.padelclub.app/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '1']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<!-- End Matomo Code -->
</head>
<body>
<header>
<div id="header">
<div class="left-content bubble">
<img src="{% static 'tournaments/images/PadelClub_logo_512.png' %}" alt="logo" class="logo">
<div class="left-margin">
<h1 class="club">{{ tournament.broadcast_event_display_name }}</h1>
<h1 class="event">Tableau {{ tournament.broadcast_display_name }}</h1>
</div>
</div>
<div class="right-content">{% qr_from_text qr_code_url options=qr_code_options %}</div>
</div>
</header>
<div class="wrapper">
<main>
<div class="bracket-container">
<div class="butterfly-bracket" id="bracket"></div>
</div>
</main>
</div>
<script src="{% static 'tournaments/js/tournament_bracket.js' %}"></script>
<script>
// Simple vanilla JS to handle the bracket rendering
const tournamentId = "{{ tournament.id }}";
function createMatchHTML(match) {
let html = '<div>';
// Generate HTML for each team
match.teams.forEach((team, teamIndex) => {
html += `
<div class="match-result ${teamIndex === 0 ? 'bottom-border' : ''}">
<div class="player ${team.names.length === 1 ? 'single-player' : 'two-players'}">
${team.is_lucky_loser ? '<div class="overlay-text">(LL)</div>' : ''}
${team.walk_out === 1 ? '<div class="overlay-text">(WO)</div>' : ''}
${team.names.map(name => `
<div class="semibold ${team.walk_out === 1 ? 'strikethrough' : ''} ${team.is_winner ? 'winner' : ''}">
${name && name.length > 0 ? name : '&nbsp;'}
</div>
`).join('')}
</div>
${match.has_walk_out ? `
<span class="score ws w60px">${team.is_walk_out ? 'WO' : ''}</span>
` : ''}
${team.scores && team.scores.length > 0 ? `
<div class="scores">
${team.scores.map(score => `
<span class="score ws ${score.tiebreak ? 'w35px' : 'w30px'} ${team.is_winner ? 'winner' : ''}">
${score.main}
${score.tiebreak ? `<sup>${score.tiebreak}</sup>` : ''}
</span>
`).join('')}
</div>
` : ''}
${team.weight && !match.has_walk_out && (!team.scores || team.scores.length === 0) ? `
<span class="score ws numbers">${team.weight}</span>
` : ''}
</div>
`;
});
html += '</div>';
return html;
}
function fetchAndRenderBracket() {
fetch('/tournament/' + tournamentId + '/bracket/json/')
.then(res => res.json())
.then((data) => {
// Create a hidden div to hold all our match templates
const tempContainer = document.createElement('div');
tempContainer.style.display = 'none';
tempContainer.id = 'match-templates';
document.body.appendChild(tempContainer);
// Create templates for each match with the right data attributes
data.match_groups.forEach((group, groupIndex) => {
group.matches.forEach((match, matchIndex) => {
const template = document.createElement('div');
template.className = 'match-template';
template.dataset.matchRound = groupIndex;
template.dataset.matchIndex = matchIndex;
template.dataset.disabled = match.disabled;
template.dataset.matchGroupName = group.name;
template.dataset.matchFormat = match.format;
template.dataset.matchTitle = match.title;
template.dataset.roundId = group.round_id;
template.dataset.matchRealIndex = match.index;
// Create the match content using our HTML generator
template.innerHTML = `<div class="bubble broadcast-bracket-match ${(!match.ended && match.started) ? 'match-running' : ''}">${createMatchHTML(match)}</div>`;
tempContainer.appendChild(template);
});
});
// Now call the renderBracket function with our data
renderBracket({
doubleButterflyMode: true,
displayLoserFinal: true,
tournamentId: tournamentId,
isBroadcast: true
});
// Clean up our temporary container
document.body.removeChild(tempContainer);
document.getElementById('bracket').classList.add('broadcast-mode');
});
}
// Initial render
fetchAndRenderBracket();
// Set up the refresh interval
setInterval(fetchAndRenderBracket, 15000);
</script>
</body>
</html>

@ -9,13 +9,11 @@
</template>
</div>
</div>
<div class="summons-right">
<div class="table-cell left">
<div class="table-cell large"><span x-text="summon.date"></span></div>
<div class="table-cell"><span x-text="summon.court"></span></div>
</div>
<div class="table-cell right"><div class="mybox center"><span x-text="summon.stage"></span></div></div>
<div class="table-cell left">
<div class="table-cell large"><span x-text="summon.date"></span></div>
<div class="table-cell"><span x-text="summon.court"></span></div>
</div>
<div class="table-cell right"><div class="mybox center"><span x-text="summon.stage"></span></div></div>
</div>
<div x-show="index < column.length - 1">

@ -2,9 +2,9 @@
<a href="{% url 'tournament-info' tournament.id %}" class="topmargin5 orange">Informations</a>
<!-- {% if tournament.display_matches %}
{% if tournament.display_matches %}
<a href="{% url 'tournament-bracket' tournament.id %}" class="topmargin5 orange">Tableau</a>
{% endif %} -->
{% endif %}
{% if tournament.display_matches or tournament.display_group_stages %}
<a href="{% url 'tournament' tournament.id %}" class="topmargin5 orange">Matches</a>

@ -1,4 +1,9 @@
{% extends 'tournaments/base.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'tournaments/css/tournament_bracket.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 %}
</div>
<script src="{% static 'tournaments/js/tournament_bracket.js' %}"></script>
<script>
const tournamentId = "{{ tournament.id }}";
function renderBracket() {
const bracket = document.getElementById('bracket');
const matchTemplates = document.getElementById('match-templates').children;
const rounds = [];
const matchPositions = [];
const matchDisabled = []; // New array to track disabled matches
const doubleButterflyMode = {{ double_butterfly_mode|lower }};
const displayLoserFinal = {{ display_loser_final|lower }};
// 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 = `<div class="match-content">${rounds[0][0].innerHTML}</div>`;
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;
rounds.forEach((roundMatches, roundIndex) => {
if (rounds[0].length <= 2) {
minimumMatchDistance = 2
nextMatchDistance = baseDistance * 2;
}
const roundDiv = document.createElement('div');
roundDiv.className = 'butterfly-round';
roundDiv.style.setProperty('--match-width', `${365}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) {
} 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);
matchPositions[roundIndex] = [];
matchDisabled[roundIndex] = []; // Initialize array for this round
roundMatches.forEach((matchTemplate, matchIndex) => {
const matchTitle = matchTemplate.dataset.matchTitle;
const matchDiv = document.createElement('div');
matchDiv.className = 'butterfly-match';
matchDiv.style.position = 'absolute';
const isDisabled = matchTemplate.dataset.disabled === 'true';
matchDisabled[roundIndex][matchIndex] = isDisabled;
let isIncomingLineIsDisabled = isDisabled;
let top;
let left;
let right;
const currentMatchesCount = roundMatches.length;
if (roundIndex > finalRoundIndex) {
matchDiv.classList.add('reverse-bracket');
top = matchPositions[roundCount - roundIndex - 1][matchIndex];
}
if (roundIndex === 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) {
const nextMatchesCount = rounds[roundIndex - 1].length;
if (currentMatchesCount == nextMatchesCount) {
nextMatchDistance = 0;
}
} else if (roundIndex == finalRoundIndex) {
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 = matchPositions[roundIndex - 1][0] - baseDistance / 2;
} else {
top = matchPositions[roundIndex - 1][0] + baseDistance / 2;
}
nextMatchDistance = baseDistance;
} else {
top = matchPositions[roundIndex - 1][0];
nextMatchDistance = 0;
}
} else {
const parentIndex1 = matchIndex * 2;
const parentIndex2 = parentIndex1 + 1;
const parentPos1 = matchPositions[roundIndex - 1][parentIndex1];
const parentPos2 = matchPositions[roundIndex - 1][parentIndex2];
top = (parentPos1 + parentPos2) / 2;
nextMatchDistance = 0;
if (displayLoserFinal == true) {
if (matchIndex == 1) {
top = matchPositions[roundIndex][0] + baseDistance + 80;
isIncomingLineIsDisabled = true;
}
}
}
} else if (roundIndex < finalRoundIndex) {
const previousMatchesCount = rounds[roundIndex - 1].length;
const nextMatchesCount = rounds[roundIndex + 1].length;
if (currentMatchesCount == nextMatchesCount && displayLoserFinal == false) {
nextMatchDistance = 0;
} else if (matchPositions.length > roundIndex - 1) {
nextMatchDistance = (matchPositions[roundIndex - 1][1] - matchPositions[roundIndex - 1][0]);
nextMatchDistance = nextMatchDistance * (previousMatchesCount / currentMatchesCount);
}
if (currentMatchesCount == previousMatchesCount) {
if (matchDisabled[roundIndex - 1][matchIndex] == true) {
isIncomingLineIsDisabled = true
}
top = matchPositions[roundIndex - 1][matchIndex];
} else {
const parentIndex1 = matchIndex * 2;
const parentIndex2 = parentIndex1 + 1;
const parentPos1 = matchPositions[roundIndex - 1][parentIndex1];
const parentPos2 = matchPositions[roundIndex - 1][parentIndex2];
top = (parentPos1 + parentPos2) / 2;
}
} else if (roundIndex < roundCount) {
const nextMatchesCount = rounds[roundIndex - 1].length;
const previousMatchesCount = rounds[roundIndex + 1].length;
if (currentMatchesCount == nextMatchesCount) {
nextMatchDistance = 0;
} else if (matchPositions.length > roundCount - roundIndex - 1 - 1) {
nextMatchDistance = (matchPositions[roundCount - roundIndex - 1 - 1][1] - matchPositions[roundCount - roundIndex - 1 - 1][0]);
nextMatchDistance = nextMatchDistance * (previousMatchesCount / currentMatchesCount);
}
}
if (roundCount > 5 && doubleButterflyMode == true) {
if (roundIndex >= finalRoundIndex - 2) {
if (roundIndex == finalRoundIndex - 1) {
matchDiv.classList.add('inward');
}
if (roundIndex == finalRoundIndex + 1) {
matchDiv.classList.add('outward');
}
if (roundIndex === finalRoundIndex - 2 || roundIndex === finalRoundIndex + 2) {
nextMatchDistance = nextMatchDistance - baseDistance;
}
}
}
if (displayLoserFinal == true) {
if (doubleButterflyMode == true && (roundIndex == finalRoundIndex - 1 || roundIndex == finalRoundIndex + 1)) {
nextMatchDistance = baseDistance;
}
}
matchDiv.style.setProperty('--next-match-distance', `${nextMatchDistance}px`);
matchDiv.style.top = `${top}px`;
matchPositions[roundIndex][matchIndex] = 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
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 = '50px';
} else if (roundIndex == finalRoundIndex + 1) {
titleDiv.style.marginLeft = '-50px';
}
}
matchesContainer.appendChild(titleDiv);
}
if (roundIndex == finalRoundIndex && matchIndex === 1 && 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 = `
<div class="incoming-line ${isIncomingLineIsDisabled ? 'disabled' : ''}"></div>
<div class="match-content ${isDisabled ? 'disabled' : ''}">${matchTemplate.innerHTML}</div>
`;
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 = `<div class="match-content">${rounds[0][0].innerHTML}</div>`;
matchesContainer.appendChild(matchDiv2); // Append to matchesContainer instead of roundDiv
}
matchesContainer.appendChild(matchDiv); // Append to matchesContainer instead of roundDiv
});
bracket.appendChild(roundDiv);
});
}
renderBracket();
const tournamentId = "{{ tournament.id }}";
renderBracket({
doubleButterflyMode: {{ double_butterfly_mode|lower }},
displayLoserFinal: {{ display_loser_final|lower }},
tournamentId: tournamentId
});
</script>
<style>
.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,
.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 */
padding: 5px 10px;
text-align: center;
font-weight: bold;
width: 100%; /* Change from 100% to auto */
}
.round-name {
color: #707070;
font-size: 1.5em;
padding: 8px 12px;
}
.round-name.button {
border-radius: 16px;
width: 100%;
display: inline-block;
background-color: #fae7ce;
}
.button:hover {
color: white;
background-color: #f39200;
}
.round-format {
font-size: 0.9em;
color: #707070;
margin-top: -10px;
}
.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%;
}
/* Horizontal line after match */
.butterfly-match::after {
content: "";
position: absolute;
left: 100%; /* Start from end of match cell */
top: 50%;
width: 20px;
height: 2px;
background: #707070;
}
.semi-final::after {
content: "";
position: absolute;
left: calc(100% + 20px); /* After horizontal line */
width: 2px;
height: calc((var(--next-match-distance)) / 2);
background: #707070;
}
/* Vertical line connecting pair of matches */
.butterfly-match:nth-child(2n)::before {
content: "";
position: absolute;
left: calc(100% + 20px); /* After horizontal line */
top: 50%;
width: 2px;
height: calc((var(--next-match-distance)) / 2);
background: #707070;
}
.butterfly-match:nth-child(2n+1)::before {
content: "";
position: absolute;
left: calc(100% + 20px);
bottom: calc(50% - 2px); /* Account for half of horizontal line height */
width: 2px;
height: calc(((var(--next-match-distance)) / 2)); /* Add half of horizontal line height */
background: #707070;
}
/* Vertical line connecting pair of matches */
.butterfly-match.reverse-bracket:nth-child(2n)::before {
content: "";
position: absolute;
left: calc(0% - 20px); /* After horizontal line */
top: 50%;
width: 2px;
height: calc((var(--next-match-distance)) / 2);
background: #707070;
}
.butterfly-match.reverse-bracket:nth-child(2n+1)::before {
content: "";
position: absolute;
left: calc(0% - 20px);
bottom: 50%; /* Account for half of horizontal line height */
width: 2px;
height: calc((var(--next-match-distance)) / 2); /* Add half of horizontal line height */
background: #707070;
}
/* Horizontal line to next round match */
.butterfly-match .incoming-line {
position: absolute;
left: -20px;
top: 50%;
width: 20px;
height: 2px;
background: #707070;
}
.inward .incoming-line {
display: none;
}
.butterfly-match.outward::after {
display: none;
}
.butterfly-round:last-child .butterfly-match::after,
.butterfly-round:first-child .incoming-line {
display: none;
}
</style>
{% endif %}
{% endblock %}

@ -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'),

@ -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

Loading…
Cancel
Save