1/* /app Decks tab + deck archetype detail page â window.TDTT.lbDecksView 2 * (Task 9, TCG Decks B3). 3 * 4 * R1 (batch ruling): the shared PURE core twin generated from 5 * packages/shared/src/leaderboard/decks.ts already owns the name 6 * `window.TDTT.lbDecks` (leaderboard-decks-core.js â DECK_SORT_KEYS, 7 * sortDecks, matchesDeckQuery, shareDelta, deckInsights, topArchetypes, 8 * metaNotes, ...). This RENDER module is therefore `window.TDTT.lbDecksView` 9 * instead, matching the plan's original two-function shape 10 * `{ listHtml(ctx), pageHtml(ctx) }` plus a `wire(root, ctx)` this file adds 11 * for the same reason lb-hero-page.js has one (window/sort seg-tabs, a 12 * format select, a live search box and click-through breadcrumbs/rows â none 13 * of which fit the "everything is a generic `[data-deck-key]` click" seam 14 * lb-tcg-rail.js/lb-formats.js use). 15 * 16 * Same delegation seam as lb-hero-page.js: leaderboard.js owns every fetch 17 * (decks(game,window,format) for the grid; deckDetail/deckMatchups/deckGames 18 * for the archetype page) and builds the `ctx` object each function renders 19 * from â see leaderboard.js's own "Decks tab (Task 9)" comment block for the 20 * exact contract. 21 * 22 * Mirrors the desktop DecksView/DeckCard/DeckArchetypePage components 23 * section for section, in the same order, with the same copy verbatim â 24 * see those files' own header comments for the underlying design rationale. 25 * ONE difference, an explicit orchestrator resolution: desktop's empty-grid 26 * state links "Choose a deck in Quick Match" to `/matchmaking` (Webcam Play); 27 * `/app` has no webcam play at all (access matrix: desktop-app-only), so 28 * here it renders as plain hint text with no link. 29 * 30 * A deck/player/opponent name is ALWAYS run through esc() before it reaches 31 * markup â the card grid, the Top Archetypes rail (lb-tcg-rail.js, already 32 * escaped, unaffected here), the page header, top pilots, matchups and 33 * recent games. This is the one thing every review pass on this file checks 34 * first (Review Focus 2 / the bite proof). 35 * 36 * Deck-key buttons (grid cards, matchup rows, a public recent-game opponent) 37 * all carry `data-deck-key` and are wired GENERICALLY by leaderboard.js's own 38 * wireEvents() on every full render() â this file does not wire those itself, 39 * with ONE exception: the live search box patches `#lb-deck-gridarea` 40 * directly (see wire()'s comment) to avoid stealing focus on every keystroke, 41 * the same technique leaderboard.js's own hero search already uses, and MUST 42 * therefore re-wire the cards it just injected by hand (wireCardClicks()). 43 */ 44(function () { 45 'use strict'; 46 47 function esc(s) { 48 return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { 49 return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; 50 }); 51 } 52 53 var WINDOW_OPTIONS = [ 54 { value: 'all', label: 'All Time' }, 55 { value: '90d', label: '90D' }, 56 { value: '30d', label: '30D' }, 57 { value: '7d', label: '7D' }, 58 ]; 59 60 var SORT_LABELS = { share: 'Play Share', winRate: 'Win Rate', games: 'Games', players: 'Pilots' }; 61 62 function windowSegHtml(ctx) { 63 var tabs = ''; 64 for (var i = 0; i < WINDOW_OPTIONS.length; i++) { 65 var o = WINDOW_OPTIONS[i]; 66 tabs += 67 '<button type="button" class="t' + (o.value === ctx.window ? ' on' : '') + 68 '" data-window="' + esc(o.value) + '">' + esc(o.label) + '</button>'; 69 } 70 return '<div class="seg-slab" id="lb-deck-window">' + tabs + '</div>'; 71 } 72 73 function sortSegHtml(ctx) { 74 var keys = window.TDTT.lbDecks.DECK_SORT_KEYS; 75 var tabs = ''; 76 for (var i = 0; i < keys.length; i++) { 77 var k = keys[i]; 78 tabs += 79 '<button type="button" class="chip' + (k === ctx.sort ? ' on' : '') + 80 '" data-sort="' + esc(k) + '">' + esc(SORT_LABELS[k]) + '</button>'; 81 } 82 return '<div class="chip-row" id="lb-deck-sort">' + tabs + '</div>'; 83 } 84 85 function formatSelectHtml(ctx) { 86 var opts = '<option value="">All formats</option>'; 87 var formats = ctx.formats || []; 88 for (var i = 0; i < formats.length; i++) { 89 var f = formats[i]; 90 opts += 91 '<option value="' + esc(f.id) + '"' + (f.id === ctx.format ? ' selected' : '') + '>
91' + 92 esc(f.name) + '</option>'; 93 } 94 return '<select class="lb-select" id="lb-deck-format" aria-label="Format">' + opts + '</select>'; 95 } 96 97 function searchInputHtml(ctx) { 98 return ( 99 '<input type="search" id="lb-deck-search" class="lb-search-input" placeholder="Search decksâ¦" ' + 100 'autocomplete="off" value="' + esc(ctx.query) + '">' 101 ); 102 } 103 104 // Duplicated from leaderboard.js's private deckIdentityChipsHtml() on 105 // purpose â a browser IIFE can't reach another file's closure (same 106 // reasoning lb-tcg-rail.js's own header comment gives). 107 function identityChipsHtml(gameType, identity) { 108 var html = ''; 109 var list = identity || []; 110 for (var i = 0; i < list.length; i++) { 111 var v = list[i]; 112 var hex = window.TDTT.decks.identityColor(gameType, v); 113 var cls = 'tdtt-idchip' + (hex ? '' : ' is-neutral'); 114 var style = hex ? ' style="--id-c:' + esc(hex) + '"' : ''; 115 html += '<span class="' + cls + '"' + style + '>' + esc(window.TDTT.decks.identityLabel(gameType, v)) + '</span>'; 116 } 117 return html; 118 } 119 120 function totalsLineHtml(totals) { 121 return esc(totals.trackedGames + ' games with a deck · ' + totals.decklessGames + ' without'); 122 } 123 124 // ââ Deck card (Decks landing grid) âââââââââââââââââââââââââââââââââââââââ 125 // Same trend-arrow rule as desktop's DeckCard: shareDelta() null (no 126 // previous-window data, e.g. the 'all' window) renders no arrow at all, 127 // never a stale one; >= +1 is green up, <= -1 is red down, otherwise none. 128 function deckCardHtml(row, gameType) { 129 var band = window.TDTT.bands.winRateBand(row.wins, row.losses); 130 var delta = window.TDTT.lbDecks.shareDelta(row); 131 var arrow = ''; 132 if (delta !== null && delta >= 1) { 133 arrow = '<span class="lb-deck-arrow up" aria-hidden="true">▲</span>'; 134 } else if (delta !== null && delta <= -1) { 135 arrow = '<span class="lb-deck-arrow down" aria-hidden="true">▼</span>'; 136 } 137 return ( 138 '<button type="button" class="tdtt-cardtile lb-deck-card" data-deck-key="' + esc(row.key) + '">' + 139 '<div class="lb-deck-card-chips">' + identityChipsHtml(gameType, row.identity) + '</div>' + 140 '<p class="lb-deck-card-name">' + esc(row.name) + '</p>' + 141 '<div class="lb-deck-card-row">' + 142 '<span class="lb-deck-card-share">' + row.share + '%' + arrow + '</span>' + 143 '<span class="lb-deck-card-wr ' + band + '">' + row.winRate + '%</span>' + 144 '</div>' + 145 '<div class="lb-deck-card-meta"><span>' + row.games + ' games</span><span>' + row.players + ' pilots</span></div>' + 146 '</button>' 147 ); 148 } 149 150 function noQueryMatchHtml(query) { 151 return '<p class="lb-empty-sub lb-deck-center">No deck matches â' + esc(query) + 'â</p>'; 152 } 153 154 function emptyGridHtml(totals) { 155 return ( 156 '<div class="lb-status lb-empty">' + 157 '<p class="lb-empty-title">No deck data yet</p>' + 158 '<p class="lb-empty-sub">' + 159 esc('Archetypes appear once ' + window.TDTT.decks.DECK_LIMITS.publicMinPlayers + ' players have played the same deck.') + 160 '</p>' + 161 '<p class="lb-deck-totals-sub">' + totalsLineHtml(totals) + '</p>' + 162 '<p class="lb-deck-quickmatch-hint">Choose a deck in Quick Match</p>' + 163 '</div>' 164 ); 165 } 166 167 function filteredSortedDecks(ctx) { 168 var filtered = []; 169 for (var i = 0; i < ctx.decks.length; i++) { 170 if (window.TDTT.lbDecks.matchesDeckQuery(ctx.decks[i].name, ctx.query)) filtered.push(ctx.decks[i]); 171 } 172 return window.TDTT.lbDecks.sortDecks(filtered, ctx.sort); 173 } 174 175 function gridAreaHtml(sorted, query, gameType) { 176 if (!sorted.length) return '<div id="lb-deck-gridarea">' + noQueryMatchHtml(query) + '</div>'; 177 var cards = ''; 178 for (var i = 0; i < sorted.length; i++) cards += deckCardHtml(sorted[i], gameType); 179 return '<div id="lb-deck-gridarea"><div class="lb-deck-grid">' + cards + '</div></div>'; 180 } 181 182 function gridBodyHtml(ctx) { 183 if (ctx.gridState === 'loading') return '<div class="lb-status lb-loading">Loadingâ¦</div>'; 184 if (ctx.gridState === 'error') { 185 return '<div class="lb-status lb-error"><p class="lb-error-title">Couldnât load deck archetypes</p></div>'; 186 } 187 if (!ctx.decks.length) return emptyGridHtml(ctx.totals); 188 189 var sorted = filteredSortedDecks(ctx); 190 var controls = 191 '<div class="lb-filterrow lb-deck-controls">' + 192 '<div class="lb-deck-sortwrap"><span class="lb-deck-sort-label">Sort by</span>' + sortSegHtml(ctx) + '</div>' + 193 '<div class="lb-deck-controls-right">' + formatSelectHtml(ctx) + 194 '<div class="lb-search-wrap">' + searchInputHtml(ctx) + '</div></div>' + 195 '</div>'; 196 var totalsLine = '<p class="lb-deck-totals">' + totalsLineHtml(ctx.totals) + '</p>'; 197 return controls + totalsLine + gridAreaHtml(sorted, ctx.query, ctx.game); 198 } 199 200 /** 201 * `ctx` (leaderboard.js's buildDecksListCtx()): 202 * { game, gameName, window, format, formats (id/name pairs), sort, query, 203 * gridState, decks, totals, railHtml, 204 * onWindow(w), onFormat(id), onSort(key), onQuery(q), onSelectDeck(key) } 205 * `gridState` is 'loading' | 'ready' | 'error'. `railHtml` is a fully 206 * rendered `<aside>` string (leaderboard.js's own tcgRailAsideHtml(), 207 * reused verbatim â the Decks tab shares the SAME all-time, all-format 208 * rail data the Players tab's insights rail shows, per the brief). 209 */ 210 function listHtml(ctx) { 211 return ( 212 '<div class="lb-deck-list">' + 213 windowSegHtml(ctx) + 214 '<div class="lb-tcg-layout"><div class="lb-tcg-main">' + gridBodyHtml(ctx) + '</div>' + 215 (ctx.railHtml || '') + 216 '</div>' + 217 '</div>' 218 ); 219 } 220 221 // ââ Deck archetype detail page âââââââââââââââââââââââââââââââââââââââââââ 222 223 function breadcrumbHtml(name) { 224 return ( 225 '<nav class="lb-deck-crumb">' + 226 '<button type="button" class="lb-deck-crumb-link" id="lb-deck-crumb-lb">Leaderboards</button>' + 227 '<span class="lb-deck-crumb-sep">›</span>' + 228 '<button type="button" class="lb-deck-crumb-link" id="lb-deck-crumb-decks">Decks</button>' + 229 (name != null 230 ? '<span class="lb-deck-crumb-sep">›</span><span class="lb-deck-crumb-current">' + esc(name) + '</span>' 231 : '') + 232 '</nav>' 233 ); 234 } 235 236 function statTileHtml(value, label, accent) { 237 return ( 238 '<div class="tdtt-stat lb-deck-tile" style="--sec-accent:' + accent + '">' + 239 '<span class="v">' + esc(value) + '</span>' + 240 '<span class="l">' + esc(label) + '</span>' + 241 '<span class="u"></span>' + 242 '</div>' 243 ); 244 } 245 246 // `trend` is a 1/0 series, oldest -> newest â same cumulative-win-rate 247 // conversion as desktop's DeckArchetypePage.toTrendPoints(), so this page's 248 // trend line can never disagree with the desktop one for the same data.
249 function toTrendPoints(trend) { 250 var wins = 0; 251 var out = []; 252 for (var i = 0; i < trend.length; i++) { 253 if (trend[i]) wins++; 254 out.push({ index: i, won: trend[i] === 1, cumulativeWinRate: Math.round((wins / (i + 1)) * 100) }); 255 } 256 return out; 257 } 258 259 // Same inline-SVG shape as lb-hero-page.js's trendSvgHtml() â duplicated on 260 // purpose (browser IIFEs can't share a closure); a 300x100 viewBox with 261 // 0/25/50/75/100 gridlines, one polyline point per trend point. 262 function trendSvgHtml(trend) { 263 var WIDTH = 300; 264 var HEIGHT = 100; 265 var GRIDLINES = [0, 25, 50, 75, 100]; 266 var latest = trend[trend.length - 1]; 267 var points = []; 268 var i; 269 for (i = 0; i < trend.length; i++) { 270 var x = trend.length > 1 ? (i / (trend.length - 1)) * WIDTH : WIDTH / 2; 271 var y = HEIGHT - (trend[i].cumulativeWinRate / 100) * HEIGHT; 272 points.push(x + ',' + y); 273 } 274 var lines = ''; 275 for (i = 0; i < GRIDLINES.length; i++) { 276 var gy = HEIGHT - (GRIDLINES[i] / 100) * HEIGHT; 277 lines += 278 '<line x1="0" y1="' + gy + '" x2="' + WIDTH + '" y2="' + gy + 279 '" stroke="rgba(255,255,255,0.08)" stroke-width="1"></line>'; 280 } 281 return ( 282 '<div class="lb-deck-trend-row">' + 283 '<svg viewBox="0 0 ' + WIDTH + ' ' + HEIGHT + '" preserveAspectRatio="none" class="lb-deck-trend-svg" ' + 284 'role="img" aria-label="Win rate trend, latest ' + latest.cumulativeWinRate + '%">' + 285 lines + 286 '<polyline points="' + points.join(' ') + '" fill="none" stroke="var(--cyan)" stroke-width="2" ' + 287 'stroke-linejoin="round" stroke-linecap="round"></polyline>' + 288 '</svg>' + 289 '<span class="lb-deck-trend-latest">' + latest.cumulativeWinRate + '%</span>' + 290 '</div>' 291 ); 292 } 293 294 function resultChipParts(result) { 295 if (result === 'WIN') return { label: 'Win', cls: 'win' }; 296 if (result === 'LOSS') return { label: 'Loss', cls: 'loss' }; 297 if (result === 'DRAW') return { label: 'Draw', cls: 'draw' }; 298 return { label: 'â', cls: 'draw' }; 299 } 300 301 function pilotsPanelHtml(pilots, ctx) { 302 var body; 303 if (!pilots.length) { 304 body = '<p class="lb-empty-sub lb-deck-center">No pilot data yet</p>'; 305 } else { 306 var rows = ''; 307 for (var i = 0; i < pilots.length; i++) { 308 var p = pilots[i]; 309 var band = window.TDTT.bands.winRateBand(p.wins, p.losses); 310 var avatar = typeof ctx.avatarHtml === 'function' 311 ? ctx.avatarHtml({ avatarUrl: p.avatarUrl, displayName: p.displayName }, '') 312 : ''; 313 rows += 314 '<div class="lb-deck-pilot-row">' + avatar + 315 '<button type="button" class="lb-deck-pilot-name" data-pid="' + esc(p.playerId) + '">' + 316 esc(p.displayName) + '</button>' + 317 '<span class="tdtt-record"><span class="w">' + p.wins + '</span><span class="sep">â</span>' + 318 '<span class="l">' + p.losses + '</span></span>' + 319 '<span class="lb-deck-pilot-wr ' + band + '">' + p.winRate + '%</span>' + 320 '</div>'; 321 } 322 body = '<div class="lb-deck-pilot-list">' + rows + '</div>'; 323 } 324 return '<div class="tdtt-panel lb-deck-panel"><div class="tdtt-panel__head">Top Pilots</div>' + body + '</div>'; 325 } 326 327 function formatsPanelHtml(formats) { 328 var body; 329 if (!formats.length) { 330 body = '<p class="lb-empty-sub lb-deck-center">No format data yet</p>'; 331 } else { 332 var rows = ''; 333 for (var i = 0; i < formats.length; i++) { 334 var f = formats[i]; 335 var band = window.TDTT.bands.winRateBand(f.wins, f.losses); 336 rows += 337 '<div class="lb-deck-format-row">' + 338 '<span class="lb-deck-format-name">' + esc(f.name) + '</span>' + 339 '<span class="tdtt-record"><span class="w">' + f.wins + '</span><span class="sep">â</span>' + 340 '<span class="l">' + f.losses + '</span></span>' + 341 '<span class="lb-deck-format-wr ' + band + '">' + f.winRate + '%</span>' + 342 '</div>'; 343 } 344 body = '<div class="lb-deck-format-list">' + rows + '</div>'; 345 }
346 return '<div class="tdtt-panel lb-deck-panel"><div class="tdtt-panel__head">Format Split</div>' + body + '</div>'; 347 } 348 349 function matchupsPanelHtml(ctx) { 350 var body; 351 if (ctx.matchupsState === 'loading') { 352 body = '<p class="lb-status lb-loading">Loading…</p>'; 353 } else if (ctx.matchupsState === 'error') { 354 body = '<p class="lb-error-detail lb-deck-center">Couldnât load matchups.</p>'; 355 } else if (!ctx.matchups || !ctx.matchups.length) { 356 body = '<p class="lb-empty-sub lb-deck-center">No matchup data yet</p>'; 357 } else { 358 var rows = ''; 359 for (var i = 0; i < ctx.matchups.length; i++) { 360 var mu = ctx.matchups[i]; 361 var band = window.TDTT.bands.winRateBand(mu.wins, mu.losses); 362 rows += 363 '<button type="button" class="lb-deck-matchup-row" data-deck-key="' + esc(mu.key) + '">' + 364 '<div class="lb-deck-matchup-chips">' + identityChipsHtml(ctx.game, mu.identity) + '</div>' + 365 '<span class="lb-deck-matchup-name">' + esc(mu.name) + '</span>' + 366 '<span class="lb-deck-matchup-wr ' + band + '">' + mu.winRate + '%</span>' + 367 '<span class="tdtt-record"><span class="w">' + mu.wins + '</span><span class="sep">â</span>' + 368 '<span class="l">' + mu.losses + '</span></span>' + 369 '</button>'; 370 } 371 body = '<div class="lb-deck-matchup-list">' + rows + '</div>'; 372 } 373 return '<div class="tdtt-panel lb-deck-panel"><div class="tdtt-panel__head">Matchups</div>' + body + '</div>'; 374 } 375 376 function recentGamesPanelHtml(ctx) { 377 var body; 378 if (ctx.gamesState === 'loading') { 379 body = '<p class="lb-status lb-loading">Loading…</p>'; 380 } else if (ctx.gamesState === 'error') { 381 body = '<p class="lb-error-detail lb-deck-center">Couldnât load recent games.</p>'; 382 } else if (!ctx.games || !ctx.games.length) { 383 body = '<p class="lb-empty-sub lb-deck-center">No games recorded yet</p>'; 384 } else { 385 var rows = ''; 386 for (var i = 0; i < ctx.games.length; i++) { 387 var g = ctx.games[i]; 388 var chip = resultChipParts(g.result); 389 var when = g.endedAt && window.TDTT.fmt && typeof window.TDTT.fmt.formatRelativeDate === 'function' 390 ? window.TDTT.fmt.formatRelativeDate(g.endedAt) 391 : 'â'; 392 var opp = g.opponent || {}; 393 var opponentHtml = opp.archetypeKey 394 ? '<button type="button" class="lb-deck-game-opp-link" data-deck-key="' + esc(opp.archetypeKey) + '">' + 395 esc(opp.archetypeName || opp.displayName) + '</button>' 396 : esc(opp.displayName); 397 rows += 398 '<div class="lb-deck-game-row">' + 399 '<span class="lb-deck-game-chip ' + chip.cls + '">' + esc(chip.label) + '</span>' + 400 '<span class="lb-deck-game-vs">vs ' + opponentHtml + '</span>' + 401 '<span class="lb-deck-game-when">' + esc(when) + '</span>' + 402 '</div>'; 403 } 404 body = '<div class="lb-deck-game-list">' + rows + '</div>'; 405 } 406 return '<div class="tdtt-panel lb-deck-panel"><div class="tdtt-panel__head">Recent Games</div>' + body + '</div>'; 407 } 408 409 /** 410 * `ctx` (leaderboard.js's buildDeckPageCtx()): 411 * { game, window, detailState, detail (archetype/pilots/formats/trend), 412 * matchupsState, matchups, gamesState, games, avatarHtml(row,cls), 413 * onWindow(w), onLeaderboards(), onBack(), onOpenPlayer(id) } 414 * `detailState` is 'loading' | 'ready' | 'error' | 'notFound'. A 404 (an 415 * archetype that dropped below DECK_LIMITS.publicMinPlayers pilots in a 416 * narrower window, or simply doesn't exist) gets its OWN state here â an 417 * expected, common case (Review Focus 1), not a generic fetch failure. 418 */ 419 function pageHtml(ctx) { 420 var windowChips = windowSegHtml(ctx); 421 422 if (ctx.detailState === 'loading') { 423 return '<div class="lb-deck-page">' + windowChips + breadcrumbHtml(null) + 424 '<p class="lb-status lb-loading">Loading…</p></div>'; 425 } 426 if (ctx.detailState === 'notFound') { 427 return ( 428 '<div class="lb-deck-page">' + windowChips + breadcrumbHtml(null) + 429 '<div class="lb-status lb-empty"><p class="lb-empty-title">This deck archetype isnât public yet</p>' + 430 '<button type="button" class="lb-btn" id="lb-deck-back-notfound">Back</button></div>' + 431 '</div>' 432 ); 433 } 434 if (ctx.detailState === 'error' || !ctx.detail) { 435 return ( 436 '<div class="lb-deck-page">' + windowChips + breadcrumbHtml(null) + 437 '<div class="lb-status lb-error"><p class="lb-error-title">Couldnât load this deck archetype.</p>' + 438 '<button type="button" class="lb-btn" id="lb-deck-back-error">Back</button></div>' + 439 '</div>' 440 ); 441 } 442 443 var archetype = ctx.detail.archetype; 444 var pilots = ctx.detail.pilots || []; 445 var formats = ctx.detail.formats || []; 446 var trend = toTrendPoints(ctx.detail.trend || []); 447 448 var header =
449 '<div class="tdtt-panel lb-deck-header">' + 450 '<div class="lb-deck-header-body">' + 451 '<div class="lb-deck-header-chips">' + identityChipsHtml(ctx.game, archetype.identity) + '</div>' + 452 '<h1 class="lb-deck-name">' + esc(archetype.name) + '</h1>' + 453 '</div>' + 454 '<button type="button" class="lb-btn lb-deck-back-btn" id="lb-deck-back-header">Back</button>' + 455 '</div>'; 456 457 var tiles = 458 '<div class="lb-deck-tiles">' + 459 statTileHtml(String(archetype.games), 'Games', 'var(--cyan)') + 460 statTileHtml(archetype.winRate + '%', 'Win Rate', 'var(--gold)') + 461 statTileHtml(archetype.share + '%', 'Play Share', 'var(--magenta)') + 462 statTileHtml(String(archetype.players), 'Pilots', 'var(--green)') + 463 '</div>'; 464 465 var trendPanel = 466 '<div class="tdtt-panel lb-deck-panel"><div class="tdtt-panel__head">Win Rate Trend</div>' + 467 (trend.length === 0 468 ? '<p class="lb-empty-sub lb-deck-center">No decided games recorded yet</p>' 469 : trendSvgHtml(trend)) + 470 '</div>'; 471 472 return ( 473 '<div class="lb-deck-page">' + windowChips + breadcrumbHtml(archetype.name) + 474 header + tiles + trendPanel + 475 pilotsPanelHtml(pilots, ctx) + 476 formatsPanelHtml(formats) + 477 matchupsPanelHtml(ctx) + 478 recentGamesPanelHtml(ctx) + 479 '</div>' 480 ); 481 } 482 483 // Re-wires the deck-key buttons a local DOM patch (the search handler 484 // below) just injected â the top-level generic `[data-deck-key]` 485 // delegation in leaderboard.js's wireEvents() only runs once per full 486 // render(), so anything created afterward via a direct innerHTML write has 487 // to be wired by hand, same as leaderboard.js's own wireHeroGridTriggers() 488 // re-run after its hero-search local patch. 489 function wireCardClicks(container, onSelectDeck) { 490 if (!container || typeof onSelectDeck !== 'function') return; 491 var cards = container.querySelectorAll('[data-deck-key]'); 492 for (var i = 0; i < cards.length; i++) { 493 cards[i].addEventListener('click', function (e) { 494 onSelectDeck(e.currentTarget.getAttribute('data-deck-key')); 495 }); 496 } 497 } 498 499 /** 500 * Wires the body html() just painted into `root` (the leaderboard
501 * section's whole container). Scoped to `.lb-deck-list`/`.lb-deck-page` so 502 * nothing here can double-wire an unrelated element elsewhere on the page. 503 */ 504 function wire(root, ctx) { 505 if (!root) return; 506 var listScope = root.querySelector('.lb-deck-list'); 507 var pageScope = root.querySelector('.lb-deck-page'); 508 var scope = listScope || pageScope; 509 if (!scope) return; 510 511 var windowSeg = scope.querySelector('#lb-deck-window'); 512 if (windowSeg) { 513 var wbtns = windowSeg.querySelectorAll('.t'); 514 for (var wi = 0; wi < wbtns.length; wi++) { 515 wbtns[wi].addEventListener('click', function (e) { 516 ctx.onWindow(e.currentTarget.getAttribute('data-window')); 517 }); 518 } 519 } 520 521 if (listScope) { 522 var sortSeg = listScope.querySelector('#lb-deck-sort'); 523 if (sortSeg) { 524 var sbtns = sortSeg.querySelectorAll('.chip'); 525 for (var si = 0; si < sbtns.length; si++) { 526 sbtns[si].addEventListener('click', function (e) { 527 ctx.onSort(e.currentTarget.getAttribute('data-sort')); 528 }); 529 } 530 } 531 532 var formatSel = listScope.querySelector('#lb-deck-format'); 533 if (formatSel) { 534 formatSel.addEventListener('change', function () { 535 ctx.onFormat(formatSel.value || ''); 536 }); 537 } 538 539 // Client-side only (matchesDeckQuery/sortDecks over the already-fetched 540 // ctx.decks) â kept OUTSIDE a full leaderboard.js render() on every 541 // keystroke, same reasoning leaderboard.js's own hero search gives: a 542 // full innerHTML rebuild would steal focus mid-word. ctx.onQuery only 543 // PERSISTS the value into leaderboard.js's state (no render()), so a 544 // later window/format change starts from the right query. 545 var searchInput = listScope.querySelector('#lb-deck-search'); 546 if (searchInput) { 547 searchInput.addEventListener('input', function () { 548 var q = searchInput.value; 549 ctx.onQuery(q); 550 var gridArea = listScope.querySelector('#lb-deck-gridarea'); 551 if (!gridArea) return; 552 var filtered = []; 553 for (var fi = 0; fi < ctx.decks.length; fi++) { 554 if (window.TDTT.lbDecks.matchesDeckQuery(ctx.decks[fi].name, q)) filtered.push(ctx.decks[fi]); 555 } 556 var sorted = window.TDTT.lbDecks.sortDecks(filtered, ctx.sort); 557 if (!sorted.length) { 558 gridArea.innerHTML = noQueryMatchHtml(q); 559 return; 560 } 561 var cards = ''; 562 for (var ci = 0; ci < sorted.length; ci++) cards += deckCardHtml(sorted[ci], ctx.game); 563 gridArea.innerHTML = '<div class="lb-deck-grid">' + cards + '</div>'; 564 wireCardClicks(gridArea, ctx.onSelectDeck); 565 }); 566 } 567 } 568 569 if (pageScope) { 570 var crumbLb = pageScope.querySelector('#lb-deck-crumb-lb'); 571 if (crumbLb) crumbLb.addEventListener('click', function () { ctx.onLeaderboards(); }); 572 var crumbDecks = pageScope.querySelector('#lb-deck-crumb-decks'); 573 if (crumbDecks) crumbDecks.addEventListener('click', function () { ctx.onBack(); }); 574 575 var backBtns = pageScope.querySelectorAll('#lb-deck-back-header, #lb-deck-back-notfound, #lb-deck-back-error'); 576 for (var bi = 0; bi < backBtns.length; bi++) { 577 backBtns[bi].addEventListener('click', function () { ctx.onBack(); }); 578 } 579 580 var pilotBtns = pageScope.querySelectorAll('.lb-deck-pilot-name'); 581 for (var pi = 0; pi < pilotBtns.length; pi++) { 582 pilotBtns[pi].addEventListener('click', function (e) { 583 ctx.onOpenPlayer(e.currentTarget.getAttribute('data-pid')); 584 }); 585 } 586 } 587 } 588 589 window.TDTT = window.TDTT || {}; 590 window.TDTT.lbDecksView = { 591 listHtml: listHtml, 592 pageHtml: pageHtml, 593 wire: wire, 594 }; 595})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.