Changeset 3670528
- Timestamp:
- 08/28/2026 02:32:46 PM (2 months ago)
- Location:
- netdraw
- Files:
-
- 30 added
- 14 edited
-
tags/1.1.0 (added)
-
tags/1.1.0/CHANGELOG.md (added)
-
tags/1.1.0/assets (added)
-
tags/1.1.0/assets/css (added)
-
tags/1.1.0/assets/css/admin.css (added)
-
tags/1.1.0/assets/css/frontend.css (added)
-
tags/1.1.0/assets/img (added)
-
tags/1.1.0/assets/img/banner-772x250.png (added)
-
tags/1.1.0/assets/img/icon-128x128.png (added)
-
tags/1.1.0/assets/img/netdraw-board.png (added)
-
tags/1.1.0/assets/img/netdraw-logo.png (added)
-
tags/1.1.0/assets/js (added)
-
tags/1.1.0/assets/js/admin.js (added)
-
tags/1.1.0/assets/js/frontend.js (added)
-
tags/1.1.0/assets/screenshots (added)
-
tags/1.1.0/assets/screenshots/screenshot-1.png (added)
-
tags/1.1.0/assets/screenshots/screenshot-2.png (added)
-
tags/1.1.0/languages (added)
-
tags/1.1.0/languages/netdraw-es_ES.mo (added)
-
tags/1.1.0/languages/netdraw-es_ES.po (added)
-
tags/1.1.0/languages/netdraw-fr_FR.mo (added)
-
tags/1.1.0/languages/netdraw-fr_FR.po (added)
-
tags/1.1.0/languages/netdraw-gl_ES.mo (added)
-
tags/1.1.0/languages/netdraw-gl_ES.po (added)
-
tags/1.1.0/languages/netdraw-it_IT.mo (added)
-
tags/1.1.0/languages/netdraw-it_IT.po (added)
-
tags/1.1.0/netdraw.php (added)
-
tags/1.1.0/readme.txt (added)
-
tags/1.1.0/uninstall.php (added)
-
trunk/CHANGELOG.md (added)
-
trunk/assets/css/admin.css (modified) (1 diff)
-
trunk/assets/css/frontend.css (modified) (3 diffs)
-
trunk/assets/js/admin.js (modified) (17 diffs)
-
trunk/assets/js/frontend.js (modified) (8 diffs)
-
trunk/languages/netdraw-es_ES.mo (modified) ( previous)
-
trunk/languages/netdraw-es_ES.po (modified) (2 diffs)
-
trunk/languages/netdraw-fr_FR.mo (modified) ( previous)
-
trunk/languages/netdraw-fr_FR.po (modified) (2 diffs)
-
trunk/languages/netdraw-gl_ES.mo (modified) ( previous)
-
trunk/languages/netdraw-gl_ES.po (modified) (2 diffs)
-
trunk/languages/netdraw-it_IT.mo (modified) ( previous)
-
trunk/languages/netdraw-it_IT.po (modified) (2 diffs)
-
trunk/netdraw.php (modified) (10 diffs)
-
trunk/readme.txt (modified) (8 diffs)
Legend:
- Unmodified
- Added
- Removed
-
netdraw/trunk/assets/css/admin.css
r3620277 r3670528 41 41 font-style: italic; 42 42 color: #646970; 43 } 44 45 .netdraw-draw-type-wrap { 46 display: flex; 47 align-items: center; 48 gap: 10px; 49 } 50 51 .netdraw-qualifying-spots-wrap { 52 display: flex; 53 align-items: center; 54 flex-wrap: wrap; 55 gap: 10px; 56 } 57 58 .netdraw-qualifying-spots-wrap .description { 59 flex-basis: 100%; 60 } 61 62 .netdraw-hidden { 63 display: none !important; 64 } 65 66 /* Qualifying Round column emphasis */ 67 .netdraw-admin-round-header.is-qualifying-round { 68 background: #e7eefb; 69 border-bottom-color: #2271b1; 70 } 71 72 .netdraw-admin-round-header.is-qualifying-round h3 { 73 color: #1d4e89; 74 } 75 76 .netdraw-admin-match-box.is-qualifying-round { 77 border-color: #9ec2e6; 78 background: #ffffff; 79 } 80 81 .netdraw-admin-match-box.is-qualifying-round:hover { 82 border-color: #2271b1; 83 } 84 85 /* Qualifier badge in the admin editor */ 86 .netdraw-admin-qualifier-badge { 87 display: inline-flex; 88 align-items: center; 89 justify-content: center; 90 min-width: 22px; 91 height: 22px; 92 padding: 0 6px; 93 font-size: 11px; 94 font-weight: 700; 95 color: #ffffff; 96 background: #2271b1; 97 border-radius: 12px; 98 flex-shrink: 0; 43 99 } 44 100 -
netdraw/trunk/assets/css/frontend.css
r3620277 r3670528 36 36 } 37 37 38 .netdraw-badge.qualifying { 39 background: #e7eefb; 40 color: #1d4e89; 41 border-color: #9ec2e6; 42 } 43 38 44 /* Scroll Wrapper to support wide screen layouts and mobile swiping */ 39 45 .netdraw-scroll-wrapper { … … 81 87 position: relative; 82 88 padding-right: 40px; /* Gap between rounds */ 89 } 90 91 /* Top level holds one card per top-round match (1 for Standard, N for Qualifying) */ 92 .netdraw-top-level { 93 display: flex; 94 flex-direction: column; 95 justify-content: flex-start; 83 96 } 84 97 … … 136 149 .netdraw-player.is-loser { 137 150 color: #94a3b8; 151 } 152 153 /* Qualified player badge in the qualifying round */ 154 .netdraw-qualified-badge { 155 display: inline-flex; 156 align-items: center; 157 justify-content: center; 158 min-width: 20px; 159 height: 20px; 160 padding: 0 6px; 161 margin-left: 6px; 162 font-size: 10px; 163 font-weight: 800; 164 line-height: 1; 165 color: #ffffff; 166 background: #2271b1; 167 border-radius: 12px; 168 flex-shrink: 0; 169 } 170 171 /* Qualifying round cards get a subtle blue tint so the draw truncation is obvious */ 172 .netdraw-card.is-qualifying-round { 173 border-color: #9ec2e6; 174 } 175 176 .netdraw-card.is-qualifying-round:hover { 177 border-color: #2271b1; 178 } 179 180 .netdraw-card.is-qualifying-round.has-winner { 181 border-color: #15803d; 182 box-shadow: 0 0 0 1px #15803d; 138 183 } 139 184 -
netdraw/trunk/assets/js/admin.js
r3620296 r3670528 1 1 document.addEventListener('DOMContentLoaded', function() { 2 2 const sizeSelect = document.getElementById('netdraw_bracket_size'); 3 const drawTypeSelect = document.getElementById('netdraw_draw_type'); 4 const spotsSelect = document.getElementById('netdraw_qualifying_spots'); 5 const spotsWrap = document.querySelector('.netdraw-qualifying-spots-wrap'); 3 6 const dataInput = document.getElementById('netdraw_bracket_data_input'); 4 7 const gridContainer = document.getElementById('netdraw_admin_grid_container'); 5 8 6 if (!sizeSelect || !d ataInput || !gridContainer) {9 if (!sizeSelect || !drawTypeSelect || !spotsSelect || !dataInput || !gridContainer) { 7 10 return; 8 11 } … … 17 20 18 21 // Load initial data 19 let bracketData = { size: 8, matches: {} };22 let bracketData = { size: 8, drawType: 'standard', qualifyingSpots: 4, matches: {} }; 20 23 if (typeof netdrawAdminData !== 'undefined' && netdrawAdminData.bracketData) { 21 24 bracketData = netdrawAdminData.bracketData; … … 23 26 bracketData.matches = {}; 24 27 } 28 if (bracketData.drawType !== 'qualifying' && bracketData.drawType !== 'standard') { 29 bracketData.drawType = 'standard'; 30 } 31 if (!bracketData.qualifyingSpots) { 32 bracketData.qualifyingSpots = 4; 33 } 34 } 35 36 function getTotalRounds() { 37 return Math.log2(bracketData.size); 38 } 39 40 // Number of rounds actually rendered / propagated. 41 // Qualifying draws stop at the round where remaining winners == qualifying spots. 42 function getEffectiveRounds() { 43 if (bracketData.drawType === 'qualifying' && bracketData.qualifyingSpots >= 2) { 44 return Math.log2(bracketData.size / bracketData.qualifyingSpots); 45 } 46 return getTotalRounds(); 47 } 48 49 // Repopulate the qualifying spots dropdown for the current bracket size. 50 function updateQualifyingSpotsOptions() { 51 const current = parseInt(bracketData.qualifyingSpots, 10); 52 spotsSelect.innerHTML = ''; 53 let matched = null; 54 for (let spot = 2; spot < bracketData.size; spot *= 2) { 55 const option = document.createElement('option'); 56 option.value = spot; 57 option.textContent = __('players_n', '%d Players').replace('%d', spot); 58 spotsSelect.appendChild(option); 59 if (spot === current) { 60 matched = spot; 61 } 62 } 63 if (!matched) { 64 // Falls back to half the bracket so the round count stays integral. 65 bracketData.qualifyingSpots = bracketData.size / 2; 66 matched = bracketData.qualifyingSpots; 67 } 68 spotsSelect.value = matched.toString(); 25 69 } 26 70 … … 28 72 function syncBracketData(size) { 29 73 bracketData.size = parseInt(size, 10); 30 const rounds = Math.log2(bracketData.size); 74 updateQualifyingSpotsOptions(); 75 const rounds = getTotalRounds(); 31 76 const newMatches = {}; 32 77 … … 46 91 } 47 92 48 // Propagate winners from round to round 93 // Propagate winners from round to round (stopping at the qualifying round) 49 94 function propagateWinners() { 50 const rounds = Math.log2(bracketData.size);51 52 // Clear all subsequent rounds' player names first, we will rebuild them53 for (let r = 2; r <= rounds; r++) {95 const effectiveRounds = getEffectiveRounds(); 96 97 // Clear all displayed subsequent rounds' player names first, we will rebuild them 98 for (let r = 2; r <= effectiveRounds; r++) { 54 99 const matchCount = bracketData.size / Math.pow(2, r); 55 100 for (let m = 1; m <= matchCount; m++) { … … 60 105 } 61 106 62 // Forward propagation loop 63 for (let r = 1; r < rounds; r++) {107 // Forward propagation loop (winners never propagate out of the qualifying round) 108 for (let r = 1; r < effectiveRounds; r++) { 64 109 const matchCount = bracketData.size / Math.pow(2, r); 65 110 for (let m = 1; m <= matchCount; m++) { 66 111 const matchId = `r${r}_m${m}`; 67 112 const match = bracketData.matches[matchId]; 68 113 69 114 let winnerName = ''; 70 115 if (match.winner === 'p1') { … … 106 151 // Get round display name 107 152 function getRoundName(r, totalRounds) { 108 if (r === totalRounds) { 153 if (bracketData.drawType === 'qualifying' && r === getEffectiveRounds()) { 154 return __('qualifying_round', 'Qualifying Round'); 155 } else if (r === totalRounds) { 109 156 return __('finals', 'Finals'); 110 157 } else if (r === totalRounds - 1) { … … 119 166 function renderEditor() { 120 167 gridContainer.innerHTML = ''; 121 const rounds = Math.log2(bracketData.size); 168 const rounds = getEffectiveRounds(); 169 const totalRounds = getTotalRounds(); 122 170 123 171 for (let r = 1; r <= rounds; r++) { 172 const isQualifyingRound = (bracketData.drawType === 'qualifying' && r === rounds); 124 173 const roundCol = document.createElement('div'); 125 174 roundCol.className = 'netdraw-admin-round-col'; 126 175 127 176 const roundHeader = document.createElement('div'); 128 roundHeader.className = 'netdraw-admin-round-header' ;129 roundHeader.innerHTML = `<h3>${getRoundName(r, rounds)}</h3><span class="netdraw-round-info">${__('round_n', 'Round %d').replace('%d', r)}</span>`;177 roundHeader.className = 'netdraw-admin-round-header' + (isQualifyingRound ? ' is-qualifying-round' : ''); 178 roundHeader.innerHTML = `<h3>${getRoundName(r, totalRounds)}</h3><span class="netdraw-round-info">${__('round_n', 'Round %d').replace('%d', r)}</span>`; 130 179 roundCol.appendChild(roundHeader); 131 180 … … 139 188 140 189 const matchBox = document.createElement('div'); 141 matchBox.className = `netdraw-admin-match-box ${match.winner ? 'has-winner' : ''}` ;190 matchBox.className = `netdraw-admin-match-box ${match.winner ? 'has-winner' : ''}` + (isQualifyingRound ? ' is-qualifying-round' : ''); 142 191 matchBox.dataset.matchId = matchId; 143 192 … … 168 217 p1Row.appendChild(p1Input); 169 218 219 if (isQualifyingRound && match.winner === 'p1') { 220 const qBadge = document.createElement('span'); 221 qBadge.className = 'netdraw-admin-qualifier-badge'; 222 qBadge.title = __('qualified', 'Qualified'); 223 qBadge.textContent = __('qualified_q', 'Q'); 224 p1Row.appendChild(qBadge); 225 } 226 170 227 const p1WinBtn = document.createElement('button'); 171 228 p1WinBtn.type = 'button'; … … 198 255 } 199 256 p2Row.appendChild(p2Input); 257 258 if (isQualifyingRound && match.winner === 'p2') { 259 const qBadge = document.createElement('span'); 260 qBadge.className = 'netdraw-admin-qualifier-badge'; 261 qBadge.title = __('qualified', 'Qualified'); 262 qBadge.textContent = __('qualified_q', 'Q'); 263 p2Row.appendChild(qBadge); 264 } 200 265 201 266 const p2WinBtn = document.createElement('button'); … … 301 366 } 302 367 }); 368 369 // Handle draw type change 370 if (drawTypeSelect) { 371 drawTypeSelect.addEventListener('change', function(e) { 372 const newType = e.target.value; 373 if (newType === bracketData.drawType) { 374 return; 375 } 376 if (confirm(__('confirm_type_change', 'Switching the draw type may hide or reveal progression matches depending on the qualifying spots. Do you want to proceed?'))) { 377 bracketData.drawType = newType; 378 if (spotsWrap) { 379 spotsWrap.classList.toggle('netdraw-hidden', newType !== 'qualifying'); 380 } 381 syncBracketData(bracketData.size); 382 renderEditor(); 383 } else { 384 drawTypeSelect.value = bracketData.drawType; 385 } 386 }); 387 } 388 389 // Handle qualifying spots change 390 if (spotsSelect) { 391 spotsSelect.addEventListener('change', function(e) { 392 const newSpots = parseInt(e.target.value, 10); 393 if (newSpots === bracketData.qualifyingSpots) { 394 return; 395 } 396 if (confirm(__('confirm_spots_change', 'Changing the qualifying spots will adjust which rounds are shown. Do you want to proceed?'))) { 397 bracketData.qualifyingSpots = newSpots; 398 syncBracketData(bracketData.size); 399 renderEditor(); 400 } else { 401 spotsSelect.value = bracketData.qualifyingSpots.toString(); 402 } 403 }); 404 } 303 405 304 406 // PDF/Print Download Action … … 402 504 const container = document.getElementById('print-area'); 403 505 506 const isQualifying = data.drawType === 'qualifying'; 507 const badgeText = isQualifying 508 ? ${JSON.stringify(__('qualifying_draw', '%d Player Qualifying Draw — %d Spots'))}.replace('%d', data.size).replace('%d', data.qualifyingSpots) 509 : ${JSON.stringify(__('knockout_draw', '%d Player Knockout Draw'))}.replace('%d', data.size); 510 404 511 // Header 405 512 const header = document.createElement('div'); 406 513 header.className = 'netdraw-frontend-header'; 407 header.innerHTML = '<h2>' + ${JSON.stringify(title)} + '</h2><span class="netdraw-badge ">' + ${JSON.stringify(__('knockout_draw', '%d Player Knockout Draw'))}.replace('%d', data.size)+ '</span>';514 header.innerHTML = '<h2>' + ${JSON.stringify(title)} + '</h2><span class="netdraw-badge' + (isQualifying ? ' qualifying' : '') + '">' + badgeText + '</span>'; 408 515 container.appendChild(header); 409 516 … … 416 523 const size = parseInt(data.size, 10); 417 524 const totalRounds = Math.log2(size); 418 419 function buildTreeNode(round, matchNum, totalRounds, matches) { 525 const effectiveRounds = isQualifying ? Math.log2(size / data.qualifyingSpots) : totalRounds; 526 527 function buildTreeNode(round, matchNum, effectiveRounds, matches) { 420 528 const matchId = 'r' + round + '_m' + matchNum; 421 529 const match = matches[matchId] || { p1: '', p2: '', score: '', winner: '', datetime: '' }; 422 530 531 const isQualifyingRound = isQualifying && round === effectiveRounds; 532 423 533 const node = document.createElement('div'); 424 534 node.className = 'netdraw-node'; 425 535 426 536 const matchCard = document.createElement('div'); 427 matchCard.className = 'netdraw-card' + (match.winner ? ' has-winner' : '') ;537 matchCard.className = 'netdraw-card' + (match.winner ? ' has-winner' : '') + (isQualifyingRound ? ' is-qualifying-round' : ''); 428 538 429 539 const p1Name = match.p1 || ''; … … 433 543 const p1Tbd = !p1Name ? 'is-tbd' : ''; 434 544 const p2Tbd = !p2Name ? 'is-tbd' : ''; 435 436 const p1Html = '<div class="netdraw-player p1-slot ' + p1Class + ' ' + p1Tbd + '"><span class="netdraw-pname">' + (p1Name || ${JSON.stringify(__('tbd', 'TBD'))}) + '</span></div>'; 437 const p2Html = '<div class="netdraw-player p2-slot ' + p2Class + ' ' + p2Tbd + '"><span class="netdraw-pname">' + (p2Name || ${JSON.stringify(__('tbd', 'TBD'))}) + '</span></div>'; 545 const qBadge = '<span class="netdraw-qualified-badge">' + ${JSON.stringify(__('qualified_q', 'Q'))} + '</span>'; 546 const p1Q = (isQualifyingRound && match.winner === 'p1') ? qBadge : ''; 547 const p2Q = (isQualifyingRound && match.winner === 'p2') ? qBadge : ''; 548 549 const p1Html = '<div class="netdraw-player p1-slot ' + p1Class + ' ' + p1Tbd + '"><span class="netdraw-pname">' + (p1Name || ${JSON.stringify(__('tbd', 'TBD'))}) + '</span>' + p1Q + '</div>'; 550 const p2Html = '<div class="netdraw-player p2-slot ' + p2Class + ' ' + p2Tbd + '"><span class="netdraw-pname">' + (p2Name || ${JSON.stringify(__('tbd', 'TBD'))}) + '</span>' + p2Q + '</div>'; 438 551 const scoreHtml = match.score ? '<div class="netdraw-score-row">' + match.score + '</div>' : ''; 439 552 const datetimeHtml = match.datetime ? '<div class="netdraw-datetime-row">' + match.datetime + '</div>' : ''; … … 445 558 const childrenContainer = document.createElement('div'); 446 559 childrenContainer.className = 'netdraw-children'; 447 childrenContainer.appendChild(buildTreeNode(round - 1, 2 * matchNum - 1, totalRounds, matches));448 childrenContainer.appendChild(buildTreeNode(round - 1, 2 * matchNum, totalRounds, matches));560 childrenContainer.appendChild(buildTreeNode(round - 1, 2 * matchNum - 1, effectiveRounds, matches)); 561 childrenContainer.appendChild(buildTreeNode(round - 1, 2 * matchNum, effectiveRounds, matches)); 449 562 node.appendChild(childrenContainer); 450 563 } … … 452 565 } 453 566 454 bracketTree.appendChild(buildTreeNode(totalRounds, 1, totalRounds, data.matches)); 567 const topMatchCount = Math.pow(2, totalRounds - effectiveRounds); 568 const topLevel = document.createElement('div'); 569 topLevel.className = 'netdraw-top-level'; 570 for (let m = 1; m <= topMatchCount; m++) { 571 topLevel.appendChild(buildTreeNode(effectiveRounds, m, effectiveRounds, data.matches)); 572 } 573 bracketTree.appendChild(topLevel); 455 574 456 575 // Dynamic zoom based on size to fit 1 page without layout clipping -
netdraw/trunk/assets/js/frontend.js
r3620296 r3670528 24 24 const size = parseInt(bracketData.size, 10); 25 25 const totalRounds = Math.log2(size); 26 const isQualifying = bracketData.drawType === 'qualifying' && bracketData.qualifyingSpots >= 2; 27 // Qualifying draws stop at the round where remaining winners equal the qualifying spots. 28 const effectiveRounds = isQualifying ? Math.log2(size / bracketData.qualifyingSpots) : totalRounds; 26 29 27 30 // Header for Tournament Info 28 31 const header = document.createElement('div'); 29 32 header.className = 'netdraw-frontend-header'; 33 let badgeText = __('knockout_draw', '%d Player Knockout Draw').replace('%d', size); 34 if (isQualifying) { 35 badgeText = __('qualifying_draw', '%d Player Qualifying Draw — %d Spots') 36 .replace('%d', size) 37 .replace('%d', bracketData.qualifyingSpots); 38 } 30 39 header.innerHTML = ` 31 40 <div class="netdraw-bracket-meta"> 32 <span class="netdraw-badge ">${escapeHtml(__('knockout_draw', '%d Player Knockout Draw').replace('%d', size))}</span>41 <span class="netdraw-badge${isQualifying ? ' qualifying' : ''}">${escapeHtml(badgeText)}</span> 33 42 </div> 34 43 `; … … 42 51 bracketTree.className = 'netdraw-bracket-tree'; 43 52 44 // Start recursive tree rendering from the Finals (root node) 45 // Finals is round = totalRounds, matchNum = 1 46 const rootNode = buildTreeNode(totalRounds, 1, totalRounds, bracketData.matches); 47 bracketTree.appendChild(rootNode); 53 // The top level holds every match card of the effective top round. 54 // Standard draws have a single Finals card; qualifying draws have one 55 // card per qualifying spot, each with its own feeder subtree below. 56 const topMatchCount = Math.pow(2, totalRounds - effectiveRounds); 57 const topLevel = document.createElement('div'); 58 topLevel.className = 'netdraw-top-level'; 59 for (let m = 1; m <= topMatchCount; m++) { 60 topLevel.appendChild(buildTreeNode(effectiveRounds, m, effectiveRounds, isQualifying, bracketData.matches)); 61 } 62 bracketTree.appendChild(topLevel); 48 63 49 64 scrollWrapper.appendChild(bracketTree); … … 55 70 56 71 /** 57 * Recursively builds tree nodes (Finals down to Round 1) 72 * Recursively builds tree nodes (top round down to Round 1). 73 * For qualifying draws, top round is the qualifying round. 58 74 */ 59 function buildTreeNode(round, matchNum, totalRounds, matches) {75 function buildTreeNode(round, matchNum, effectiveRounds, isQualifying, matches) { 60 76 const matchId = `r${round}_m${matchNum}`; 61 77 const match = matches[matchId] || { p1: '', p2: '', score: '', winner: '' }; 78 const isQualifyingRound = isQualifying && round === effectiveRounds; 62 79 63 80 const node = document.createElement('div'); … … 65 82 66 83 const matchCard = document.createElement('div'); 67 matchCard.className = `netdraw-card ${match.winner ? 'has-winner' : ''} `;84 matchCard.className = `netdraw-card ${match.winner ? 'has-winner' : ''} ${isQualifyingRound ? 'is-qualifying-round' : ''}`; 68 85 69 86 // Clean player names … … 77 94 const p2Tbd = !p2Name ? 'is-tbd' : ''; 78 95 96 const qualifiedBadge = `<span class="netdraw-qualified-badge" title="${escapeHtml(__('qualified', 'Qualified'))}">${escapeHtml(__('qualified_q', 'Q'))}</span>`; 97 79 98 // Player 1 DOM 80 99 const p1Html = ` 81 100 <div class="netdraw-player p1-slot ${p1Class} ${p1Tbd}" data-player-name="${p1Name}"> 82 101 <span class="netdraw-pname">${p1Name || escapeHtml(__('tbd', 'TBD'))}</span> 102 ${isQualifyingRound && match.winner === 'p1' ? qualifiedBadge : ''} 83 103 </div> 84 104 `; … … 88 108 <div class="netdraw-player p2-slot ${p2Class} ${p2Tbd}" data-player-name="${p2Name}"> 89 109 <span class="netdraw-pname">${p2Name || escapeHtml(__('tbd', 'TBD'))}</span> 110 ${isQualifyingRound && match.winner === 'p2' ? qualifiedBadge : ''} 90 111 </div> 91 112 `; … … 119 140 120 141 // Top feeder: previous round, match odd (2 * m - 1) 121 const topChild = buildTreeNode(round - 1, 2 * matchNum - 1, totalRounds, matches);142 const topChild = buildTreeNode(round - 1, 2 * matchNum - 1, effectiveRounds, isQualifying, matches); 122 143 // Bottom feeder: previous round, match even (2 * m) 123 const bottomChild = buildTreeNode(round - 1, 2 * matchNum, totalRounds, matches);144 const bottomChild = buildTreeNode(round - 1, 2 * matchNum, effectiveRounds, isQualifying, matches); 124 145 125 146 childrenContainer.appendChild(topChild); … … 180 201 const containers = document.querySelectorAll('.netdraw-bracket-container'); 181 202 containers.forEach(container => { 182 const rawData = container.getAttribute('data-bracket'); 183 if (rawData) { 184 try { 185 const bracketData = JSON.parse(rawData); 186 window.netdrawRenderInstance(container, bracketData); 187 } catch (e) { 188 console.error("NetDraw parsing error:", e); 189 container.innerHTML = '<div class="netdraw-error">' + escapeHtml(__('error_parsing', 'Error parsing bracket data.')) + '</div>'; 190 } 203 // Each container carries its own bracket data in the data-bracket attribute. 204 const bracketJson = container.getAttribute('data-bracket'); 205 if (!bracketJson) return; 206 207 let bracketData; 208 try { 209 bracketData = JSON.parse(bracketJson); 210 } catch (e) { 211 console.error("NetDraw parsing error:", e); 212 container.innerHTML = '<div class="netdraw-error">' + escapeHtml(__('error_parsing', 'Error parsing bracket data.')) + '</div>'; 213 return; 214 } 215 216 try { 217 window.netdrawRenderInstance(container, bracketData); 218 } catch (e) { 219 console.error("NetDraw parsing error:", e); 220 container.innerHTML = '<div class="netdraw-error">' + escapeHtml(__('error_parsing', 'Error parsing bracket data.')) + '</div>'; 191 221 } 192 222 }); -
netdraw/trunk/languages/netdraw-es_ES.po
r3620296 r3670528 1 1 msgid "" 2 2 msgstr "" 3 "Project-Id-Version: NetDraw 1. 0.0\n"3 "Project-Id-Version: NetDraw 1.1.0\n" 4 4 "Report-Msgid-Bugs-To: \n" 5 5 "POT-Creation-Date: 2026-07-23 18:41+0200\n" … … 158 158 msgid "Error parsing bracket data." 159 159 msgstr "Error al analizar los datos del cuadro." 160 161 msgid "Draw Type:" 162 msgstr "Tipo de cuadro:" 163 164 msgid "Standard" 165 msgstr "Estándar" 166 167 msgid "Qualifying" 168 msgstr "Clasificatorio" 169 170 msgid "Main Draw Qualifying Spots:" 171 msgstr "Plazas de clasificación al cuadro principal:" 172 173 msgid "Qualifying Round" 174 msgstr "Ronda de clasificación" 175 176 msgid "Qualified" 177 msgstr "Clasificado" 178 179 msgid "Q" 180 msgstr "Q" 181 182 msgid "%d Player Qualifying Draw — %d Spots" 183 msgstr "Cuadro clasificatorio de %d jugadores — %d plazas" 184 185 msgid "Switching the draw type may hide or reveal progression matches depending on the qualifying spots. Do you want to proceed?" 186 msgstr "Cambiar el tipo de cuadro puede ocultar o mostrar partidos de progresión según las plazas de clasificación. ¿Desea continuar?" 187 188 msgid "Changing the qualifying spots will adjust which rounds are shown. Do you want to proceed?" 189 msgstr "Cambiar las plazas de clasificación ajustará las rondas que se muestran. ¿Desea continuar?" 190 191 msgid "%d Players" 192 msgstr "%d Jugadores" 193 194 msgid "How many players advance to the main draw." 195 msgstr "Cuántos jugadores avanzan al cuadro principal." 196 197 msgid "%d Player" 198 msgid_plural "%d Players" 199 msgstr[0] "%d Jugador" 200 msgstr[1] "%d Jugadores" -
netdraw/trunk/languages/netdraw-fr_FR.po
r3620296 r3670528 1 1 msgid "" 2 2 msgstr "" 3 "Project-Id-Version: NetDraw 1. 0.0\n"3 "Project-Id-Version: NetDraw 1.1.0\n" 4 4 "Report-Msgid-Bugs-To: \n" 5 5 "POT-Creation-Date: 2026-07-23 18:41+0200\n" … … 158 158 msgid "Error parsing bracket data." 159 159 msgstr "Erreur lors de l'analyse des données du tableau." 160 161 msgid "Draw Type:" 162 msgstr "Type de tableau :" 163 164 msgid "Standard" 165 msgstr "Standard" 166 167 msgid "Qualifying" 168 msgstr "Qualifications" 169 170 msgid "Main Draw Qualifying Spots:" 171 msgstr "Places qualificatives pour le tableau principal :" 172 173 msgid "Qualifying Round" 174 msgstr "Tour de qualification" 175 176 msgid "Qualified" 177 msgstr "Qualifié" 178 179 msgid "Q" 180 msgstr "Q" 181 182 msgid "%d Player Qualifying Draw — %d Spots" 183 msgstr "Tableau de qualification de %d joueurs — %d places" 184 185 msgid "Switching the draw type may hide or reveal progression matches depending on the qualifying spots. Do you want to proceed?" 186 msgstr "Changer le type de tableau peut masquer ou révéler les matchs de progression selon les places qualificatives. Voulez-vous continuer ?" 187 188 msgid "Changing the qualifying spots will adjust which rounds are shown. Do you want to proceed?" 189 msgstr "Modifier les places qualificatives ajustera les tours affichés. Voulez-vous continuer ?" 190 191 msgid "%d Players" 192 msgstr "%d Joueurs" 193 194 msgid "How many players advance to the main draw." 195 msgstr "Combien de joueurs se qualifient pour le tableau principal ?" 196 197 msgid "%d Player" 198 msgid_plural "%d Players" 199 msgstr[0] "%d Joueur" 200 msgstr[1] "%d Joueurs" -
netdraw/trunk/languages/netdraw-gl_ES.po
r3620296 r3670528 1 1 msgid "" 2 2 msgstr "" 3 "Project-Id-Version: NetDraw 1. 0.0\n"3 "Project-Id-Version: NetDraw 1.1.0\n" 4 4 "Report-Msgid-Bugs-To: \n" 5 5 "POT-Creation-Date: 2026-07-23 18:45+0200\n" … … 158 158 msgid "Error parsing bracket data." 159 159 msgstr "Erro ao analizar os datos do cadro." 160 161 msgid "Draw Type:" 162 msgstr "Tipo de cadro:" 163 164 msgid "Standard" 165 msgstr "Estándar" 166 167 msgid "Qualifying" 168 msgstr "Clasificatorio" 169 170 msgid "Main Draw Qualifying Spots:" 171 msgstr "Prazas de clasificación ao cadro principal:" 172 173 msgid "Qualifying Round" 174 msgstr "Rolda de clasificación" 175 176 msgid "Qualified" 177 msgstr "Clasificado" 178 179 msgid "Q" 180 msgstr "Q" 181 182 msgid "%d Player Qualifying Draw — %d Spots" 183 msgstr "Cadro clasificatorio de %d xogadores — %d prazas" 184 185 msgid "Switching the draw type may hide or reveal progression matches depending on the qualifying spots. Do you want to proceed?" 186 msgstr "Cambiar o tipo de cadro pode ocultar ou mostrar partidos de progresión segundo as prazas de clasificación. ¿Quere continuar?" 187 188 msgid "Changing the qualifying spots will adjust which rounds are shown. Do you want to proceed?" 189 msgstr "Cambiar as prazas de clasificación axustará as roldas que se mostran. ¿Quere continuar?" 190 191 msgid "%d Players" 192 msgstr "%d Xogadores" 193 194 msgid "How many players advance to the main draw." 195 msgstr "Cantos xogadores avanzan ao cadro principal." 196 197 msgid "%d Player" 198 msgid_plural "%d Players" 199 msgstr[0] "%d Xogador" 200 msgstr[1] "%d Xogadores" -
netdraw/trunk/languages/netdraw-it_IT.po
r3620296 r3670528 1 1 msgid "" 2 2 msgstr "" 3 "Project-Id-Version: NetDraw 1. 0.0\n"3 "Project-Id-Version: NetDraw 1.1.0\n" 4 4 "Report-Msgid-Bugs-To: \n" 5 5 "POT-Creation-Date: 2026-07-23 18:41+0200\n" … … 158 158 msgid "Error parsing bracket data." 159 159 msgstr "Errore durante l'analisi dei dati del tabellone." 160 161 msgid "Draw Type:" 162 msgstr "Tipo di tabellone:" 163 164 msgid "Standard" 165 msgstr "Standard" 166 167 msgid "Qualifying" 168 msgstr "Qualificazioni" 169 170 msgid "Main Draw Qualifying Spots:" 171 msgstr "Posti di qualificazione per il tabellone principale:" 172 173 msgid "Qualifying Round" 174 msgstr "Turno di qualificazione" 175 176 msgid "Qualified" 177 msgstr "Qualificato" 178 179 msgid "Q" 180 msgstr "Q" 181 182 msgid "%d Player Qualifying Draw — %d Spots" 183 msgstr "Tabellone di qualificazione a %d giocatori — %d posti" 184 185 msgid "Switching the draw type may hide or reveal progression matches depending on the qualifying spots. Do you want to proceed?" 186 msgstr "Cambiare il tipo di tabellone può nascondere o mostrare le partite di progressione in base ai posti di qualificazione. Vuoi procedere?" 187 188 msgid "Changing the qualifying spots will adjust which rounds are shown. Do you want to proceed?" 189 msgstr "Modificare i posti di qualificazione regolerà i turni mostrati. Vuoi procedere?" 190 191 msgid "%d Players" 192 msgstr "%d Giocatori" 193 194 msgid "How many players advance to the main draw." 195 msgstr "Quanti giocatori avanzano al tabellone principale." 196 197 msgid "%d Player" 198 msgid_plural "%d Players" 199 msgstr[0] "%d Giocatore" 200 msgstr[1] "%d Giocatori" -
netdraw/trunk/netdraw.php
r3620296 r3670528 3 3 * Plugin Name: NetDraw 4 4 * Description: A lightweight, spreadsheet-style WordPress plugin for managing and displaying knockout tennis tournaments with visual brackets. 5 * Version: 1. 0.15 * Version: 1.1.0 6 6 * Author: Aida Martinez 7 7 * Text Domain: netdraw … … 20 20 // Constants 21 21 // --------------------------------------------------------------------------- 22 define( 'NETDRAW_VERSION', '1. 0.1' );22 define( 'NETDRAW_VERSION', '1.1.0' ); 23 23 define( 'NETDRAW_FILE', __FILE__ ); 24 24 define( 'NETDRAW_DIR', plugin_dir_path( __FILE__ ) ); … … 115 115 $bracket_data = wp_json_encode( 116 116 array( 117 'size' => 8, 118 'matches' => new stdClass(), 119 ) 117 'size' => 8, 118 'drawType' => 'standard', 119 'qualifyingSpots' => 4, 120 'matches' => new stdClass(), 121 ), 122 JSON_UNESCAPED_UNICODE | JSON_HEX_TAG 120 123 ); 121 124 } … … 128 131 'frontendCssUrl' => NETDRAW_URL . 'assets/css/frontend.css', 129 132 'strings' => array( 133 'draw_type' => __( 'Draw Type:', 'netdraw' ), 134 'standard' => __( 'Standard', 'netdraw' ), 135 'qualifying' => __( 'Qualifying', 'netdraw' ), 136 'qualifying_spots' => __( 'Main Draw Qualifying Spots:', 'netdraw' ), 137 'qualifying_round' => __( 'Qualifying Round', 'netdraw' ), 138 'qualified' => __( 'Qualified', 'netdraw' ), 139 'qualified_q' => __( 'Q', 'netdraw' ), 140 'qualifying_draw' => __( '%d Player Qualifying Draw — %d Spots', 'netdraw' ), 141 'confirm_type_change' => __( 'Switching the draw type may hide or reveal progression matches depending on the qualifying spots. Do you want to proceed?', 'netdraw' ), 142 'confirm_spots_change'=> __( 'Changing the qualifying spots will adjust which rounds are shown. Do you want to proceed?', 'netdraw' ), 130 143 'finals' => __( 'Finals', 'netdraw' ), 131 144 'semifinals' => __( 'Semifinals', 'netdraw' ), … … 134 147 'match_n' => __( 'Match %d', 'netdraw' ), 135 148 'player_n' => __( 'Player %d', 'netdraw' ), 149 'players_n' => __( '%d Players', 'netdraw' ), 136 150 'tbd' => __( 'TBD', 'netdraw' ), 137 151 'cannot_set_winner' => __( 'Cannot set winner for an empty player slot.', 'netdraw' ), … … 183 197 $bracket_data = json_decode( $bracket_data_json, true ); 184 198 $size = isset( $bracket_data['size'] ) ? intval( $bracket_data['size'] ) : 8; 199 $draw_type = isset( $bracket_data['drawType'] ) && 'qualifying' === $bracket_data['drawType'] ? 'qualifying' : 'standard'; 200 $qualifying_spots = isset( $bracket_data['qualifyingSpots'] ) ? intval( $bracket_data['qualifyingSpots'] ) : 4; 201 if ( $qualifying_spots < 2 || ( $qualifying_spots & ( $qualifying_spots - 1 ) ) !== 0 || $qualifying_spots >= $size ) { 202 $qualifying_spots = 4; 203 } 185 204 ?> 186 205 <div class="netdraw-admin-wrapper"> … … 197 216 </div> 198 217 218 <div class="netdraw-draw-type-wrap"> 219 <label for="netdraw_draw_type"><strong><?php esc_html_e( 'Draw Type:', 'netdraw' ); ?></strong></label> 220 <select id="netdraw_draw_type" name="netdraw_draw_type" class="postbox" style="margin: 0;"> 221 <option value="standard" <?php selected( $draw_type, 'standard' ); ?>><?php esc_html_e( 'Standard', 'netdraw' ); ?></option> 222 <option value="qualifying" <?php selected( $draw_type, 'qualifying' ); ?>><?php esc_html_e( 'Qualifying', 'netdraw' ); ?></option> 223 </select> 224 </div> 225 226 <div class="netdraw-qualifying-spots-wrap<?php echo 'standard' === $draw_type ? ' netdraw-hidden' : ''; ?>"> 227 <label for="netdraw_qualifying_spots"><strong><?php esc_html_e( 'Main Draw Qualifying Spots:', 'netdraw' ); ?></strong></label> 228 <select id="netdraw_qualifying_spots" name="netdraw_qualifying_spots" class="postbox" style="margin: 0;"> 229 <?php for ( $spot = 2; $spot < $size; $spot *= 2 ) : ?> 230 <option value="<?php echo esc_attr( $spot ); ?>" <?php selected( $qualifying_spots, $spot ); ?>><?php echo esc_html( sprintf( _n( '%d Player', '%d Players', $spot, 'netdraw' ), $spot ) ); ?></option> 231 <?php endfor; ?> 232 </select> 233 <p class="description" style="margin: 0;"><?php esc_html_e( 'How many players advance to the main draw.', 'netdraw' ); ?></p> 234 </div> 235 199 236 <div class="netdraw-shortcode-display" style="display: flex; align-items: center; gap: 8px;"> 200 237 <label><strong><?php esc_html_e( 'Shortcode:', 'netdraw' ); ?></strong></label> … … 245 282 246 283 // Sanitize input: decode JSON first, then sanitize individual fields. 247 // Do NOT run sanitize_textarea_field on raw JSON – it strips backslashes 248 // that are required for valid JSON encoding. 284 // Do NOT run wp_unslash() on raw JSON – stripslashes() removes the \ 285 // from JSON Unicode escapes (\u00eda), corrupting accented characters. 286 // WordPress 6.0+ no longer calls wp_magic_quotes(), so $_POST is raw. 249 287 if ( isset( $_POST['netdraw_bracket_data'] ) ) { 250 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized field-by-field after decodingbelow.251 $raw_json = wp_unslash( $_POST['netdraw_bracket_data'] );288 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Decoded via json_decode(); individual fields sanitized below. 289 $raw_json = $_POST['netdraw_bracket_data']; 252 290 $data_decoded = json_decode( $raw_json, true ); 291 292 // If decode failed, WordPress may have added slashes (wp_magic_quotes). 293 // Try again after removing them. 294 if ( ! is_array( $data_decoded ) ) { 295 $raw_json = wp_unslash( $raw_json ); 296 $data_decoded = json_decode( $raw_json, true ); 297 } 253 298 254 299 if ( is_array( $data_decoded ) ) { 255 300 $sanitized_matches = array(); 256 301 $size = isset( $data_decoded['size'] ) ? intval( $data_decoded['size'] ) : 8; 302 $draw_type = isset( $data_decoded['drawType'] ) && 'qualifying' === $data_decoded['drawType'] ? 'qualifying' : 'standard'; 303 $qualifying_spots = isset( $data_decoded['qualifyingSpots'] ) ? intval( $data_decoded['qualifyingSpots'] ) : 4; 304 305 // Qualifying spots must be a power of two and smaller than the bracket size. 306 if ( $qualifying_spots < 2 || ( $qualifying_spots & ( $qualifying_spots - 1 ) ) !== 0 || $qualifying_spots >= $size ) { 307 $qualifying_spots = 4; 308 } 257 309 258 310 if ( isset( $data_decoded['matches'] ) && is_array( $data_decoded['matches'] ) ) { … … 270 322 271 323 $sanitized_payload = array( 272 'size' => $size, 273 'matches' => $sanitized_matches, 324 'size' => $size, 325 'drawType' => $draw_type, 326 'qualifyingSpots' => $qualifying_spots, 327 'matches' => $sanitized_matches, 274 328 ); 275 329 276 330 // update_post_meta handles slashing internally; do not wrap with wp_slash(). 277 update_post_meta( $post_id, '_netdraw_bracket_data', wp_json_encode( $sanitized_payload ) );331 update_post_meta( $post_id, '_netdraw_bracket_data', wp_json_encode( $sanitized_payload, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG ) ); 278 332 } 279 333 } … … 303 357 wp_enqueue_script( 'netdraw-frontend-script' ); 304 358 305 wp_localize_script( 306 'netdraw-frontend-script', 307 'netdrawFrontData', 308 array( 309 'strings' => array( 310 'tbd' => __( 'TBD', 'netdraw' ), 311 'knockout_draw' => __( '%d Player Knockout Draw', 'netdraw' ), 312 'no_data' => __( 'No bracket data available.', 'netdraw' ), 313 'match_score' => __( 'Match Score', 'netdraw' ), 314 'match_datetime' => __( 'Match Date & Time', 'netdraw' ), 315 'error_parsing' => __( 'Error parsing bracket data.', 'netdraw' ), 316 ), 317 ) 318 ); 319 320 // Output HTML container with the JSON data directly in data attribute (no inline scripts) 359 // Localize the shared strings only once per page. Per-instance bracket 360 // data is passed inline below via the container's data-bracket attribute, 361 // so multiple shortcodes on the same page each render their own bracket. 362 static $strings_localized = false; 363 if ( ! $strings_localized ) { 364 $strings_localized = true; 365 wp_localize_script( 366 'netdraw-frontend-script', 367 'netdrawFrontData', 368 array( 369 'strings' => array( 370 'tbd' => __( 'TBD', 'netdraw' ), 371 'knockout_draw' => __( '%d Player Knockout Draw', 'netdraw' ), 372 'qualifying_draw' => __( '%d Player Qualifying Draw — %d Spots', 'netdraw' ), 373 'qualifying_round'=> __( 'Qualifying Round', 'netdraw' ), 374 'qualified' => __( 'Qualified', 'netdraw' ), 375 'qualified_q' => __( 'Q', 'netdraw' ), 376 'no_data' => __( 'No bracket data available.', 'netdraw' ), 377 'match_score' => __( 'Match Score', 'netdraw' ), 378 'match_datetime' => __( 'Match Date & Time', 'netdraw' ), 379 'error_parsing' => __( 'Error parsing bracket data.', 'netdraw' ), 380 ), 381 ) 382 ); 383 } 384 385 // Output HTML container (data is passed inline, not via wp_localize_script) 321 386 ob_start(); 322 387 ?> -
netdraw/trunk/readme.txt
r3620296 r3670528 5 5 Tested up to: 7.0 6 6 Requires PHP: 7.4 7 Stable tag: 1. 0.17 Stable tag: 1.1.0 8 8 License: GPL-2.0-or-later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 21 21 * **Simple Setup**: Create a new tournament draw just like writing a standard post or page in WordPress. 22 22 * **Flexible Sizing**: Choose your tournament size (8, 16, 32, or 64 players) via a simple dropdown selection. 23 * **Draw Types**: Choose between a **Standard** single-elimination draw and a **Qualifying** draw that stops once the configured number of players secure advancement spots into a main draw. 23 24 * **Easy Data Entry**: Dynamically generates a clean grid of input fields where you can type in player names for the first round and log scores as matches finish. 24 25 * **Automatic Progression**: Mark a player as the winner of a match and the plugin automatically pushes their name forward into the next round's slot — saving you from manual re-typing. … … 27 28 28 29 * **Visual Brackets**: Generates a clean, classic, tree-style tournament bracket on any page or post using a simple shortcode (e.g., `[netdraw id="123"]`). 30 * **Qualified (Q) Badges**: In qualifying draws, players who win their final qualifying match get a clear **Q** badge indicating they have advanced to the main draw. 29 31 * **Path Highlighting**: Hovering over any player highlights their entire journey (matches won, played, and progressed) across all rounds in real-time. 30 32 * **Responsive Design**: Uses a custom CSS tree-structure layout that enables smooth horizontal scrolling on mobile, tablet, and desktop screens without breaking the visual integrity. … … 47 49 3. Enter a title for your tournament (e.g., *Summer Club Open 2026*). 48 50 4. Under the **Tournament Bracket Editor** meta box, choose the tournament size (8, 16, 32, or 64 players). 49 5. Fill in the player names for **Round 1**. 50 6. As matches finish, fill in the score (e.g., `6-4 6-2`) and click the checkmark next to the winning player's name to automatically progress them to the next round. 51 7. Click **Publish** or **Update** to save. 51 5. Choose the **Draw Type**: 52 * **Standard** — the bracket plays out to a single champion and finalist. 53 * **Qualifying** — set the **Main Draw Qualifying Spots** (2, 4, 8, ...). The bracket stops at the round where the remaining winners equal that number; those players advance to the main draw. 54 6. Fill in the player names for **Round 1**. 55 7. As matches finish, fill in the score (e.g., `6-4 6-2`) and click the checkmark next to the winning player's name to automatically progress them to the next round. 56 8. Click **Publish** or **Update** to save. 52 57 53 58 = Displaying the Bracket = … … 63 68 = What tournament formats are supported? = 64 69 65 NetDraw currently supports single-elimination (knockout) brackets with 8, 16, 32, or 64 players. 70 NetDraw currently supports single-elimination (knockout) brackets with 8, 16, 32, or 64 players. Draws can be **Standard** (crowned through to a champion) or **Qualifying** (stopped once the configured number of players have secured their advancement spots). 66 71 67 72 = Can I use this for sports other than tennis? = … … 97 102 98 103 { 99 "size": 8, 104 "size": 16, 105 "drawType": "qualifying", 106 "qualifyingSpots": 4, 100 107 "matches": { 101 108 "r1_m1": { "p1": "Federer", "p2": "Nadal", "score": "6-4 6-2", "winner": "p1" }, 102 109 "r1_m2": { "p1": "Djokovic", "p2": "Murray", "score": "7-5 6-3", "winner": "p1" }, 103 "r2_m1": { "p1": "Federer", "p2": " Djokovic", "score": "", "winner": "" }110 "r2_m1": { "p1": "Federer", "p2": "Sinner", "score": "", "winner": "" } 104 111 } 105 112 } 113 114 * `drawType` is `"standard"` or `"qualifying"`. 115 * `qualifyingSpots` is only meaningful for qualifying draws; it is always a power of two and smaller than the bracket size. 116 * For qualifying draws, the bracket renders only up to `effectiveRounds = log2(size / qualifyingSpots)` rounds. Match data beyond that (if any) is preserved but hidden, so switching back to Standard is non-destructive. 106 117 107 118 = Dynamic Progression Logic = … … 113 124 * If m is even, the winner becomes Player 2 (p2) of the next match. 114 125 * Subsequent rounds are set to read-only in the admin panel to prevent data out-of-sync issues. 126 * In a qualifying draw, propagation stops at the qualifying round: winners there receive a **Qualified (Q)** badge instead of being pushed into another round. 115 127 116 128 == Changelog == 129 130 = 1.1.0 = 131 * Added **Qualifying Draw** support: choose Standard or Qualifying draws with configurable Main Draw Qualifying Spots (2, 4, 8, ...). 132 * Qualifying brackets truncate automatically at the round where remaining winners equal the qualifying spots. 133 * Added **Qualified (Q)** advancement badges and a dedicated Qualifying Round label in both the admin editor and the frontend bracket. 134 * Preserves hidden bracket data so switching between draw types is non-destructive. 135 * Fixed multiple shortcodes on the same page each rendering their own bracket instead of always showing the last one. 117 136 118 137 = 1.0.1 = … … 129 148 == Upgrade Notice == 130 149 150 = 1.1.0 = 151 Added qualifying draw support with configurable main draw qualifying spots and Qualified (Q) badges. 152 131 153 = 1.0.1 = 132 154 Added translation support for Spanish, French, Italian, and Galician.
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)