Created
August 14, 2026 15:52
-
-
Save EncodeTheCode/77c6ac91a379f3973a044d1590742a0c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta | |
| name="viewport" | |
| content="width=device-width, initial-scale=1.0" | |
| > | |
| <title>CS2 Tournament Bracket</title> | |
| <style> | |
| /* ========================================================= | |
| ROOT / THEME | |
| ========================================================= */ | |
| :root { | |
| --bg: #080a0d; | |
| --bg-soft: #0d1115; | |
| --panel: #11161b; | |
| --panel-2: #171d23; | |
| --panel-3: #1c232a; | |
| --border: #2c353e; | |
| --border-soft: rgba(255,255,255,.055); | |
| --text: #e7ebee; | |
| --muted: #7f8992; | |
| --muted-2: #626b73; | |
| --accent: #d6a84f; | |
| --accent-soft: rgba(214,168,79,.16); | |
| --winner-bg: #b9ddc0; | |
| --winner-bg-2: #a7d0ae; | |
| --winner-text: #18351e; | |
| --eliminated-bg: #686d71; | |
| --eliminated-bg-2: #565b5f; | |
| --eliminated-text: #363a3d; | |
| --card-width: 250px; | |
| --card-height: 94px; | |
| --round-gap: 100px; | |
| --match-gap: 28px; | |
| --shadow: | |
| 0 15px 40px rgba(0,0,0,.42); | |
| --emboss: | |
| 1px 1px 0 rgba(255,255,255,.10), | |
| -1px -1px 0 rgba(0,0,0,.50); | |
| } | |
| /* ========================================================= | |
| RESET | |
| ========================================================= */ | |
| * { | |
| box-sizing: border-box; | |
| } | |
| html { | |
| min-height: 100%; | |
| } | |
| body { | |
| margin: 0; | |
| min-height: 100vh; | |
| color: var(--text); | |
| background: | |
| radial-gradient( | |
| circle at 50% -10%, | |
| rgba(214,168,79,.10), | |
| transparent 35% | |
| ), | |
| radial-gradient( | |
| circle at 10% 100%, | |
| rgba(255,255,255,.025), | |
| transparent 30% | |
| ), | |
| var(--bg); | |
| font-family: | |
| Inter, | |
| ui-sans-serif, | |
| system-ui, | |
| -apple-system, | |
| BlinkMacSystemFont, | |
| "Segoe UI", | |
| sans-serif; | |
| } | |
| /* ========================================================= | |
| APP | |
| ========================================================= */ | |
| .app { | |
| min-height: 100vh; | |
| padding: | |
| 28px | |
| 28px | |
| 50px; | |
| } | |
| /* ========================================================= | |
| HEADER | |
| ========================================================= */ | |
| .header { | |
| max-width: 1900px; | |
| margin: | |
| 0 auto | |
| 22px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-end; | |
| gap: 30px; | |
| } | |
| .brand { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 7px; | |
| } | |
| .brand-eyebrow { | |
| color: var(--accent); | |
| font-size: 10px; | |
| font-weight: 900; | |
| text-transform: uppercase; | |
| letter-spacing: .18em; | |
| } | |
| .brand h1 { | |
| margin: 0; | |
| font-size: 29px; | |
| line-height: 1; | |
| font-weight: 900; | |
| letter-spacing: -.025em; | |
| text-transform: uppercase; | |
| } | |
| .brand p { | |
| margin: 0; | |
| color: var(--muted); | |
| font-size: 12px; | |
| font-weight: 600; | |
| } | |
| /* ========================================================= | |
| CONTROLS | |
| ========================================================= */ | |
| .controls { | |
| display: flex; | |
| align-items: center; | |
| gap: 7px; | |
| flex-wrap: wrap; | |
| } | |
| .control-label { | |
| color: var(--muted); | |
| font-size: 9px; | |
| font-weight: 900; | |
| text-transform: uppercase; | |
| letter-spacing: .12em; | |
| margin-right: 3px; | |
| } | |
| .layout-button { | |
| appearance: none; | |
| border: 1px solid var(--border); | |
| background: var(--panel); | |
| color: var(--muted); | |
| padding: | |
| 9px | |
| 13px; | |
| border-radius: 3px; | |
| font-size: 10px; | |
| font-weight: 900; | |
| text-transform: uppercase; | |
| letter-spacing: .08em; | |
| cursor: pointer; | |
| transition: | |
| background .15s ease, | |
| color .15s ease, | |
| border-color .15s ease, | |
| transform .15s ease; | |
| } | |
| .layout-button:hover { | |
| color: var(--text); | |
| border-color: #56616b; | |
| transform: | |
| translateY(-1px); | |
| } | |
| .layout-button.active { | |
| color: #18140d; | |
| background: var(--accent); | |
| border-color: var(--accent); | |
| } | |
| /* ========================================================= | |
| MODE INDICATOR | |
| ========================================================= */ | |
| .mode-indicator { | |
| margin-left: 10px; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 7px; | |
| padding: | |
| 7px | |
| 10px; | |
| border: | |
| 1px solid var(--border); | |
| background: var(--panel); | |
| color: var(--muted); | |
| font-size: 9px; | |
| font-weight: 900; | |
| letter-spacing: .08em; | |
| text-transform: uppercase; | |
| } | |
| .mode-dot { | |
| width: 6px; | |
| height: 6px; | |
| border-radius: 50%; | |
| background: #6f7880; | |
| } | |
| .mode-indicator.live .mode-dot { | |
| background: #7dcc8b; | |
| box-shadow: | |
| 0 0 8px | |
| rgba(125,204,139,.55); | |
| } | |
| /* ========================================================= | |
| BRACKET SHELL | |
| ========================================================= */ | |
| .bracket-shell { | |
| max-width: 1900px; | |
| margin: 0 auto; | |
| min-height: 720px; | |
| overflow: auto; | |
| border: | |
| 1px solid var(--border); | |
| background: | |
| linear-gradient( | |
| 135deg, | |
| rgba(255,255,255,.018), | |
| transparent 40% | |
| ), | |
| var(--bg-soft); | |
| box-shadow: var(--shadow); | |
| } | |
| .bracket { | |
| min-width: max-content; | |
| min-height: 720px; | |
| padding: | |
| 65px | |
| 70px; | |
| } | |
| /* ========================================================= | |
| ROUND | |
| ========================================================= */ | |
| .round { | |
| width: var(--card-width); | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .round-title { | |
| height: 35px; | |
| display: flex; | |
| align-items: center; | |
| color: var(--muted); | |
| border-bottom: | |
| 1px solid var(--border); | |
| margin-bottom: 25px; | |
| font-size: 10px; | |
| font-weight: 900; | |
| text-transform: uppercase; | |
| letter-spacing: .13em; | |
| } | |
| .round-matches { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: space-around; | |
| gap: var(--match-gap); | |
| } | |
| /* ========================================================= | |
| CLASSIC | |
| ========================================================= */ | |
| .bracket.classic { | |
| display: flex; | |
| align-items: stretch; | |
| gap: var(--round-gap); | |
| } | |
| /* ========================================================= | |
| MATCH | |
| ========================================================= */ | |
| .match { | |
| width: var(--card-width); | |
| position: relative; | |
| overflow: hidden; | |
| background: | |
| linear-gradient( | |
| 180deg, | |
| rgba(255,255,255,.035), | |
| rgba(255,255,255,.008) | |
| ), | |
| var(--panel); | |
| border: | |
| 1px solid var(--border); | |
| border-radius: 3px; | |
| box-shadow: | |
| 0 7px 20px rgba(0,0,0,.25); | |
| transition: | |
| border-color .15s ease, | |
| box-shadow .15s ease, | |
| transform .15s ease; | |
| } | |
| .match:hover { | |
| border-color: | |
| rgba(255,255,255,.18); | |
| box-shadow: | |
| 0 12px 30px rgba(0,0,0,.38); | |
| } | |
| .match.interactive:hover { | |
| transform: | |
| translateY(-2px); | |
| } | |
| .match-header { | |
| height: 28px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: | |
| 0 10px; | |
| background: | |
| rgba(255,255,255,.025); | |
| border-bottom: | |
| 1px solid var(--border-soft); | |
| color: var(--muted); | |
| font-size: 9px; | |
| font-weight: 900; | |
| text-transform: uppercase; | |
| letter-spacing: .08em; | |
| } | |
| .match-header .series { | |
| color: var(--muted-2); | |
| } | |
| .match-header .match-id { | |
| color: var(--muted); | |
| } | |
| /* ========================================================= | |
| TEAM ENTRY | |
| ========================================================= */ | |
| .team { | |
| min-height: 34px; | |
| display: grid; | |
| grid-template-columns: | |
| 29px | |
| minmax(0,1fr) | |
| 32px; | |
| align-items: center; | |
| gap: 7px; | |
| padding: | |
| 0 10px; | |
| border-bottom: | |
| 1px solid var(--border-soft); | |
| cursor: pointer; | |
| transition: | |
| background .15s ease, | |
| filter .15s ease; | |
| } | |
| .team:last-child { | |
| border-bottom: 0; | |
| } | |
| .team:hover { | |
| background: | |
| rgba(255,255,255,.035); | |
| } | |
| .team.readonly { | |
| cursor: default; | |
| } | |
| .team.readonly:hover { | |
| background: inherit; | |
| } | |
| /* ========================================================= | |
| TEAM LOGO | |
| ========================================================= */ | |
| .team-logo { | |
| width: 22px; | |
| height: 22px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| overflow: hidden; | |
| border: | |
| 1px solid #3a424b; | |
| border-radius: 2px; | |
| background: | |
| #222930; | |
| color: #d7dde2; | |
| font-size: 8px; | |
| font-weight: 900; | |
| } | |
| .team-logo img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: contain; | |
| } | |
| /* ========================================================= | |
| TEAM NAME | |
| ========================================================= */ | |
| .team-name { | |
| min-width: 0; | |
| overflow: hidden; | |
| white-space: nowrap; | |
| text-overflow: ellipsis; | |
| font-size: 12px; | |
| font-weight: 800; | |
| letter-spacing: .01em; | |
| } | |
| .team-seed { | |
| margin-right: 4px; | |
| color: var(--muted); | |
| font-size: 9px; | |
| font-weight: 700; | |
| } | |
| .score { | |
| text-align: right; | |
| font-size: 13px; | |
| font-weight: 950; | |
| } | |
| /* ========================================================= | |
| WINNER | |
| ========================================================= */ | |
| .team.winner { | |
| background: | |
| linear-gradient( | |
| 90deg, | |
| var(--winner-bg), | |
| var(--winner-bg-2) | |
| ); | |
| color: var(--winner-text); | |
| } | |
| .team.winner .team-seed { | |
| color: #42614a; | |
| } | |
| .team.winner .team-logo { | |
| background: | |
| rgba(255,255,255,.35); | |
| border-color: | |
| rgba(0,0,0,.13); | |
| } | |
| /* ========================================================= | |
| ELIMINATED | |
| ========================================================= */ | |
| .team.eliminated { | |
| background: | |
| linear-gradient( | |
| 180deg, | |
| var(--eliminated-bg), | |
| var(--eliminated-bg-2) | |
| ); | |
| color: var(--eliminated-text); | |
| text-decoration: | |
| line-through; | |
| text-decoration-thickness: 1px; | |
| text-decoration-color: | |
| rgba(40,43,45,.85); | |
| text-shadow: | |
| 1px 1px 0 rgba(255,255,255,.12), | |
| -1px -1px 0 rgba(0,0,0,.55); | |
| filter: | |
| saturate(.45); | |
| } | |
| .team.eliminated .team-logo { | |
| filter: | |
| grayscale(1); | |
| opacity: .62; | |
| } | |
| .team.eliminated .team-seed { | |
| color: | |
| #474b4e; | |
| } | |
| .team.eliminated .score { | |
| color: | |
| #414548; | |
| } | |
| /* ========================================================= | |
| TBD | |
| ========================================================= */ | |
| .team.tbd { | |
| color: | |
| var(--muted-2); | |
| } | |
| .team.tbd .team-logo { | |
| color: | |
| #596169; | |
| border-color: | |
| #303840; | |
| } | |
| /* ========================================================= | |
| FINAL | |
| ========================================================= */ | |
| .match.final-match { | |
| border-color: | |
| var(--accent); | |
| box-shadow: | |
| 0 0 0 1px | |
| rgba(214,168,79,.10), | |
| 0 18px 50px | |
| rgba(0,0,0,.48); | |
| } | |
| .final-match .match-header { | |
| color: | |
| var(--accent); | |
| background: | |
| linear-gradient( | |
| 90deg, | |
| var(--accent-soft), | |
| transparent | |
| ); | |
| } | |
| .final-match .match-header .series { | |
| color: | |
| var(--accent); | |
| } | |
| /* ========================================================= | |
| CHAMPION | |
| ========================================================= */ | |
| .champion { | |
| padding: | |
| 12px; | |
| text-align: center; | |
| background: | |
| linear-gradient( | |
| 180deg, | |
| rgba(214,168,79,.20), | |
| rgba(214,168,79,.035) | |
| ); | |
| border-top: | |
| 1px solid var(--accent); | |
| color: | |
| var(--accent); | |
| font-size: 9px; | |
| font-weight: 950; | |
| text-transform: uppercase; | |
| letter-spacing: .16em; | |
| } | |
| .champion-name { | |
| display: block; | |
| margin-top: 5px; | |
| color: | |
| #f3f4f5; | |
| font-size: 16px; | |
| font-weight: 950; | |
| letter-spacing: .015em; | |
| } | |
| /* ========================================================= | |
| CENTERED LAYOUT | |
| ========================================================= */ | |
| .bracket.centered { | |
| display: grid; | |
| grid-template-columns: | |
| 1fr | |
| auto | |
| 1fr; | |
| gap: | |
| 95px; | |
| align-items: center; | |
| } | |
| .center-side { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 60px; | |
| } | |
| .center-side.left { | |
| align-items: flex-end; | |
| } | |
| .center-side.right { | |
| align-items: flex-start; | |
| } | |
| .center-final { | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| } | |
| /* ========================================================= | |
| COMPACT LAYOUT | |
| ========================================================= */ | |
| .bracket.compact { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 28px; | |
| } | |
| .compact-round { | |
| display: grid; | |
| grid-template-columns: | |
| 135px | |
| 1fr; | |
| gap: 18px; | |
| align-items: stretch; | |
| } | |
| .compact-round > .round-title { | |
| height: auto; | |
| margin: 0; | |
| align-items: flex-start; | |
| padding-top: 8px; | |
| } | |
| .compact-round .round-matches { | |
| display: grid; | |
| grid-template-columns: | |
| repeat( | |
| auto-fit, | |
| minmax( | |
| var(--card-width), | |
| 1fr | |
| ) | |
| ); | |
| gap: 12px; | |
| } | |
| /* ========================================================= | |
| SPECTATOR MODE | |
| ========================================================= */ | |
| body.spectator-mode .team { | |
| cursor: default; | |
| } | |
| body.spectator-mode .team:hover { | |
| background: inherit; | |
| } | |
| body.spectator-mode .match:hover { | |
| transform: none; | |
| } | |
| /* ========================================================= | |
| RESPONSIVE | |
| ========================================================= */ | |
| @media (max-width: 900px) { | |
| .app { | |
| padding: 12px; | |
| } | |
| .header { | |
| align-items: | |
| flex-start; | |
| flex-direction: | |
| column; | |
| } | |
| .bracket { | |
| padding: | |
| 40px; | |
| } | |
| :root { | |
| --card-width: | |
| 215px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <!-- ===================================================== | |
| HEADER | |
| ====================================================== --> | |
| <header class="header"> | |
| <div class="brand"> | |
| <div class="brand-eyebrow"> | |
| Counter-Strike 2 · Playoffs | |
| </div> | |
| <h1 id="tournamentTitle"> | |
| IEM CS2 Championship | |
| </h1> | |
| <p id="tournamentSubtitle"> | |
| Playoffs · Single Elimination | |
| </p> | |
| </div> | |
| <div class="controls"> | |
| <span class="control-label"> | |
| Layout | |
| </span> | |
| <button | |
| class="layout-button active" | |
| data-layout="classic" | |
| > | |
| Classic | |
| </button> | |
| <button | |
| class="layout-button" | |
| data-layout="centered" | |
| > | |
| Centered | |
| </button> | |
| <button | |
| class="layout-button" | |
| data-layout="compact" | |
| > | |
| Compact | |
| </button> | |
| <div | |
| id="modeIndicator" | |
| class="mode-indicator" | |
| > | |
| <span class="mode-dot"></span> | |
| <span id="modeText"> | |
| Spectator | |
| </span> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- ===================================================== | |
| BRACKET | |
| ====================================================== --> | |
| <main class="bracket-shell"> | |
| <div | |
| id="bracket" | |
| class="bracket classic" | |
| ></div> | |
| </main> | |
| </div> | |
| <script> | |
| /* ========================================================= | |
| ========================================================= | |
| CS2 TOURNAMENT BRACKET CONFIGURATION | |
| ========================================================= | |
| ========================================================= */ | |
| /* | |
| --------------------------------------------------------- | |
| LAYOUT | |
| --------------------------------------------------------- | |
| "classic" | |
| Traditional left → right bracket. | |
| "centered" | |
| Bracket converges around the Grand Final. | |
| "compact" | |
| Dense tournament-board layout. | |
| */ | |
| let LAYOUT_STYLE = "classic"; | |
| /* | |
| --------------------------------------------------------- | |
| INTERACTION MODE | |
| --------------------------------------------------------- | |
| true: | |
| Teams can be clicked. | |
| Clicking a team advances it. | |
| false: | |
| Spectator mode. | |
| Nothing can be clicked. | |
| The bracket displays only the | |
| state supplied by your data. | |
| This is the main switch you requested. | |
| */ | |
| const CONFIG = { | |
| title: | |
| "IEM CS2 Championship", | |
| subtitle: | |
| "Playoffs · Single Elimination", | |
| /* | |
| CHANGE THIS TO true IF YOU WANT | |
| CLICKABLE MATCHES. | |
| */ | |
| interactiveMode: | |
| false, | |
| /* | |
| Default series length. | |
| */ | |
| bestOf: | |
| "BO3", | |
| /* | |
| Accent color used for | |
| Grand Final / tournament branding. | |
| */ | |
| accent: | |
| "#d6a84f", | |
| /* | |
| Display options. | |
| */ | |
| showSeeds: | |
| true, | |
| showScores: | |
| true, | |
| /* | |
| Layout dimensions. | |
| */ | |
| cardWidth: | |
| 250, | |
| roundGap: | |
| 100, | |
| matchGap: | |
| 28 | |
| }; | |
| /* ========================================================= | |
| TEAM DATA | |
| ========================================================= */ | |
| /* | |
| You can replace this entire array. | |
| The bracket will automatically resize | |
| itself according to the number of teams. | |
| Supported properties: | |
| id | |
| name | |
| seed | |
| logo | |
| color | |
| status | |
| score | |
| status can be: | |
| "active" | |
| "winner" | |
| "eliminated" | |
| */ | |
| const contestants = [ | |
| { | |
| id: "spirit", | |
| name: "Spirit", | |
| seed: 1, | |
| logo: "", | |
| color: "#d83b3b" | |
| }, | |
| { | |
| id: "vitality", | |
| name: "Vitality", | |
| seed: 2, | |
| logo: "", | |
| color: "#e4bd42" | |
| }, | |
| { | |
| id: "navi", | |
| name: "Natus Vincere", | |
| seed: 3, | |
| logo: "", | |
| color: "#e4d449" | |
| }, | |
| { | |
| id: "g2", | |
| name: "G2 Esports", | |
| seed: 4, | |
| logo: "", | |
| color: "#ffffff" | |
| }, | |
| { | |
| id: "faze", | |
| name: "FaZe", | |
| seed: 5, | |
| logo: "", | |
| color: "#d32f2f" | |
| }, | |
| { | |
| id: "mouz", | |
| name: "MOUZ", | |
| seed: 6, | |
| logo: "", | |
| color: "#d9a43b" | |
| }, | |
| { | |
| id: "astralis", | |
| name: "Astralis", | |
| seed: 7, | |
| logo: "", | |
| color: "#d71920" | |
| }, | |
| { | |
| id: "liquid", | |
| name: "Liquid", | |
| seed: 8, | |
| logo: "", | |
| color: "#5d8ed8" | |
| } | |
| ]; | |
| /* ========================================================= | |
| PREDEFINED TOURNAMENT STATE | |
| ========================================================= */ | |
| /* | |
| THIS IS THE IMPORTANT PART FOR | |
| SPECTATOR MODE. | |
| You don't have to click anything. | |
| You can define the tournament state | |
| directly here. | |
| Each match can have: | |
| teams | |
| winner | |
| scores | |
| statuses | |
| bestOf | |
| The renderer simply displays it. | |
| */ | |
| const PREDEFINED_MATCHES = [ | |
| /* | |
| ----------------------------------------------- | |
| QUARTERFINAL 1 | |
| ----------------------------------------------- | |
| */ | |
| { | |
| id: "qf1", | |
| round: "Quarterfinals", | |
| bestOf: "BO3", | |
| teams: [ | |
| { | |
| id: "navi", | |
| name: "Natus Vincere", | |
| seed: 3, | |
| score: 2, | |
| status: "winner", | |
| logo: "", | |
| color: "#e4d449" | |
| }, | |
| { | |
| id: "faze", | |
| name: "FaZe", | |
| seed: 5, | |
| score: 1, | |
| status: "eliminated", | |
| logo: "", | |
| color: "#d32f2f" | |
| } | |
| ], | |
| winner: "navi" | |
| }, | |
| /* | |
| ----------------------------------------------- | |
| QUARTERFINAL 2 | |
| ----------------------------------------------- | |
| */ | |
| { | |
| id: "qf2", | |
| round: "Quarterfinals", | |
| bestOf: "BO3", | |
| teams: [ | |
| { | |
| id: "spirit", | |
| name: "Spirit", | |
| seed: 1, | |
| score: 2, | |
| status: "winner", | |
| logo: "", | |
| color: "#d83b3b" | |
| }, | |
| { | |
| id: "g2", | |
| name: "G2 Esports", | |
| seed: 4, | |
| score: 0, | |
| status: "eliminated", | |
| logo: "", | |
| color: "#ffffff" | |
| } | |
| ], | |
| winner: "spirit" | |
| }, | |
| /* | |
| ----------------------------------------------- | |
| QUARTERFINAL 3 | |
| ----------------------------------------------- | |
| */ | |
| { | |
| id: "qf3", | |
| round: "Quarterfinals", | |
| bestOf: "BO3", | |
| teams: [ | |
| { | |
| id: "vitality", | |
| name: "Vitality", | |
| seed: 2, | |
| score: 2, | |
| status: "winner", | |
| logo: "", | |
| color: "#e4bd42" | |
| }, | |
| { | |
| id: "astralis", | |
| name: "Astralis", | |
| seed: 7, | |
| score: 1, | |
| status: "eliminated", | |
| logo: "", | |
| color: "#d71920" | |
| } | |
| ], | |
| winner: "vitality" | |
| }, | |
| /* | |
| ----------------------------------------------- | |
| QUARTERFINAL 4 | |
| ----------------------------------------------- | |
| */ | |
| { | |
| id: "qf4", | |
| round: "Quarterfinals", | |
| bestOf: "BO3", | |
| teams: [ | |
| { | |
| id: "mouz", | |
| name: "MOUZ", | |
| seed: 6, | |
| score: 2, | |
| status: "winner", | |
| logo: "", | |
| color: "#d9a43b" | |
| }, | |
| { | |
| id: "liquid", | |
| name: "Liquid", | |
| seed: 8, | |
| score: 1, | |
| status: "eliminated", | |
| logo: "", | |
| color: "#5d8ed8" | |
| } | |
| ], | |
| winner: "mouz" | |
| }, | |
| /* | |
| ----------------------------------------------- | |
| SEMIFINAL 1 | |
| ----------------------------------------------- | |
| */ | |
| { | |
| id: "sf1", | |
| round: "Semifinals", | |
| bestOf: "BO3", | |
| teams: [ | |
| { | |
| id: "navi", | |
| name: "Natus Vincere", | |
| seed: 3, | |
| score: 2, | |
| status: "winner", | |
| logo: "", | |
| color: "#e4d449" | |
| }, | |
| { | |
| id: "spirit", | |
| name: "Spirit", | |
| seed: 1, | |
| score: 1, | |
| status: "eliminated", | |
| logo: "", | |
| color: "#d83b3b" | |
| } | |
| ], | |
| winner: "navi" | |
| }, | |
| /* | |
| ----------------------------------------------- | |
| SEMIFINAL 2 | |
| ----------------------------------------------- | |
| */ | |
| { | |
| id: "sf2", | |
| round: "Semifinals", | |
| bestOf: "BO3", | |
| teams: [ | |
| { | |
| id: "vitality", | |
| name: "Vitality", | |
| seed: 2, | |
| score: 0, | |
| status: "eliminated", | |
| logo: "", | |
| color: "#e4bd42" | |
| }, | |
| { | |
| id: "mouz", | |
| name: "MOUZ", | |
| seed: 6, | |
| score: 2, | |
| status: "winner", | |
| logo: "", | |
| color: "#d9a43b" | |
| } | |
| ], | |
| winner: "mouz" | |
| }, | |
| /* | |
| ----------------------------------------------- | |
| GRAND FINAL | |
| ----------------------------------------------- | |
| */ | |
| { | |
| id: "final", | |
| round: "Grand Final", | |
| bestOf: "BO5", | |
| teams: [ | |
| { | |
| id: "navi", | |
| name: "Natus Vincere", | |
| seed: 3, | |
| score: 3, | |
| status: "winner", | |
| logo: "", | |
| color: "#e4d449" | |
| }, | |
| { | |
| id: "mouz", | |
| name: "MOUZ", | |
| seed: 6, | |
| score: 1, | |
| status: "eliminated", | |
| logo: "", | |
| color: "#d9a43b" | |
| } | |
| ], | |
| winner: "navi" | |
| } | |
| ]; | |
| /* | |
| The final champion is explicitly defined. | |
| This means spectator mode doesn't need | |
| to calculate anything from clicks. | |
| */ | |
| const PREDEFINED_CHAMPION = | |
| "navi"; | |
| /* ========================================================= | |
| STATE | |
| ========================================================= */ | |
| let tournament = { | |
| rounds: [], | |
| champion: null | |
| }; | |
| /* ========================================================= | |
| HELPERS | |
| ========================================================= */ | |
| function nextPowerOfTwo(number) { | |
| let result = 1; | |
| while (result < number) { | |
| result *= 2; | |
| } | |
| return result; | |
| } | |
| function roundName(index, total) { | |
| const distance = | |
| total - index; | |
| if (distance === 1) | |
| return "Grand Final"; | |
| if (distance === 2) | |
| return "Semifinals"; | |
| if (distance === 3) | |
| return "Quarterfinals"; | |
| return `Round ${index + 1}`; | |
| } | |
| function cloneTeam(team) { | |
| if (!team) | |
| return null; | |
| return { | |
| ...team | |
| }; | |
| } | |
| /* ========================================================= | |
| BUILD AUTOMATIC BRACKET | |
| ========================================================= */ | |
| function buildAutomaticBracket() { | |
| const size = | |
| nextPowerOfTwo( | |
| contestants.length | |
| ); | |
| const players = | |
| contestants.map( | |
| team => ({ | |
| ...team, | |
| status: | |
| team.status || | |
| "active", | |
| score: | |
| team.score ?? | |
| 0 | |
| }) | |
| ); | |
| while ( | |
| players.length < size | |
| ) { | |
| players.push(null); | |
| } | |
| tournament.rounds = []; | |
| /* | |
| First round. | |
| */ | |
| const firstRound = []; | |
| for ( | |
| let i = 0; | |
| i < size; | |
| i += 2 | |
| ) { | |
| firstRound.push({ | |
| id: | |
| `r1-m${i / 2}`, | |
| round: | |
| 0, | |
| teams: [ | |
| cloneTeam( | |
| players[i] | |
| ), | |
| cloneTeam( | |
| players[i + 1] | |
| ) | |
| ], | |
| winner: | |
| null, | |
| bestOf: | |
| CONFIG.bestOf | |
| }); | |
| } | |
| tournament.rounds.push( | |
| firstRound | |
| ); | |
| /* | |
| Generate remaining rounds. | |
| */ | |
| let matchCount = | |
| firstRound.length; | |
| let roundIndex = 1; | |
| while ( | |
| matchCount > 1 | |
| ) { | |
| matchCount /= | |
| 2; | |
| const round = []; | |
| for ( | |
| let i = 0; | |
| i < matchCount; | |
| i++ | |
| ) { | |
| round.push({ | |
| id: | |
| `r${roundIndex + 1}-m${i}`, | |
| round: | |
| roundIndex, | |
| teams: [ | |
| null, | |
| null | |
| ], | |
| winner: | |
| null, | |
| bestOf: | |
| CONFIG.bestOf | |
| }); | |
| } | |
| tournament.rounds.push( | |
| round | |
| ); | |
| roundIndex++; | |
| } | |
| processAutomaticResults(); | |
| } | |
| /* ========================================================= | |
| PROCESS AUTOMATIC RESULTS | |
| ========================================================= */ | |
| function processAutomaticResults() { | |
| /* | |
| Apply BYEs. | |
| */ | |
| tournament.rounds[0] | |
| .forEach(match => { | |
| const a = | |
| match.teams[0]; | |
| const b = | |
| match.teams[1]; | |
| if (a && !b) { | |
| a.status = | |
| "winner"; | |
| match.winner = | |
| a; | |
| } | |
| if (!a && b) { | |
| b.status = | |
| "winner"; | |
| match.winner = | |
| b; | |
| } | |
| }); | |
| propagateAutomaticWinners(); | |
| } | |
| /* ========================================================= | |
| PROPAGATE WINNERS | |
| ========================================================= */ | |
| function propagateAutomaticWinners() { | |
| for ( | |
| let r = 0; | |
| r < | |
| tournament.rounds.length - 1; | |
| r++ | |
| ) { | |
| const current = | |
| tournament.rounds[r]; | |
| const next = | |
| tournament.rounds[r + 1]; | |
| current.forEach( | |
| (match, index) => { | |
| if (!match.winner) | |
| return; | |
| const nextMatch = | |
| next[ | |
| Math.floor( | |
| index / 2 | |
| ) | |
| ]; | |
| const slot = | |
| index % 2; | |
| if ( | |
| !nextMatch.teams[slot] | |
| ) { | |
| nextMatch.teams[slot] = | |
| cloneTeam( | |
| match.winner | |
| ); | |
| } | |
| } | |
| ); | |
| } | |
| } | |
| /* ========================================================= | |
| BUILD PREDEFINED BRACKET | |
| ========================================================= */ | |
| function buildPredefinedBracket() { | |
| /* | |
| Group supplied matches by round. | |
| */ | |
| const groups = {}; | |
| PREDEFINED_MATCHES | |
| .forEach(match => { | |
| if (!groups[match.round]) { | |
| groups[match.round] = []; | |
| } | |
| groups[match.round] | |
| .push({ | |
| ...match, | |
| teams: | |
| match.teams.map( | |
| cloneTeam | |
| ) | |
| }); | |
| }); | |
| const order = [ | |
| "Round 1", | |
| "Round 2", | |
| "Quarterfinals", | |
| "Semifinals", | |
| "Grand Final" | |
| ]; | |
| const discoveredRounds = | |
| Object.keys(groups); | |
| /* | |
| Use supplied round order. | |
| */ | |
| let ordered = | |
| order.filter( | |
| round => | |
| groups[round] | |
| ); | |
| /* | |
| Include custom round names | |
| that aren't in the defaults. | |
| */ | |
| discoveredRounds | |
| .forEach(round => { | |
| if ( | |
| !ordered.includes(round) | |
| ) { | |
| ordered.push(round); | |
| } | |
| }); | |
| tournament.rounds = | |
| ordered.map( | |
| (roundNameValue, index) => { | |
| return groups[ | |
| roundNameValue | |
| ].map( | |
| match => ({ | |
| ...match, | |
| round: | |
| index | |
| }) | |
| ); | |
| } | |
| ); | |
| /* | |
| Champion. | |
| */ | |
| tournament.champion = | |
| findChampion( | |
| PREDEFINED_CHAMPION | |
| ); | |
| } | |
| /* ========================================================= | |
| FIND CHAMPION | |
| ========================================================= */ | |
| function findChampion( | |
| championId | |
| ) { | |
| for ( | |
| const round | |
| of tournament.rounds | |
| ) { | |
| for ( | |
| const match | |
| of round | |
| ) { | |
| const team = | |
| match.teams.find( | |
| t => | |
| t && | |
| t.id === | |
| championId | |
| ); | |
| if (team) { | |
| return team; | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| /* ========================================================= | |
| TEAM HTML | |
| ========================================================= */ | |
| function teamHTML( | |
| team, | |
| matchId | |
| ) { | |
| if (!team) { | |
| return ` | |
| <div class="team tbd"> | |
| <div class="team-logo"> | |
| — | |
| </div> | |
| <div class="team-name"> | |
| TBD | |
| </div> | |
| <div class="score"></div> | |
| </div> | |
| `; | |
| } | |
| const status = | |
| team.status || | |
| "active"; | |
| const initials = | |
| team.name | |
| .substring(0, 2) | |
| .toUpperCase(); | |
| const logo = | |
| team.logo | |
| ? | |
| ` | |
| <img | |
| src="${team.logo}" | |
| alt="${team.name}" | |
| > | |
| ` | |
| : | |
| initials; | |
| const seed = | |
| CONFIG.showSeeds && | |
| team.seed | |
| ? | |
| ` | |
| <span class="team-seed"> | |
| #${team.seed} | |
| </span> | |
| ` | |
| : | |
| ""; | |
| const score = | |
| CONFIG.showScores | |
| ? | |
| ` | |
| <div class="score"> | |
| ${team.score ?? ""} | |
| </div> | |
| ` | |
| : | |
| ` | |
| <div class="score"></div> | |
| `; | |
| return ` | |
| <div | |
| class=" | |
| team | |
| ${status} | |
| ${!CONFIG.interactiveMode | |
| ? "readonly" | |
| : "" | |
| } | |
| " | |
| data-team-id="${team.id}" | |
| data-match-id="${matchId}" | |
| > | |
| <div | |
| class="team-logo" | |
| style=" | |
| border-top: | |
| 2px solid | |
| ${team.color || "#444"}; | |
| " | |
| > | |
| ${logo} | |
| </div> | |
| <div class="team-name"> | |
| ${seed} | |
| ${team.name} | |
| </div> | |
| ${score} | |
| </div> | |
| `; | |
| } | |
| /* ========================================================= | |
| MATCH HTML | |
| ========================================================= */ | |
| function matchHTML( | |
| match, | |
| isFinal = false | |
| ) { | |
| const series = | |
| match.bestOf || | |
| CONFIG.bestOf; | |
| return ` | |
| <div | |
| class=" | |
| match | |
| ${isFinal | |
| ? "final-match" | |
| : "" | |
| } | |
| ${CONFIG.interactiveMode | |
| ? "interactive" | |
| : "" | |
| } | |
| " | |
| > | |
| <div class="match-header"> | |
| <span class="series"> | |
| ${series} | |
| </span> | |
| <span class="match-id"> | |
| ${match.id} | |
| </span> | |
| </div> | |
| ${teamHTML( | |
| match.teams?.[0], | |
| match.id | |
| )} | |
| ${teamHTML( | |
| match.teams?.[1], | |
| match.id | |
| )} | |
| ${ | |
| isFinal && | |
| tournament.champion | |
| ? | |
| ` | |
| <div class="champion"> | |
| 🏆 Champion | |
| <span | |
| class="champion-name" | |
| > | |
| ${tournament.champion.name} | |
| </span> | |
| </div> | |
| ` | |
| : | |
| "" | |
| } | |
| </div> | |
| `; | |
| } | |
| /* ========================================================= | |
| CLASSIC LAYOUT | |
| ========================================================= */ | |
| function renderClassic() { | |
| const container = | |
| document.getElementById( | |
| "bracket" | |
| ); | |
| container.className = | |
| "bracket classic"; | |
| tournament.rounds | |
| .forEach( | |
| (round, index) => { | |
| const roundElement = | |
| document.createElement( | |
| "section" | |
| ); | |
| roundElement.className = | |
| "round"; | |
| roundElement.innerHTML = ` | |
| <div class="round-title"> | |
| ${roundName( | |
| index, | |
| tournament.rounds.length | |
| )} | |
| </div> | |
| `; | |
| const matches = | |
| document.createElement( | |
| "div" | |
| ); | |
| matches.className = | |
| "round-matches"; | |
| round.forEach( | |
| match => { | |
| matches.innerHTML += | |
| matchHTML( | |
| match, | |
| index === | |
| tournament.rounds.length - 1 | |
| ); | |
| } | |
| ); | |
| roundElement.appendChild( | |
| matches | |
| ); | |
| container.appendChild( | |
| roundElement | |
| ); | |
| } | |
| ); | |
| } | |
| /* ========================================================= | |
| CENTERED LAYOUT | |
| ========================================================= */ | |
| function renderCentered() { | |
| const container = | |
| document.getElementById( | |
| "bracket" | |
| ); | |
| container.className = | |
| "bracket centered"; | |
| const total = | |
| tournament.rounds.length; | |
| const left = | |
| document.createElement( | |
| "div" | |
| ); | |
| left.className = | |
| "center-side left"; | |
| const right = | |
| document.createElement( | |
| "div" | |
| ); | |
| right.className = | |
| "center-side right"; | |
| tournament.rounds | |
| .slice(0, -1) | |
| .forEach( | |
| (round, index) => { | |
| const wrapper = | |
| document.createElement( | |
| "div" | |
| ); | |
| wrapper.innerHTML = ` | |
| <div class="round-title"> | |
| ${roundName( | |
| index, | |
| total | |
| )} | |
| </div> | |
| `; | |
| round.forEach( | |
| match => { | |
| wrapper.innerHTML += | |
| matchHTML( | |
| match | |
| ); | |
| } | |
| ); | |
| if ( | |
| index % 2 === 0 | |
| ) { | |
| left.appendChild( | |
| wrapper | |
| ); | |
| } else { | |
| right.appendChild( | |
| wrapper | |
| ); | |
| } | |
| } | |
| ); | |
| const final = | |
| tournament.rounds[ | |
| total - 1 | |
| ][0]; | |
| const finalColumn = | |
| document.createElement( | |
| "div" | |
| ); | |
| finalColumn.className = | |
| "center-final"; | |
| finalColumn.innerHTML = ` | |
| <div class="round-title"> | |
| GRAND FINAL | |
| </div> | |
| ${matchHTML( | |
| final, | |
| true | |
| )} | |
| `; | |
| container.appendChild( | |
| left | |
| ); | |
| container.appendChild( | |
| finalColumn | |
| ); | |
| container.appendChild( | |
| right | |
| ); | |
| } | |
| /* ========================================================= | |
| COMPACT LAYOUT | |
| ========================================================= */ | |
| function renderCompact() { | |
| const container = | |
| document.getElementById( | |
| "bracket" | |
| ); | |
| container.className = | |
| "bracket compact"; | |
| tournament.rounds | |
| .forEach( | |
| (round, index) => { | |
| const wrapper = | |
| document.createElement( | |
| "section" | |
| ); | |
| wrapper.className = | |
| "compact-round"; | |
| wrapper.innerHTML = ` | |
| <div class="round-title"> | |
| ${roundName( | |
| index, | |
| tournament.rounds.length | |
| )} | |
| </div> | |
| `; | |
| const matches = | |
| document.createElement( | |
| "div" | |
| ); | |
| matches.className = | |
| "round-matches"; | |
| round.forEach( | |
| match => { | |
| matches.innerHTML += | |
| matchHTML( | |
| match, | |
| index === | |
| tournament.rounds.length - 1 | |
| ); | |
| } | |
| ); | |
| wrapper.appendChild( | |
| matches | |
| ); | |
| container.appendChild( | |
| wrapper | |
| ); | |
| } | |
| ); | |
| } | |
| /* ========================================================= | |
| INTERACTIVE WINNER SELECTION | |
| ========================================================= */ | |
| function selectWinner( | |
| matchId, | |
| teamId | |
| ) { | |
| /* | |
| Spectator mode completely blocks | |
| manual state changes. | |
| */ | |
| if ( | |
| !CONFIG.interactiveMode | |
| ) { | |
| return; | |
| } | |
| let match = null; | |
| for ( | |
| const round | |
| of tournament.rounds | |
| ) { | |
| const found = | |
| round.find( | |
| m => | |
| m.id === | |
| matchId | |
| ); | |
| if (found) { | |
| match = | |
| found; | |
| break; | |
| } | |
| } | |
| if (!match) | |
| return; | |
| const selected = | |
| match.teams.find( | |
| team => | |
| team && | |
| team.id === | |
| teamId | |
| ); | |
| if (!selected) | |
| return; | |
| const loser = | |
| match.teams.find( | |
| team => | |
| team && | |
| team.id !== | |
| teamId | |
| ); | |
| selected.status = | |
| "winner"; | |
| if (loser) { | |
| loser.status = | |
| "eliminated"; | |
| } | |
| match.winner = | |
| selected; | |
| /* | |
| Final. | |
| */ | |
| const finalRound = | |
| tournament.rounds[ | |
| tournament.rounds.length - 1 | |
| ]; | |
| if ( | |
| finalRound.includes( | |
| match | |
| ) | |
| ) { | |
| tournament.champion = | |
| selected; | |
| } | |
| propagateAutomaticWinners(); | |
| render(); | |
| } | |
| /* ========================================================= | |
| INTERACTIONS | |
| ========================================================= */ | |
| function attachInteractions() { | |
| document | |
| .querySelectorAll( | |
| ".team" | |
| ) | |
| .forEach( | |
| element => { | |
| if ( | |
| !CONFIG.interactiveMode | |
| ) { | |
| element.classList | |
| .add("readonly"); | |
| return; | |
| } | |
| element.addEventListener( | |
| "click", | |
| () => { | |
| selectWinner( | |
| element.dataset.matchId, | |
| element.dataset.teamId | |
| ); | |
| } | |
| ); | |
| } | |
| ); | |
| } | |
| /* ========================================================= | |
| RENDER | |
| ========================================================= */ | |
| function render() { | |
| const container = | |
| document.getElementById( | |
| "bracket" | |
| ); | |
| container.innerHTML = | |
| ""; | |
| document.documentElement | |
| .style | |
| .setProperty( | |
| "--card-width", | |
| `${CONFIG.cardWidth}px` | |
| ); | |
| document.documentElement | |
| .style | |
| .setProperty( | |
| "--round-gap", | |
| `${CONFIG.roundGap}px` | |
| ); | |
| document.documentElement | |
| .style | |
| .setProperty( | |
| "--match-gap", | |
| `${CONFIG.matchGap}px` | |
| ); | |
| /* | |
| Spectator body state. | |
| */ | |
| document.body.classList.toggle( | |
| "spectator-mode", | |
| !CONFIG.interactiveMode | |
| ); | |
| /* | |
| Choose layout. | |
| */ | |
| switch ( | |
| LAYOUT_STYLE | |
| ) { | |
| case "classic": | |
| renderClassic(); | |
| break; | |
| case "centered": | |
| renderCentered(); | |
| break; | |
| case "compact": | |
| renderCompact(); | |
| break; | |
| } | |
| attachInteractions(); | |
| updateModeIndicator(); | |
| } | |
| /* ========================================================= | |
| MODE INDICATOR | |
| ========================================================= */ | |
| function updateModeIndicator() { | |
| const indicator = | |
| document.getElementById( | |
| "modeIndicator" | |
| ); | |
| const text = | |
| document.getElementById( | |
| "modeText" | |
| ); | |
| if ( | |
| CONFIG.interactiveMode | |
| ) { | |
| indicator.classList | |
| .add("live"); | |
| text.textContent = | |
| "Interactive"; | |
| } else { | |
| indicator.classList | |
| .remove("live"); | |
| text.textContent = | |
| "Spectator"; | |
| } | |
| } | |
| /* ========================================================= | |
| CHANGE LAYOUT | |
| ========================================================= */ | |
| function setLayout( | |
| layout | |
| ) { | |
| const allowed = [ | |
| "classic", | |
| "centered", | |
| "compact" | |
| ]; | |
| if ( | |
| !allowed.includes( | |
| layout | |
| ) | |
| ) { | |
| throw new Error( | |
| `Invalid layout: ${layout}` | |
| ); | |
| } | |
| LAYOUT_STYLE = | |
| layout; | |
| document | |
| .querySelectorAll( | |
| "[data-layout]" | |
| ) | |
| .forEach( | |
| button => { | |
| button.classList.toggle( | |
| "active", | |
| button.dataset.layout === | |
| layout | |
| ); | |
| } | |
| ); | |
| render(); | |
| } | |
| /* ========================================================= | |
| SET INTERACTIVE MODE | |
| ========================================================= */ | |
| function setInteractiveMode( | |
| enabled | |
| ) { | |
| CONFIG.interactiveMode = | |
| Boolean(enabled); | |
| render(); | |
| } | |
| /* ========================================================= | |
| REPLACE CONTESTANTS | |
| ========================================================= */ | |
| function setContestants( | |
| newContestants | |
| ) { | |
| contestants = | |
| newContestants.map( | |
| (team, index) => ({ | |
| id: | |
| team.id ?? | |
| `team-${index}`, | |
| name: | |
| team.name ?? | |
| "Unknown", | |
| seed: | |
| team.seed ?? | |
| index + 1, | |
| logo: | |
| team.logo ?? | |
| "", | |
| color: | |
| team.color ?? | |
| "#444", | |
| status: | |
| team.status ?? | |
| "active", | |
| score: | |
| team.score ?? | |
| 0 | |
| }) | |
| ); | |
| tournament.champion = | |
| null; | |
| buildAutomaticBracket(); | |
| render(); | |
| } | |
| /* ========================================================= | |
| LOAD PREDEFINED STATE | |
| ========================================================= */ | |
| function loadPredefinedTournament() { | |
| buildPredefinedBracket(); | |
| render(); | |
| } | |
| /* ========================================================= | |
| UPDATE A SINGLE MATCH | |
| ========================================================= */ | |
| function updateMatch( | |
| matchId, | |
| data | |
| ) { | |
| for ( | |
| const round | |
| of tournament.rounds | |
| ) { | |
| const match = | |
| round.find( | |
| m => | |
| m.id === | |
| matchId | |
| ); | |
| if (!match) | |
| continue; | |
| Object.assign( | |
| match, | |
| data | |
| ); | |
| if ( | |
| data.winner | |
| ) { | |
| const winner = | |
| match.teams.find( | |
| team => | |
| team && | |
| team.id === | |
| data.winner | |
| ); | |
| if (winner) { | |
| winner.status = | |
| "winner"; | |
| match.winner = | |
| winner; | |
| } | |
| match.teams | |
| .forEach( | |
| team => { | |
| if ( | |
| team && | |
| team.id !== | |
| data.winner | |
| ) { | |
| team.status = | |
| "eliminated"; | |
| } | |
| } | |
| ); | |
| } | |
| if ( | |
| match.id === | |
| "final" && | |
| data.winner | |
| ) { | |
| tournament.champion = | |
| match.teams.find( | |
| team => | |
| team && | |
| team.id === | |
| data.winner | |
| ); | |
| } | |
| render(); | |
| return; | |
| } | |
| } | |
| /* ========================================================= | |
| LAYOUT BUTTONS | |
| ========================================================= */ | |
| document | |
| .querySelectorAll( | |
| "[data-layout]" | |
| ) | |
| .forEach( | |
| button => { | |
| button.addEventListener( | |
| "click", | |
| () => { | |
| setLayout( | |
| button.dataset.layout | |
| ); | |
| } | |
| ); | |
| } | |
| ); | |
| /* ========================================================= | |
| HEADER | |
| ========================================================= */ | |
| document.getElementById( | |
| "tournamentTitle" | |
| ).textContent = | |
| CONFIG.title; | |
| document.getElementById( | |
| "tournamentSubtitle" | |
| ).textContent = | |
| CONFIG.subtitle; | |
| document.documentElement | |
| .style | |
| .setProperty( | |
| "--accent", | |
| CONFIG.accent | |
| ); | |
| /* ========================================================= | |
| INITIALIZATION | |
| ========================================================= */ | |
| /* | |
| Because interactiveMode is false, | |
| the predefined tournament is loaded | |
| as a read-only spectator bracket. | |
| */ | |
| if ( | |
| CONFIG.interactiveMode | |
| ) { | |
| buildAutomaticBracket(); | |
| } else { | |
| loadPredefinedTournament(); | |
| } | |
| render(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment