┌─ web/src/layouts/TeamLeaderboardLayout.astro ──────────────────────────────┐
│ diff --git a/web/src/layouts/TeamLeaderboardLayout.astro b/web/src/layouts/TeamLea │
│ derboardLayout.astro │
│ deleted file mode 100644 │
│ index 52574e4..0000000 │
│ --- a/web/src/layouts/TeamLeaderboardLayout.astro │
│ +++ /dev/null │
│ @@ -1,730 +0,0 @@ │
│ ---- │
│ -import PageLayout from "../components/PageLayout.astro"; │
│ -import ControlPanel from "../components/ControlPanel.astro"; │
│ -import FormSelect from "../components/FormSelect.astro"; │
│ -import LoadingState from "../components/LoadingState.astro"; │
│ -import WoWClassColors from "../components/WoWClassColors.astro"; │
│ -import "../styles/components.scss"; │
│ - │
│ -export interface Props { │
│ - initialMode?: string; │
│ -} │
│ - │
│ -const { initialMode = "players" } = Astro.props; │
│ -const baseUrl = import.meta.env.BASE_URL; │
│ ---- │
│ - │
│ -<PageLayout │
│ - title="Team Leaderboards" │
│ - description="" │
│ - pageTitle="Team Leaderboards - MoP Classic" │
│ -> │
│ - <WoWClassColors /> │
│ - │
│ - <style> │
│ - .leaderboard-container { │
│ - overflow: hidden; │
│ - margin-top: 20px; │
│ - } │
│ - │
│ - .loading-overlay, │
│ - .error-overlay { │
│ - min-width: 300px; │
│ - } │
│ - │
│ - #content { │
│ - min-height: 400px; │
│ - transition: opacity 0.2s ease; │
│ - } │
│ - │
│ - .content-loading { │
│ - opacity: 0.3; │
│ - pointer-events: none; │
│ - } │
│ - │
│ - .no-data-message { │
│ - margin: 20px 0; │
│ - } │
│ - │
│ - .no-data-content { │
│ - text-align: center; │
│ - padding: 40px 20px; │
│ - } │
│ - │
│ - .no-data-content p { │
│ - color: var(--text-secondary); │
│ - margin: 0; │
│ - font-size: 1.1em; │
│ - } │
│ - │
│ - #team-rows { │
│ - display: table; │
│ - width: 100%; │
│ - table-layout: fixed; │
│ - border-spacing: 0; │
│ - border-collapse: collapse; │
│ - } │
│ - │
│ - @media (max-width: 1024px) { │
│ - #team-rows .leaderboard-row { │
│ - display: block !important; │
│ - padding: 15px; │
│ - } │
│ - │
│ - #team-rows .leaderboard-row > div { │
│ - display: block; │
│ - padding: 5px 0; │
│ - text-align: center; │
│ - } │
│ - } │
│ - │
│ - /* Callout styles */ │
│ - .callout { │
│ - background-color: #2a3a3a; │
│ - border: 1px solid #458588; │
│ - border-left: 4px solid #458588; │
│ - border-radius: 6px; │
│ - padding: 16px; │
│ - margin-bottom: 20px; │
│ - } │
│ - │
│ - .callout-content { │
│ - color: var(--text-primary); │
│ - line-height: 1.5; │
│ - margin: 0; │
│ - } │
│ - </style> │
│ - │
│ - <style is:global> │
│ - /* Global styles for dynamically generated team leaderboard content */ │
│ - #team-rows .leaderboard-row, │
│ - #team-rows .chart-item-header { │
│ - display: table-row !important; │
│ - border-bottom: 1px solid #4a4a4a !important; │
│ - background-color: #32302f !important; │
│ - transition: background-color 0.2s ease !important; │
│ - } │
│ - │
│ - #team-rows .leaderboard-row:hover, │
│ - #team-rows .chart-item-header:hover { │
│ - background-color: rgba(255, 215, 0, 0.05) !important; │
│ - } │
│ - │
│ - /* Expandable chart item styles (copied from ChartLayout.astro) */ │
│ - .chart-item-wrapper { │
│ - border-radius: 6px; │
│ - transition: background-color 0.2s ease; │
│ - } │
│ - │
│ - .chart-item-header { │
│ - cursor: pointer; │
│ - padding: 8px 12px; │
│ - border-radius: 6px; │
│ - transition: background-color 0.2s ease; │
│ - } │
│ - │
│ - .chart-item-header:hover { │
│ - background-color: rgba(255, 255, 255, 0.05); │
│ - } │
│ - │
│ - .chart-item-expanded .chart-item-header { │
│ - background-color: rgba(255, 255, 255, 0.08); │
│ - } │
│ - │
│ - .chart-expand-icon { │
│ - margin-left: 8px; │
│ - font-size: 0.8em; │
│ - color: var(--text-muted); │
│ - transition: transform 0.2s ease; │
│ - user-select: none; │
│ - } │
│ - │
│ - .chart-item-expanded .chart-expand-icon { │
│ - transform: rotate(90deg); │
│ - } │
│ - │
│ - .chart-dropdown { │
│ - margin-top: 12px; │
│ - padding: 16px; │
│ - background-color: var(--bg-selection); │
│ - border-radius: 6px; │
│ - border: 1px solid rgba(255, 255, 255, 0.1); │
│ - display: none; │
│ - } │
│ - │
│ - .chart-item-expanded .chart-dropdown { │
│ - display: block; │
│ - animation: dropdown-expand 0.2s ease-out; │
│ - } │
│ - │
│ - @keyframes dropdown-expand { │
│ - from { │
│ - opacity: 0; │
│ - transform: translateY(-8px); │
│ - } │
│ - to { │
│ - opacity: 1; │
│ - transform: translateY(0); │
│ - } │
│ - } │
│ - </style> │
│ - │
│ - <ControlPanel title="Leaderboard Options"> │
│ - <FormSelect │
│ - id="leaderboard-mode" │
│ - label="Leaderboard Mode" │
│ - options={[ │
│ - { │
│ - value: "teams", │
│ - label: "Teams", │
│ - selected: initialMode === "teams", │
│ - }, │
│ - { │
│ - value: "players", │
│ - label: "Players", │
│ - selected: initialMode === "players", │
│ - }, │
│ - ]} │
│ - /> │
│ - </ControlPanel> │
│ - │
│ - <div class="callout"> │
│ - <div class="callout-content"> │
│ - <p id="leaderboard-description"> │
│ - <strong>Team Leaderboards</strong> identify 3-player cores that have comp │
│ leted │
│ - ALL 9 dungeons. Each team shows their core members plus extended roster. │
│ - Rankings are based on the sum of their best times across all dungeons. │
│ - </p> │
│ - </div> │
│ - </div> │
│ - │
│ - <div id="loading" class="loading-overlay hidden"> │
│ - <LoadingState message="Loading team leaderboard data..." /> │
│ - </div> │
│ - │
│ - <div id="error" class="error-overlay hidden"> │
│ - <LoadingState type="error" /> │
│ - </div> │
│ - │
│ - <div id="no-data" class="no-data-message hidden"> │
│ - <div class="card"> │
│ - <div class="no-data-content"> │
│ - <h3 class="section-title-large">No Teams Found</h3> │
│ - <p> │
│ - No team data was found. Please ensure the team leaderboard generator │
│ - has been run. │
│ - </p> │
│ - </div> │
│ - </div> │
│ - </div> │
│ - │
│ - <div id="content" class="hidden"> │
│ - <div class="card"> │
│ - <div class="info-grid"> │
│ - <div class="info-item"> │
│ - <span class="info-label">Leaderboard</span> │
│ - <span class="info-value" id="leaderboard-title">Loading...</span> │
│ - </div> │
│ - <div class="info-item"> │
│ - <span class="info-label">Generated</span> │
│ - <span class="info-value" id="generated-time">-</span> │
│ - </div> │
│ - </div> │
│ - </div> │
│ - │
│ - <div class="leaderboard-container card"> │
│ - <div id="team-rows"></div> │
│ - </div> │
│ - </div> │
│ -</PageLayout> │
│ - │
│ -<script │
│ - is:inline │
│ - define:vars={{ │
│ - baseUrl, │
│ - initialMode, │
│ - }} │
│ -> │
│ - function formatDuration(milliseconds) { │
│ - const totalSeconds = Math.floor(milliseconds / 1000); │
│ - const minutes = Math.floor(totalSeconds / 60); │
│ - const seconds = totalSeconds % 60; │
│ - const ms = milliseconds % 1000; │
│ - return ( │
│ - minutes + │
│ - ":" + │
│ - seconds.toString().padStart(2, "0") + │
│ - "." + │
│ - ms.toString().padStart(3, "0") │
│ - ); │
│ - } │
│ - │
│ - function formatTimestamp(timestamp) { │
│ - return new Date(timestamp).toLocaleDateString("en-US", { │
│ - month: "short", │
│ - day: "numeric", │
│ - hour: "2-digit", │
│ - minute: "2-digit", │
│ - }); │
│ - } │
│ - │
│ - // Global function for chart item expansion (similar to chart-logic.js) │
│ - window.toggleChartItem = (element) => { │
│ - const wrapper = element.closest(".chart-item-wrapper"); │
│ - if (!wrapper) return; │
│ - │
│ - const isExpanded = wrapper.classList.contains("chart-item-expanded"); │
│ - │
│ - // Close all other expanded items │
│ - document.querySelectorAll(".chart-item-expanded").forEach((item) => { │
│ - if (item !== wrapper) { │
│ - item.classList.remove("chart-item-expanded"); │
│ - } │
│ - }); │
│ - │
│ - // Toggle current item │
│ - wrapper.classList.toggle("chart-item-expanded", !isExpanded); │
│ - }; │
│ - │
│ - function getSpecColor(specId) { │
│ - // WoW class colors based on spec_id │
│ - const specColors = { │
│ - // Death Knight │
│ - 250: "#C41F3B", │
│ - 251: "#C41F3B", │
│ - 252: "#C41F3B", │
│ - // Druid │
│ - 102: "#FF7D0A", │
│ - 103: "#FF7D0A", │
│ - 104: "#FF7D0A", │
│ - 105: "#FF7D0A", │
│ - // Hunter │
│ - 253: "#ABD473", │
│ - 254: "#ABD473", │
│ - 255: "#ABD473", │
│ - // Mage │
│ - 62: "#69CCF0", │
│ - 63: "#69CCF0", │
│ - 64: "#69CCF0", │
│ - // Monk │
│ - 268: "#00FF96", │
│ - 269: "#00FF96", │
│ - 270: "#00FF96", │
│ - // Paladin │
│ - 65: "#F58CBA", │
│ - 66: "#F58CBA", │
│ - 70: "#F58CBA", │
│ - // Priest │
│ - 256: "#FFFFFF", │
│ - 257: "#FFFFFF", │
│ - 258: "#FFFFFF", │
│ - // Rogue │
│ - 259: "#FFF569", │
│ - 260: "#FFF569", │
│ - 261: "#FFF569", │
│ - // Shaman │
│ - 262: "#0070DE", │
│ - 263: "#0070DE", │
│ - 264: "#0070DE", │
│ - // Warlock │
│ - 265: "#9482C9", │
│ - 266: "#9482C9", │
│ - 267: "#9482C9", │
│ - // Warrior │
│ - 71: "#C79C6E", │
│ - 72: "#C79C6E", │
│ - 73: "#C79C6E", │
│ - }; │
│ - return specColors[specId] || "#FFFFFF"; │
│ - } │
│ - │
│ - function createMemberLookup(team) { │
│ - // Create a lookup map from member names to their full member data with spec_ │
│ id │
│ - const memberLookup = {}; │
│ - │
│ - // Process all runs to build a comprehensive lookup │
│ - team.all_runs.forEach((run) => { │
│ - run.members.forEach((member) => { │
│ - if (!memberLookup[member.name]) { │
│ - memberLookup[member.name] = { │
│ - name: member.name, │
│ - realm_slug: member.realm_slug, │
│ - spec_id: member.spec_id, │
│ - id: member.id, │
│ - }; │
│ - } │
│ - }); │
│ - }); │
│ - │
│ - return memberLookup; │
│ - } │
│ - │
│ - function createTeamComposition(memberNames, memberLookup) { │
│ - // Create styled team composition similar to Challenge Mode │
│ - return memberNames │
│ - .map((memberName) => { │
│ - const member = memberLookup[memberName]; │
│ - if (!member) { │
│ - // Fallback if member not found in lookup │
│ - return `<span style="color: var(--text-primary); margin-right: 8px;">${ │
│ memberName}</span>`; │
│ - } │
│ - │
│ - const spec = member.spec_id │
│ - ? window.WoW.getSpecInfo(member.spec_id) │
│ - : null; │
│ - const iconUrl = spec │
│ - ? window.WoW.getSpecIcon(spec.class, spec.spec) │
│ - : null; │
│ - const classColor = spec │
│ - ? window.WoW.getClassColor(spec.class) │
│ - : "#FFFFFF"; │
│ - │
│ - const iconHtml = iconUrl │
│ - ? `<img src="${iconUrl}" alt="${spec.spec} ${spec.class}" style="width: │
│ 16px; height: 16px; border-radius: 2px; margin-right: 4px; vertical-align: middle │
│ ; flex-shrink: 0;">` │
│ - : ""; │
│ - │
│ - return `<span style="display: inline-flex; align-items: center; margin-ri │
│ ght: 8px; gap: 4px;"> │
│ - ${iconHtml} │
│ - <span style="color: ${classColor}; font-weight: 600; font-size: 0.9em;">$ │
│ {member.name}@${member.realm_slug}</span> │
│ - </span>`; │
│ - }) │
│ - .join(""); │
│ - } │
│ - │
│ - function createTeamRow(team) { │
│ - const wrapper = document.createElement("div"); │
│ - wrapper.className = "chart-item-wrapper"; │
│ - │
│ - // Create extended roster display with spec colors │
│ - const extendedRoster = team.extended_roster │
│ - .map((member) => { │
│ - const specColor = member.spec_id │
│ - ? getSpecColor(member.spec_id) │
│ - : "#FFFFFF"; │
│ - return `<span style="display: inline-flex; align-items: center; margin-ri │
│ ght: 8px; gap: 4px;"> │
│ - <span style="color: ${specColor}; font-weight: 600; font-size: 0.9em;">${ │
│ member.name}</span> │
│ - <span style="color: var(--text-secondary); font-size: 0.8em;">@${member.r │
│ ealm_slug}</span> │
│ - </span>`; │
│ - }) │
│ - .join(""); │
│ - │
│ - // Create the team details content │
│ - const teamDetails = createTeamDetailsContent(team); │
│ - │
│ - wrapper.innerHTML = ` │
│ - <div class="leaderboard-row chart-item-header" onclick="toggleChartItem(thi │
│ s)" style="display: table-row !important; border-bottom: 1px solid #4a4a4a !import │
│ ant; background-color: #32302f; transition: background-color 0.2s ease; cursor: po │
│ inter; width: 100%;"> │
│ - <div style="display: table-cell; padding: 15px 10px; vertical-align: midd │
│ le; font-size: 1.2em; font-weight: bold; color: #d8a657; text-align: center; width │
│ : 80px;">#${team.ranking}</div> │
│ - <div style="display: table-cell; padding: 15px 10px; vertical-align: midd │
│ le; font-family: 'Courier New', monospace; font-weight: bold; color: #ffffff; text │
│ -align: right; width: 120px;">${formatDuration(team.combined_best_time)}</div> │
│ - <div style="display: table-cell; padding: 15px 10px; vertical-align: midd │
│ le; display: flex; align-items: center; overflow: hidden;"> │
│ - <span class="chart-expand-icon" style="margin-right: 8px; flex-shrink: │
│ 0;">?</span> │
│ - <div style="display: flex; flex-wrap: wrap; align-items: center; overfl │
│ ow: hidden; min-width: 0;">${extendedRoster}</div> │
│ - </div> │
│ - </div> │
│ - <div class="chart-dropdown"> │
│ - ${teamDetails} │
│ - </div> │
│ - `; │
│ - │
│ - return wrapper; │
│ - } │
│ - │
│ - function createPlayerRow(player) { │
│ - const wrapper = document.createElement("div"); │
│ - wrapper.className = "chart-item-wrapper"; │
│ - │
│ - // Get player spec info for display │
│ - const spec = player.main_spec_id │
│ - ? window.WoW.getSpecInfo(player.main_spec_id) │
│ - : null; │
│ - const iconUrl = spec ? window.WoW.getSpecIcon(spec.class, spec.spec) : null; │
│ - const classColor = spec ? window.WoW.getClassColor(spec.class) : "#FFFFFF"; │
│ - │
│ - const iconHtml = iconUrl │
│ - ? `<img src="${iconUrl}" alt="${spec.spec} ${spec.class}" style="width: 20p │
│ x; height: 20px; border-radius: 2px; margin-right: 8px; vertical-align: middle; fl │
│ ex-shrink: 0;">` │
│ - : ""; │
│ - │
│ - // Create the player details content │
│ - const playerDetails = createPlayerDetailsContent(player); │
│ - │
│ - wrapper.innerHTML = ` │
│ - <div class="leaderboard-row chart-item-header" onclick="toggleChartItem(thi │
│ s.parentElement)" style="display: table-row !important; border-bottom: 1px solid # │
│ 4a4a4a !important; transition: background-color 0.2s ease !important; cursor: poin │
│ ter;"> │
│ - <div style="display: table-cell; padding: 15px 10px; vertical-align: midd │
│ le; font-size: 1.2em; font-weight: bold; color: #d8a657; text-align: center; width │
│ : 80px;">#${player.ranking}</div> │
│ - <div style="display: table-cell; padding: 15px 10px; vertical-align: midd │
│ le; font-family: 'Courier New', monospace; font-weight: bold; color: #ffffff; text │
│ -align: right; width: 120px; white-space: nowrap;">${formatDuration(player.combine │
│ d_best_time)}</div> │
│ - <div style="display: table-cell; padding: 15px 10px; vertical-align: midd │
│ le; width: 100%;"> │
│ - <div style="display: flex; justify-content: space-between; align-items: │
│ center; width: 100%;"> │
│ - <div style="display: flex; align-items: center;"> │
│ - <span class="chart-expand-icon" style="margin-right: 8px;">?</span> │
│ - ${iconHtml} │
│ - <span style="color: ${classColor}; font-weight: 600; font-size: 1.1 │
│ em;">${player.name}</span> │
│ - </div> │
│ - <span style="color: var(--text-secondary); font-size: 0.9em;">@${play │
│ er.realm_slug}</span> │
│ - </div> │
│ - </div> │
│ - </div> │
│ - <div class="chart-dropdown"> │
│ - ${playerDetails} │
│ - </div> │
│ - `; │
│ - │
│ - return wrapper; │
│ - } │
│ - │
│ - function createPlayerDetailsContent(player) { │
│ - // Create dungeon run entries styled like Challenge Mode leaderboard rows │
│ - const dungeonEntries = Object.entries(player.best_runs_per_dungeon || {}) │
│ - .map(([_dungeonSlug, run]) => { │
│ - // Create team composition with spec icons using all_members data │
│ - const teamComposition = (run.all_members || []) │
│ - .map((member) => { │
│ - const spec = member.spec_id │
│ - ? window.WoW.getSpecInfo(member.spec_id) │
│ - : null; │
│ - const iconUrl = spec │
│ - ? window.WoW.getSpecIcon(spec.class, spec.spec) │
│ - : null; │
│ - const classColor = spec │
│ - ? window.WoW.getClassColor(spec.class) │
│ - : "#FFFFFF"; │
│ - │
│ - const iconHtml = iconUrl │
│ - ? `<img src="${iconUrl}" alt="${spec.spec} ${spec.class}" style="wi │
│ dth: 16px; height: 16px; border-radius: 2px; margin-right: 4px; vertical-align: mi │
│ ddle; flex-shrink: 0;">` │
│ - : ""; │
│ - │
│ - return `<span style="display: inline-flex; align-items: center; margi │
│ n-right: 8px; gap: 4px;"> │
│ - ${iconHtml} │
│ - <span style="color: ${classColor}; font-weight: 600; font-size: 0.9em;" │
│ >${member.name}@${member.realm_slug}</span> │
│ - </span>`; │
│ - }) │
│ - .join(""); │
│ - │
│ - return ` │
│ - <div style="display: grid; grid-template-columns: 80px 120px 2fr 100px 14 │
│ 0px; gap: 20px; align-items: center; padding: 15px 20px; border-bottom: 1px solid │
│ #3a3a3a; min-height: 60px;"> │
│ - <div style="font-size: 1.2em; font-weight: bold; color: #d8a657; text-a │
│ lign: center;">${run.ranking === "~" ? "~" : "#" + run.ranking}</div> │
│ - <div style="font-family: 'Courier New', monospace; font-weight: bold; c │
│ olor: #ffffff; text-align: right;">${formatDuration(run.duration)}</div> │
│ - <div style="display: flex; flex-wrap: wrap; gap: 6px; justify-content: │
│ flex-start; align-items: center;">${teamComposition}</div> │
│ - <div style="color: var(--text-primary); text-align: center; font-weight │
│ : 600;">${run.dungeon_name}</div> │
│ - <div style="color: #aaaaaa; font-size: 0.9em; text-align: center;">${fo │
│ rmatTimestamp(run.completed_timestamp)}</div> │
│ - </div> │
│ - `; │
│ - }) │
│ - .join(""); │
│ - │
│ - return ` │
│ - <div> │
│ - <h4 style="color: var(--highlight-color); margin-bottom: 10px;">Best Time │
│ s Per Dungeon</h4> │
│ - <div style="border-radius: 6px; overflow: hidden; border: 1px solid #4a4a │
│ 4a;"> │
│ - <div style="display: grid; grid-template-columns: 80px 120px 2fr 100px │
│ 140px; gap: 20px; padding: 15px 20px; background-color: rgba(255, 255, 255, 0.03); │
│ border-bottom: 1px solid #4a4a4a; font-weight: 600; color: var(--text-secondary); │
│ font-size: 0.9em; text-transform: uppercase; letter-spacing: 0.5px;"> │
│ - <div>Global Rank</div> │
│ - <div style="text-align: right;">Time</div> │
│ - <div>Team Composition</div> │
│ - <div style="text-align: center;">Dungeon</div> │
│ - <div style="text-align: center;">Date</div> │
│ - </div> │
│ - ${dungeonEntries} │
│ - </div> │
│ - </div> │
│ - `; │
│ - } │
│ - │
│ - function createTeamDetailsContent(team) { │
│ - // Create member lookup for this team │
│ - const memberLookup = createMemberLookup(team); │
│ - │
│ - // Create core members display with Challenge Mode styling │
│ - const coreMembers = team.core_members │
│ - .map((member) => { │
│ - const spec = member.spec_id │
│ - ? window.WoW.getSpecInfo(member.spec_id) │
│ - : null; │
│ - const iconUrl = spec │
│ - ? window.WoW.getSpecIcon(spec.class, spec.spec) │
│ - : null; │
│ - const classColor = spec │
│ - ? window.WoW.getClassColor(spec.class) │
│ - : "#FFFFFF"; │
│ - │
│ - const iconHtml = iconUrl │
│ - ? `<img src="${iconUrl}" alt="${spec.spec} ${spec.class}" style="width: │
│ 16px; height: 16px; border-radius: 2px; margin-right: 4px; vertical-align: middle │
│ ; flex-shrink: 0;">` │
│ - : ""; │
│ - │
│ - return `<span style="display: inline-flex; align-items: center; margin-ri │
│ ght: 15px; gap: 4px;"> │
│ - ${iconHtml} │
│ - <span style="color: ${classColor}; font-weight: 600; font-size: 0.9em;">$ │
│ {member.name}@${member.realm_slug}</span> │
│ - </span>`; │
│ - }) │
│ - .join(""); │
│ - │
│ - // Create dungeon run entries styled like Challenge Mode leaderboard rows │
│ - const dungeonEntries = Object.entries(team.best_runs_per_dungeon || {}) │
│ - .map(([_dungeonSlug, run]) => { │
│ - const teamComposition = createTeamComposition( │
│ - run.members, │
│ - memberLookup, │
│ - ); │
│ - │
│ - return ` │
│ - <div style="display: grid; grid-template-columns: 80px 120px 2fr 100px 14 │
│ 0px; gap: 20px; align-items: center; padding: 15px 20px; border-bottom: 1px solid │
│ #3a3a3a; min-height: 60px;"> │
│ - <div style="font-size: 1.2em; font-weight: bold; color: #d8a657; text-a │
│ lign: center;">#${run.ranking}</div> │
│ - <div style="font-family: 'Courier New', monospace; font-weight: bold; c │
│ olor: #ffffff; text-align: right;">${formatDuration(run.duration)}</div> │
│ - <div style="display: flex; flex-wrap: wrap; gap: 6px; justify-content: │
│ flex-start; align-items: center;">${teamComposition}</div> │
│ - <div style="color: var(--text-primary); text-align: center; font-weight │
│ : 600;">${run.dungeon_name}</div> │
│ - <div style="color: #aaaaaa; font-size: 0.9em; text-align: center;">${fo │
│ rmatTimestamp(run.completed_timestamp)}</div> │
│ - </div> │
│ - `; │
│ - }) │
│ - .join(""); │
│ - │
│ - return ` │
│ - <div style="margin-bottom: 20px;"> │
│ - <h4 style="color: var(--highlight-color); margin-bottom: 10px;">Core Team │
│ (3-player consistent core)</h4> │
│ - <div style="display: flex; flex-wrap: wrap; align-items: center;">${coreM │
│ embers}</div> │
│ - </div> │
│ - │
│ - <div> │
│ - <h4 style="color: var(--highlight-color); margin-bottom: 10px;">Best Time │
│ s Per Dungeon</h4> │
│ - <div style="border-radius: 6px; overflow: hidden; border: 1px solid #4a4a │
│ 4a;"> │
│ - <div style="display: grid; grid-template-columns: 80px 120px 2fr 100px │
│ 140px; gap: 20px; padding: 15px 20px; background-color: rgba(255, 255, 255, 0.03); │
│ border-bottom: 1px solid #4a4a4a; font-weight: 600; color: var(--text-secondary); │
│ font-size: 0.9em; text-transform: uppercase; letter-spacing: 0.5px;"> │
│ - <div>Rank</div> │
│ - <div style="text-align: right;">Time</div> │
│ - <div>Team Composition</div> │
│ - <div style="text-align: center;">Dungeon</div> │
│ - <div style="text-align: center;">Date</div> │
│ - </div> │
│ - ${dungeonEntries} │
│ - </div> │
│ - </div> │
│ - `; │
│ - } │
│ - │
│ - function displayLeaderboard(data, mode) { │
│ - console.log(`Displaying ${mode} leaderboard data:`, data); │
│ - │
│ - if (!data.leaderboard || data.leaderboard.length === 0) { │
│ - showNoData(); │
│ - return; │
│ - } │
│ - │
│ - document.getElementById("leaderboard-title").textContent = data.title; │
│ - document.getElementById("generated-time").textContent = │
│ - data.generated_timestamp │
│ - ? formatTimestamp(data.generated_timestamp) │
│ - : "Unknown"; │
│ - │
│ - const rowsContainer = document.getElementById("team-rows"); │
│ - rowsContainer.innerHTML = ""; │
│ - │
│ - data.leaderboard.forEach((item, index) => { │
│ - // Add ranking based on position in sorted array │
│ - item.ranking = index + 1; │
│ - │
│ - const wrapper = │
│ - mode === "players" ? createPlayerRow(item) : createTeamRow(item); │
│ - rowsContainer.appendChild(wrapper); │
│ - }); │
│ - │
│ - document.getElementById("loading").classList.add("hidden"); │
│ - document.getElementById("error").classList.add("hidden"); │
│ - document.getElementById("no-data").classList.add("hidden"); │
│ - document.getElementById("content").classList.remove("content-loading"); │
│ - document.getElementById("content").classList.remove("hidden"); │
│ - │
│ - console.log(`${mode} leaderboard displayed successfully`); │
│ - } │
│ - │
│ - function showNoData() { │
│ - document.getElementById("loading").classList.add("hidden"); │
│ - document.getElementById("error").classList.add("hidden"); │
│ - document.getElementById("no-data").classList.remove("hidden"); │
│ - document.getElementById("content").classList.add("hidden"); │
│ - } │
│ - │
│ - function showError(message) { │
│ - const errorDiv = document.getElementById("error"); │
│ - const loadingMessage = errorDiv.querySelector(".loading-message"); │
│ - if (loadingMessage) { │
│ - loadingMessage.textContent = message; │
│ - } │
│ - errorDiv.classList.remove("hidden"); │
│ - document.getElementById("loading").classList.add("hidden"); │
│ - document.getElementById("no-data").classList.add("hidden"); │
│ - document.getElementById("content").classList.add("hidden"); │
│ - } │
│ - │
│ - function showLoading() { │
│ - document.getElementById("loading").classList.remove("hidden"); │
│ - document.getElementById("error").classList.add("hidden"); │
│ - document.getElementById("no-data").classList.add("hidden"); │
│ - document.getElementById("content").classList.add("content-loading"); │
│ - } │
│ - │
│ - async function loadLeaderboard() { │
│ - const mode = document.getElementById("leaderboard-mode").value; │
│ - │
│ - showLoading(); │
│ - │
│ - try { │
│ - const dataPath = │
│ - mode === "players" ? "player-leaderboards" : "team-leaderboards"; │
│ - const fileName = "best-overall.json"; │
│ - const fullUrl = `/data/${dataPath}/${fileName}`; │
│ - console.log(`Loading ${mode} leaderboard from:`, fullUrl); │
│ - │
│ - const response = await fetch(fullUrl); │
│ - │
│ - if (!response.ok) { │
│ - throw new Error( │
│ - `Failed to load ${mode} leaderboard data: ` + response.statusText, │
│ - ); │
│ - } │
│ - │
│ - const data = await response.json(); │
│ - console.log(`${mode} leaderboard data loaded:`, data); │
│ - displayLeaderboard(data, mode); │
│ - │
│ - // Update description based on mode │
│ - updateDescription(mode); │
│ - } catch (error) { │
│ - console.error(`Error loading ${mode} leaderboard:`, error); │
│ - showError(`Error loading ${mode} leaderboard data: ` + error.message); │
│ - } │
│ - } │
│ - │
│ - function updateDescription(mode) { │
│ - const description = document.getElementById("leaderboard-description"); │
│ - if (mode === "players") { │
│ - description.innerHTML = │
│ - "<strong>Player Leaderboards</strong> identify individual players who hav │
│ e completed ALL 9 dungeons. Rankings are based on the sum of their best times acro │
│ ss all dungeons."; │
│ - } else { │
│ - description.innerHTML = │
│ - "<strong>Team Leaderboards</strong> identify 3-player cores that have com │
│ pleted ALL 9 dungeons. Each team shows their core members plus extended roster. Ra │
│ nkings are based on the sum of their best times across all dungeons."; │
│ - } │
│ - } │
│ - │
│ - function updateURL() { │
│ - const mode = document.getElementById("leaderboard-mode").value; │
│ - const newURL = │
│ - mode === "players" ? `/player-leaderboards` : `/team-leaderboards`; │
│ - window.history.pushState({}, "", newURL); │
│ - } │
│ - │
│ - // Event listeners │
│ - document.addEventListener("DOMContentLoaded", () => { │
│ - // Set initial value if provided │
│ - if (initialMode) { │
│ - document.getElementById("leaderboard-mode").value = initialMode; │
│ - loadLeaderboard(); │
│ - } │
│ - │
│ - // Bind change event │
│ - document │
│ - .getElementById("leaderboard-mode") │
│ - .addEventListener("change", () => { │
│ - console.log( │
│ - "Leaderboard mode changed to:", │
│ - document.getElementById("leaderboard-mode").value, │
│ - ); │
│ - updateURL(); │
│ - loadLeaderboard(); │
│ - }); │
│ - }); │
│ -</script> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...s/TeamLeaderboardLayout.astro ───┐
│ diff --git a/web/src/layouts/TeamLeaderboard │
│ Layout.astro b/web/src/layouts/TeamLeaderboa │
│ rdLayout.astro │
│ deleted file mode 100644 │
│ index 52574e4..0000000 │
│ --- a/web/src/layouts/TeamLeaderboardLayout. │
│ astro │
│ +++ /dev/null │
│ @@ -1,730 +0,0 @@ │
│ ---- │
│ -import PageLayout from "../components/PageL │
│ ayout.astro"; │
│ -import ControlPanel from "../components/Con │
│ trolPanel.astro"; │
│ -import FormSelect from "../components/FormS │
│ elect.astro"; │
│ -import LoadingState from "../components/Loa │
│ dingState.astro"; │
│ -import WoWClassColors from "../components/W │
│ oWClassColors.astro"; │
│ -import "../styles/components.scss"; │
│ - │
│ -export interface Props { │
│ - initialMode?: string; │
│ -} │
│ - │
│ -const { initialMode = "players" } = Astro.p │
│ rops; │
│ -const baseUrl = import.meta.env.BASE_URL; │
│ ---- │
│ - │
│ -<PageLayout │
│ - title="Team Leaderboards" │
│ - description="" │
│ - pageTitle="Team Leaderboards - MoP Classi │
│ c" │
│ -> │
│ - <WoWClassColors /> │
│ - │
│ - <style> │
│ - .leaderboard-container { │
│ - overflow: hidden; │
│ - margin-top: 20px; │
│ - } │
│ - │
│ - .loading-overlay, │
│ - .error-overlay { │
│ - min-width: 300px; │
│ - } │
│ - │
│ - #content { │
│ - min-height: 400px; │
│ - transition: opacity 0.2s ease; │
│ - } │
│ - │
│ - .content-loading { │
│ - opacity: 0.3; │
│ - pointer-events: none; │
│ - } │
│ - │
│ - .no-data-message { │
│ - margin: 20px 0; │
│ - } │
│ - │
│ - .no-data-content { │
│ - text-align: center; │
│ - padding: 40px 20px; │
│ - } │
│ - │
│ - .no-data-content p { │
│ - color: var(--text-secondary); │
│ - margin: 0; │
│ - font-size: 1.1em; │
│ - } │
│ - │
│ - #team-rows { │
│ - display: table; │
│ - width: 100%; │
│ - table-layout: fixed; │
│ - border-spacing: 0; │
│ - border-collapse: collapse; │
│ - } │
│ - │
│ - @media (max-width: 1024px) { │
│ - #team-rows .leaderboard-row { │
│ - display: block !important; │
│ - padding: 15px; │
│ - } │
│ - │
│ - #team-rows .leaderboard-row > div { │
│ - display: block; │
│ - padding: 5px 0; │
│ - text-align: center; │
│ - } │
│ - } │
│ - │
│ - /* Callout styles */ │
│ - .callout { │
│ - background-color: #2a3a3a; │
│ - border: 1px solid #458588; │
│ - border-left: 4px solid #458588; │
│ - border-radius: 6px; │
│ - padding: 16px; │
│ - margin-bottom: 20px; │
│ - } │
│ - │
│ - .callout-content { │
│ - color: var(--text-primary); │
│ - line-height: 1.5; │
│ - margin: 0; │
│ - } │
│ - </style> │
│ - │
│ - <style is:global> │
│ - /* Global styles for dynamically genera │
│ ted team leaderboard content */ │
│ - #team-rows .leaderboard-row, │
│ - #team-rows .chart-item-header { │
│ - display: table-row !important; │
│ - border-bottom: 1px solid #4a4a4a !imp │
│ ortant; │
│ - background-color: #32302f !important; │
│ - transition: background-color 0.2s eas │
│ e !important; │
│ - } │
│ - │
│ - #team-rows .leaderboard-row:hover, │
│ - #team-rows .chart-item-header:hover { │
│ - background-color: rgba(255, 215, 0, 0 │
│ .05) !important; │
│ - } │
│ - │
│ - /* Expandable chart item styles (copied │
│ from ChartLayout.astro) */ │
│ - .chart-item-wrapper { │
│ - border-radius: 6px; │
│ - transition: background-color 0.2s eas │
│ e; │
│ - } │
│ - │
│ - .chart-item-header { │
│ - cursor: pointer; │
│ - padding: 8px 12px; │
│ - border-radius: 6px; │
│ - transition: background-color 0.2s eas │
│ e; │
│ - } │
│ - │
│ - .chart-item-header:hover { │
│ - background-color: rgba(255, 255, 255, │
│ 0.05); │
│ - } │
│ - │
│ - .chart-item-expanded .chart-item-header │
│ { │
│ - background-color: rgba(255, 255, 255, │
│ 0.08); │
│ - } │
│ - │
│ - .chart-expand-icon { │
│ - margin-left: 8px; │
│ - font-size: 0.8em; │
│ - color: var(--text-muted); │
│ - transition: transform 0.2s ease; │
│ - user-select: none; │
│ - } │
│ - │
│ - .chart-item-expanded .chart-expand-icon │
│ { │
│ - transform: rotate(90deg); │
│ - } │
│ - │
│ - .chart-dropdown { │
│ - margin-top: 12px; │
│ - padding: 16px; │
│ - background-color: var(--bg-selection) │
│ ; │
│ - border-radius: 6px; │
│ - border: 1px solid rgba(255, 255, 255, │
│ 0.1); │
│ - display: none; │
│ - } │
│ - │
│ - .chart-item-expanded .chart-dropdown { │
│ - display: block; │
│ - animation: dropdown-expand 0.2s ease- │
│ out; │
│ - } │
│ - │
│ - @keyframes dropdown-expand { │
│ - from { │
│ - opacity: 0; │
│ - transform: translateY(-8px); │
│ - } │
│ - to { │
│ - opacity: 1; │
│ - transform: translateY(0); │
│ - } │
│ - } │
│ - </style> │
│ - │
│ - <ControlPanel title="Leaderboard Options" │
│ > │
│ - <FormSelect │
│ - id="leaderboard-mode" │
│ - label="Leaderboard Mode" │
│ - options={[ │
│ - { │
│ - value: "teams", │
│ - label: "Teams", │
│ - selected: initialMode === "teams" │
│ , │
│ - }, │
│ - { │
│ - value: "players", │
│ - label: "Players", │
│ - selected: initialMode === "player │
│ s", │
│ - }, │
│ - ]} │
│ - /> │
│ - </ControlPanel> │
│ - │
│ - <div class="callout"> │
│ - <div class="callout-content"> │
│ - <p id="leaderboard-description"> │
│ - <strong>Team Leaderboards</strong> │
│ identify 3-player cores that have completed │
│ - ALL 9 dungeons. Each team shows the │
│ ir core members plus extended roster. │
│ - Rankings are based on the sum of th │
│ eir best times across all dungeons. │
│ - </p> │
│ - </div> │
│ - </div> │
│ - │
│ - <div id="loading" class="loading-overlay │
│ hidden"> │
│ - <LoadingState message="Loading team lea │
│ derboard data..." /> │
│ - </div> │
│ - │
│ - <div id="error" class="error-overlay hidd │
│ en"> │
│ - <LoadingState type="error" /> │
│ - </div> │
│ - │
│ - <div id="no-data" class="no-data-message │
│ hidden"> │
│ - <div class="card"> │
│ - <div class="no-data-content"> │
│ - <h3 class="section-title-large">No │
│ Teams Found</h3> │
│ - <p> │
│ - No team data was found. Please en │
│ sure the team leaderboard generator │
│ - has been run. │
│ - </p> │
│ - </div> │
│ - </div> │
│ - </div> │
│ - │
│ - <div id="content" class="hidden"> │
│ - <div class="card"> │
│ - <div class="info-grid"> │
│ - <div class="info-item"> │
│ - <span class="info-label">Leaderbo │
│ ard</span> │
│ - <span class="info-value" id="lead │
│ erboard-title">Loading...</span> │
│ - </div> │
│ - <div class="info-item"> │
│ - <span class="info-label">Generate │
│ d</span> │
│ - <span class="info-value" id="gene │
│ rated-time">-</span> │
│ - </div> │
│ - </div> │
│ - </div> │
│ - │
│ - <div class="leaderboard-container card" │
│ > │
│ - <div id="team-rows"></div> │
│ - </div> │
│ - </div> │
│ -</PageLayout> │
│ - │
│ -<script │
│ - is:inline │
│ - define:vars={{ │
│ - baseUrl, │
│ - initialMode, │
│ - }} │
│ -> │
│ - function formatDuration(milliseconds) { │
│ - const totalSeconds = Math.floor(millise │
│ conds / 1000); │
│ - const minutes = Math.floor(totalSeconds │
│ / 60); │
│ - const seconds = totalSeconds % 60; │
│ - const ms = milliseconds % 1000; │
│ - return ( │
│ - minutes + │
│ - ":" + │
│ - seconds.toString().padStart(2, "0") + │
│ - "." + │
│ - ms.toString().padStart(3, "0") │
│ - ); │
│ - } │
│ - │
│ - function formatTimestamp(timestamp) { │
│ - return new Date(timestamp).toLocaleDate │
│ String("en-US", { │
│ - month: "short", │
│ - day: "numeric", │
│ - hour: "2-digit", │
│ - minute: "2-digit", │
│ - }); │
│ - } │
│ - │
│ - // Global function for chart item expansi │
│ on (similar to chart-logic.js) │
│ - window.toggleChartItem = (element) => { │
│ - const wrapper = element.closest(".chart │
│ -item-wrapper"); │
│ - if (!wrapper) return; │
│ - │
│ - const isExpanded = wrapper.classList.co │
│ ntains("chart-item-expanded"); │
│ - │
│ - // Close all other expanded items │
│ - document.querySelectorAll(".chart-item- │
│ expanded").forEach((item) => { │
│ - if (item !== wrapper) { │
│ - item.classList.remove("chart-item-e │
│ xpanded"); │
│ - } │
│ - }); │
│ - │
│ - // Toggle current item │
│ - wrapper.classList.toggle("chart-item-ex │
│ panded", !isExpanded); │
│ - }; │
│ - │
│ - function getSpecColor(specId) { │
│ - // WoW class colors based on spec_id │
│ - const specColors = { │
│ - // Death Knight │
│ - 250: "#C41F3B", │
│ - 251: "#C41F3B", │
│ - 252: "#C41F3B", │
│ - // Druid │
│ - 102: "#FF7D0A", │
│ - 103: "#FF7D0A", │
│ - 104: "#FF7D0A", │
│ - 105: "#FF7D0A", │
│ - // Hunter │
│ - 253: "#ABD473", │
│ - 254: "#ABD473", │
│ - 255: "#ABD473", │
│ - // Mage │
│ - 62: "#69CCF0", │
│ - 63: "#69CCF0", │
│ - 64: "#69CCF0", │
│ - // Monk │
│ - 268: "#00FF96", │
│ - 269: "#00FF96", │
│ - 270: "#00FF96", │
│ - // Paladin │
│ - 65: "#F58CBA", │
│ - 66: "#F58CBA", │
│ - 70: "#F58CBA", │
│ - // Priest │
│ - 256: "#FFFFFF", │
│ - 257: "#FFFFFF", │
│ - 258: "#FFFFFF", │
│ - // Rogue │
│ - 259: "#FFF569", │
│ - 260: "#FFF569", │
│ - 261: "#FFF569", │
│ - // Shaman │
│ - 262: "#0070DE", │
│ - 263: "#0070DE", │
│ - 264: "#0070DE", │
│ - // Warlock │
│ - 265: "#9482C9", │
│ - 266: "#9482C9", │
│ - 267: "#9482C9", │
│ - // Warrior │
│ - 71: "#C79C6E", │
│ - 72: "#C79C6E", │
│ - 73: "#C79C6E", │
│ - }; │
│ - return specColors[specId] || "#FFFFFF"; │
│ - } │
│ - │
│ - function createMemberLookup(team) { │
│ - // Create a lookup map from member name │
│ s to their full member data with spec_id │
│ - const memberLookup = {}; │
│ - │
│ - // Process all runs to build a comprehe │
│ nsive lookup │
│ - team.all_runs.forEach((run) => { │
│ - run.members.forEach((member) => { │
│ - if (!memberLookup[member.name]) { │
│ - memberLookup[member.name] = { │
│ - name: member.name, │
│ - realm_slug: member.realm_slug, │
│ - spec_id: member.spec_id, │
│ - id: member.id, │
│ - }; │
│ - } │
│ - }); │
│ - }); │
│ - │
│ - return memberLookup; │
│ - } │
│ - │
│ - function createTeamComposition(memberName │
│ s, memberLookup) { │
│ - // Create styled team composition simil │
│ ar to Challenge Mode │
│ - return memberNames │
│ - .map((memberName) => { │
│ - const member = memberLookup[memberN │
│ ame]; │
│ - if (!member) { │
│ - // Fallback if member not found i │
│ n lookup │
│ - return `<span style="color: var(- │
│ -text-primary); margin-right: 8px;">${member │
│ Name}</span>`; │
│ - } │
│ - │
│ - const spec = member.spec_id │
│ - ? window.WoW.getSpecInfo(member.s │
│ pec_id) │
│ - : null; │
│ - const iconUrl = spec │
│ - ? window.WoW.getSpecIcon(spec.cla │
│ ss, spec.spec) │
│ - : null; │
│ - const classColor = spec │
│ - ? window.WoW.getClassColor(spec.c │
│ lass) │
│ - : "#FFFFFF"; │
│ - │
│ - const iconHtml = iconUrl │
│ - ? `<img src="${iconUrl}" alt="${s │
│ pec.spec} ${spec.class}" style="width: 16px; │
│ height: 16px; border-radius: 2px; margin-ri │
│ ght: 4px; vertical-align: middle; flex-shrin │
│ k: 0;">` │
│ - : ""; │
│ - │
│ - return `<span style="display: inlin │
│ e-flex; align-items: center; margin-right: 8 │
│ px; gap: 4px;"> │
│ - ${iconHtml} │
│ - <span style="color: ${classColor}; │
│ font-weight: 600; font-size: 0.9em;">${membe │
│ r.name}@${member.realm_slug}</span> │
│ - </span>`; │
│ - }) │
│ - .join(""); │
│ - } │
│ - │
│ - function createTeamRow(team) { │
│ - const wrapper = document.createElement( │
│ "div"); │
│ - wrapper.className = "chart-item-wrapper │
│ "; │
│ - │
│ - // Create extended roster display with │
│ spec colors │
│ - const extendedRoster = team.extended_ro │
│ ster │
│ - .map((member) => { │
│ - const specColor = member.spec_id │
│ - ? getSpecColor(member.spec_id) │
│ - : "#FFFFFF"; │
│ - return `<span style="display: inlin │
│ e-flex; align-items: center; margin-right: 8 │
│ px; gap: 4px;"> │
│ - <span style="color: ${specColor}; f │
│ ont-weight: 600; font-size: 0.9em;">${member │
│ .name}</span> │
│ - <span style="color: var(--text-seco │
│ ndary); font-size: 0.8em;">@${member.realm_s │
│ lug}</span> │
│ - </span>`; │
│ - }) │
│ - .join(""); │
│ - │
│ - // Create the team details content │
│ - const teamDetails = createTeamDetailsCo │
│ ntent(team); │
│ - │
│ - wrapper.innerHTML = ` │
│ - <div class="leaderboard-row chart-ite │
│ m-header" onclick="toggleChartItem(this)" st │
│ yle="display: table-row !important; border-b │
│ ottom: 1px solid #4a4a4a !important; backgro │
│ und-color: #32302f; transition: background-c │
│ olor 0.2s ease; cursor: pointer; width: 100% │
│ ;"> │
│ - <div style="display: table-cell; pa │
│ dding: 15px 10px; vertical-align: middle; fo │
│ nt-size: 1.2em; font-weight: bold; color: #d │
│ 8a657; text-align: center; width: 80px;">#${ │
│ team.ranking}</div> │
│ - <div style="display: table-cell; pa │
│ dding: 15px 10px; vertical-align: middle; fo │
│ nt-family: 'Courier New', monospace; font-we │
│ ight: bold; color: #ffffff; text-align: righ │
│ t; width: 120px;">${formatDuration(team.comb │
│ ined_best_time)}</div> │
│ - <div style="display: table-cell; pa │
│ dding: 15px 10px; vertical-align: middle; di │
│ splay: flex; align-items: center; overflow: │
│ hidden;"> │
│ - <span class="chart-expand-icon" s │
│ tyle="margin-right: 8px; flex-shrink: 0;">?< │
│ /span> │
│ - <div style="display: flex; flex-w │
│ rap: wrap; align-items: center; overflow: hi │
│ dden; min-width: 0;">${extendedRoster}</div> │
│ - </div> │
│ - </div> │
│ - <div class="chart-dropdown"> │
│ - ${teamDetails} │
│ - </div> │
│ - `; │
│ - │
│ - return wrapper; │
│ - } │
│ - │
│ - function createPlayerRow(player) { │
│ - const wrapper = document.createElement( │
│ "div"); │
│ - wrapper.className = "chart-item-wrapper │
│ "; │
│ - │
│ - // Get player spec info for display │
│ - const spec = player.main_spec_id │
│ - ? window.WoW.getSpecInfo(player.main_ │
│ spec_id) │
│ - : null; │
│ - const iconUrl = spec ? window.WoW.getSp │
│ ecIcon(spec.class, spec.spec) : null; │
│ - const classColor = spec ? window.WoW.ge │
│ tClassColor(spec.class) : "#FFFFFF"; │
│ - │
│ - const iconHtml = iconUrl │
│ - ? `<img src="${iconUrl}" alt="${spec. │
│ spec} ${spec.class}" style="width: 20px; hei │
│ ght: 20px; border-radius: 2px; margin-right: │
│ 8px; vertical-align: middle; flex-shrink: 0 │
│ ;">` │
│ - : ""; │
│ - │
│ - // Create the player details content │
│ - const playerDetails = createPlayerDetai │
│ lsContent(player); │
│ - │
│ - wrapper.innerHTML = ` │
│ - <div class="leaderboard-row chart-ite │
│ m-header" onclick="toggleChartItem(this.pare │
│ ntElement)" style="display: table-row !impor │
│ tant; border-bottom: 1px solid #4a4a4a !impo │
│ rtant; transition: background-color 0.2s eas │
│ e !important; cursor: pointer;"> │
│ - <div style="display: table-cell; pa │
│ dding: 15px 10px; vertical-align: middle; fo │
│ nt-size: 1.2em; font-weight: bold; color: #d │
│ 8a657; text-align: center; width: 80px;">#${ │
│ player.ranking}</div> │
│ - <div style="display: table-cell; pa │
│ dding: 15px 10px; vertical-align: middle; fo │
│ nt-family: 'Courier New', monospace; font-we │
│ ight: bold; color: #ffffff; text-align: righ │
│ t; width: 120px; white-space: nowrap;">${for │
│ matDuration(player.combined_best_time)}</div │
│ > │
│ - <div style="display: table-cell; pa │
│ dding: 15px 10px; vertical-align: middle; wi │
│ dth: 100%;"> │
│ - <div style="display: flex; justif │
│ y-content: space-between; align-items: cente │
│ r; width: 100%;"> │
│ - <div style="display: flex; alig │
│ n-items: center;"> │
│ - <span class="chart-expand-ico │
│ n" style="margin-right: 8px;">?</span> │
│ - ${iconHtml} │
│ - <span style="color: ${classCo │
│ lor}; font-weight: 600; font-size: 1.1em;">$ │
│ {player.name}</span> │
│ - </div> │
│ - <span style="color: var(--text- │
│ secondary); font-size: 0.9em;">@${player.rea │
│ lm_slug}</span> │
│ - </div> │
│ - </div> │
│ - </div> │
│ - <div class="chart-dropdown"> │
│ - ${playerDetails} │
│ - </div> │
│ - `; │
│ - │
│ - return wrapper; │
│ - } │
│ - │
│ - function createPlayerDetailsContent(playe │
│ r) { │
│ - // Create dungeon run entries styled li │
│ ke Challenge Mode leaderboard rows │
│ - const dungeonEntries = Object.entries(p │
│ layer.best_runs_per_dungeon || {}) │
│ - .map(([_dungeonSlug, run]) => { │
│ - // Create team composition with spe │
│ c icons using all_members data │
│ - const teamComposition = (run.all_me │
│ mbers || []) │
│ - .map((member) => { │
│ - const spec = member.spec_id │
│ - ? window.WoW.getSpecInfo(memb │
│ er.spec_id) │
│ - : null; │
│ - const iconUrl = spec │
│ - ? window.WoW.getSpecIcon(spec │
│ .class, spec.spec) │
│ - : null; │
│ - const classColor = spec │
│ - ? window.WoW.getClassColor(sp │
│ ec.class) │
│ - : "#FFFFFF"; │
│ - │
│ - const iconHtml = iconUrl │
│ - ? `<img src="${iconUrl}" alt= │
│ "${spec.spec} ${spec.class}" style="width: 1 │
│ 6px; height: 16px; border-radius: 2px; margi │
│ n-right: 4px; vertical-align: middle; flex-s │
│ hrink: 0;">` │
│ - : ""; │
│ - │
│ - return `<span style="display: i │
│ nline-flex; align-items: center; margin-righ │
│ t: 8px; gap: 4px;"> │
│ - ${iconHtml} │
│ - <span style="color: ${classColor} │
│ ; font-weight: 600; font-size: 0.9em;">${mem │
│ ber.name}@${member.realm_slug}</span> │
│ - </span>`; │
│ - }) │
│ - .join(""); │
│ - │
│ - return ` │
│ - <div style="display: grid; grid-tem │
│ plate-columns: 80px 120px 2fr 100px 140px; g │
│ ap: 20px; align-items: center; padding: 15px │
│ 20px; border-bottom: 1px solid #3a3a3a; min │
│ -height: 60px;"> │
│ - <div style="font-size: 1.2em; fon │
│ t-weight: bold; color: #d8a657; text-align: │
│ center;">${run.ranking === "~" ? "~" : "#" + │
│ run.ranking}</div> │
│ - <div style="font-family: 'Courier │
│ New', monospace; font-weight: bold; color: │
│ #ffffff; text-align: right;">${formatDuratio │
│ n(run.duration)}</div> │
│ - <div style="display: flex; flex-w │
│ rap: wrap; gap: 6px; justify-content: flex-s │
│ tart; align-items: center;">${teamCompositio │
│ n}</div> │
│ - <div style="color: var(--text-pri │
│ mary); text-align: center; font-weight: 600; │
│ ">${run.dungeon_name}</div> │
│ - <div style="color: #aaaaaa; font- │
│ size: 0.9em; text-align: center;">${formatTi │
│ mestamp(run.completed_timestamp)}</div> │
│ - </div> │
│ - `; │
│ - }) │
│ - .join(""); │
│ - │
│ - return ` │
│ - <div> │
│ - <h4 style="color: var(--highlight-c │
│ olor); margin-bottom: 10px;">Best Times Per │
│ Dungeon</h4> │
│ - <div style="border-radius: 6px; ove │
│ rflow: hidden; border: 1px solid #4a4a4a;"> │
│ - <div style="display: grid; grid-t │
│ emplate-columns: 80px 120px 2fr 100px 140px; │
│ gap: 20px; padding: 15px 20px; background-c │
│ olor: rgba(255, 255, 255, 0.03); border-bott │
│ om: 1px solid #4a4a4a; font-weight: 600; col │
│ or: var(--text-secondary); font-size: 0.9em; │
│ text-transform: uppercase; letter-spacing: │
│ 0.5px;"> │
│ - <div>Global Rank</div> │
│ - <div style="text-align: right;" │
│ >Time</div> │
│ - <div>Team Composition</div> │
│ - <div style="text-align: center; │
│ ">Dungeon</div> │
│ - <div style="text-align: center; │
│ ">Date</div> │
│ - </div> │
│ - ${dungeonEntries} │
│ - </div> │
│ - </div> │
│ - `; │
│ - } │
│ - │
│ - function createTeamDetailsContent(team) { │
│ - // Create member lookup for this team │
│ - const memberLookup = createMemberLookup │
│ (team); │
│ - │
│ - // Create core members display with Cha │
│ llenge Mode styling │
│ - const coreMembers = team.core_members │
│ - .map((member) => { │
│ - const spec = member.spec_id │
│ - ? window.WoW.getSpecInfo(member.s │
│ pec_id) │
│ - : null; │
│ - const iconUrl = spec │
│ - ? window.WoW.getSpecIcon(spec.cla │
│ ss, spec.spec) │
│ - : null; │
│ - const classColor = spec │
│ - ? window.WoW.getClassColor(spec.c │
│ lass) │
│ - : "#FFFFFF"; │
│ - │
│ - const iconHtml = iconUrl │
│ - ? `<img src="${iconUrl}" alt="${s │
│ pec.spec} ${spec.class}" style="width: 16px; │
│ height: 16px; border-radius: 2px; margin-ri │
│ ght: 4px; vertical-align: middle; flex-shrin │
│ k: 0;">` │
│ - : ""; │
│ - │
│ - return `<span style="display: inlin │
│ e-flex; align-items: center; margin-right: 1 │
│ 5px; gap: 4px;"> │
│ - ${iconHtml} │
│ - <span style="color: ${classColor}; │
│ font-weight: 600; font-size: 0.9em;">${membe │
│ r.name}@${member.realm_slug}</span> │
│ - </span>`; │
│ - }) │
│ - .join(""); │
│ - │
│ - // Create dungeon run entries styled li │
│ ke Challenge Mode leaderboard rows │
│ - const dungeonEntries = Object.entries(t │
│ eam.best_runs_per_dungeon || {}) │
│ - .map(([_dungeonSlug, run]) => { │
│ - const teamComposition = createTeamC │
│ omposition( │
│ - run.members, │
│ - memberLookup, │
│ - ); │
│ - │
│ - return ` │
│ - <div style="display: grid; grid-tem │
│ plate-columns: 80px 120px 2fr 100px 140px; g │
│ ap: 20px; align-items: center; padding: 15px │
│ 20px; border-bottom: 1px solid #3a3a3a; min │
│ -height: 60px;"> │
│ - <div style="font-size: 1.2em; fon │
│ t-weight: bold; color: #d8a657; text-align: │
│ center;">#${run.ranking}</div> │
│ - <div style="font-family: 'Courier │
│ New', monospace; font-weight: bold; color: │
│ #ffffff; text-align: right;">${formatDuratio │
│ n(run.duration)}</div> │
│ - <div style="display: flex; flex-w │
│ rap: wrap; gap: 6px; justify-content: flex-s │
│ tart; align-items: center;">${teamCompositio │
│ n}</div> │
│ - <div style="color: var(--text-pri │
│ mary); text-align: center; font-weight: 600; │
│ ">${run.dungeon_name}</div> │
│ - <div style="color: #aaaaaa; font- │
│ size: 0.9em; text-align: center;">${formatTi │
│ mestamp(run.completed_timestamp)}</div> │
│ - </div> │
│ - `; │
│ - }) │
│ - .join(""); │
│ - │
│ - return ` │
│ - <div style="margin-bottom: 20px;"> │
│ - <h4 style="color: var(--highlight-c │
│ olor); margin-bottom: 10px;">Core Team (3-pl │
│ ayer consistent core)</h4> │
│ - <div style="display: flex; flex-wra │
│ p: wrap; align-items: center;">${coreMembers │
│ }</div> │
│ - </div> │
│ - │
│ - <div> │
│ - <h4 style="color: var(--highlight-c │
│ olor); margin-bottom: 10px;">Best Times Per │
│ Dungeon</h4> │
│ - <div style="border-radius: 6px; ove │
│ rflow: hidden; border: 1px solid #4a4a4a;"> │
│ - <div style="display: grid; grid-t │
│ emplate-columns: 80px 120px 2fr 100px 140px; │
│ gap: 20px; padding: 15px 20px; background-c │
│ olor: rgba(255, 255, 255, 0.03); border-bott │
│ om: 1px solid #4a4a4a; font-weight: 600; col │
│ or: var(--text-secondary); font-size: 0.9em; │
│ text-transform: uppercase; letter-spacing: │
│ 0.5px;"> │
│ - <div>Rank</div> │
│ - <div style="text-align: right;" │
│ >Time</div> │
│ - <div>Team Composition</div> │
│ - <div style="text-align: center; │
│ ">Dungeon</div> │
│ - <div style="text-align: center; │
│ ">Date</div> │
│ - </div> │
│ - ${dungeonEntries} │
│ - </div> │
│ - </div> │
│ - `; │
│ - } │
│ - │
│ - function displayLeaderboard(data, mode) { │
│ - console.log(`Displaying ${mode} leaderb │
│ oard data:`, data); │
│ - │
│ - if (!data.leaderboard || data.leaderboa │
│ rd.length === 0) { │
│ - showNoData(); │
│ - return; │
│ - } │
│ - │
│ - document.getElementById("leaderboard-ti │
│ tle").textContent = data.title; │
│ - document.getElementById("generated-time │
│ ").textContent = │
│ - data.generated_timestamp │
│ - ? formatTimestamp(data.generated_ti │
│ mestamp) │
│ - : "Unknown"; │
│ - │
│ - const rowsContainer = document.getEleme │
│ ntById("team-rows"); │
│ - rowsContainer.innerHTML = ""; │
│ - │
│ - data.leaderboard.forEach((item, index) │
│ => { │
│ - // Add ranking based on position in s │
│ orted array │
│ - item.ranking = index + 1; │
│ - │
│ - const wrapper = │
│ - mode === "players" ? createPlayerRo │
│ w(item) : createTeamRow(item); │
│ - rowsContainer.appendChild(wrapper); │
│ - }); │
│ - │
│ - document.getElementById("loading").clas │
│ sList.add("hidden"); │
│ - document.getElementById("error").classL │
│ ist.add("hidden"); │
│ - document.getElementById("no-data").clas │
│ sList.add("hidden"); │
│ - document.getElementById("content").clas │
│ sList.remove("content-loading"); │
│ - document.getElementById("content").clas │
│ sList.remove("hidden"); │
│ - │
│ - console.log(`${mode} leaderboard displa │
│ yed successfully`); │
│ - } │
│ - │
│ - function showNoData() { │
│ - document.getElementById("loading").clas │
│ sList.add("hidden"); │
│ - document.getElementById("error").classL │
│ ist.add("hidden"); │
│ - document.getElementById("no-data").clas │
│ sList.remove("hidden"); │
│ - document.getElementById("content").clas │
│ sList.add("hidden"); │
│ - } │
│ - │
│ - function showError(message) { │
│ - const errorDiv = document.getElementByI │
│ d("error"); │
│ - const loadingMessage = errorDiv.querySe │
│ lector(".loading-message"); │
│ - if (loadingMessage) { │
│ - loadingMessage.textContent = message; │
│ - } │
│ - errorDiv.classList.remove("hidden"); │
│ - document.getElementById("loading").clas │
│ sList.add("hidden"); │
│ - document.getElementById("no-data").clas │
│ sList.add("hidden"); │
│ - document.getElementById("content").clas │
│ sList.add("hidden"); │
│ - } │
│ - │
│ - function showLoading() { │
│ - document.getElementById("loading").clas │
│ sList.remove("hidden"); │
│ - document.getElementById("error").classL │
│ ist.add("hidden"); │
│ - document.getElementById("no-data").clas │
│ sList.add("hidden"); │
│ - document.getElementById("content").clas │
│ sList.add("content-loading"); │
│ - } │
│ - │
│ - async function loadLeaderboard() { │
│ - const mode = document.getElementById("l │
│ eaderboard-mode").value; │
│ - │
│ - showLoading(); │
│ - │
│ - try { │
│ - const dataPath = │
│ - mode === "players" ? "player-leader │
│ boards" : "team-leaderboards"; │
│ - const fileName = "best-overall.json"; │
│ - const fullUrl = `/data/${dataPath}/${ │
│ fileName}`; │
│ - console.log(`Loading ${mode} leaderbo │
│ ard from:`, fullUrl); │
│ - │
│ - const response = await fetch(fullUrl) │
│ ; │
│ - │
│ - if (!response.ok) { │
│ - throw new Error( │
│ - `Failed to load ${mode} leaderboa │
│ rd data: ` + response.statusText, │
│ - ); │
│ - } │
│ - │
│ - const data = await response.json(); │
│ - console.log(`${mode} leaderboard data │
│ loaded:`, data); │
│ - displayLeaderboard(data, mode); │
│ - │
│ - // Update description based on mode │
│ - updateDescription(mode); │
│ - } catch (error) { │
│ - console.error(`Error loading ${mode} │
│ leaderboard:`, error); │
│ - showError(`Error loading ${mode} lead │
│ erboard data: ` + error.message); │
│ - } │
│ - } │
│ - │
│ - function updateDescription(mode) { │
│ - const description = document.getElement │
│ ById("leaderboard-description"); │
│ - if (mode === "players") { │
│ - description.innerHTML = │
│ - "<strong>Player Leaderboards</stron │
│ g> identify individual players who have comp │
│ leted ALL 9 dungeons. Rankings are based on │
│ the sum of their best times across all dunge │
│ ons."; │
│ - } else { │
│ - description.innerHTML = │
│ - "<strong>Team Leaderboards</strong> │
│ identify 3-player cores that have completed │
│ ALL 9 dungeons. Each team shows their core │
│ members plus extended roster. Rankings are b │
│ ased on the sum of their best times across a │
│ ll dungeons."; │
│ - } │
│ - } │
│ - │
│ - function updateURL() { │
│ - const mode = document.getElementById("l │
│ eaderboard-mode").value; │
│ - const newURL = │
│ - mode === "players" ? `/player-leaderb │
│ oards` : `/team-leaderboards`; │
│ - window.history.pushState({}, "", newURL │
│ ); │
│ - } │
│ - │
│ - // Event listeners │
│ - document.addEventListener("DOMContentLoad │
│ ed", () => { │
│ - // Set initial value if provided │
│ - if (initialMode) { │
│ - document.getElementById("leaderboard- │
│ mode").value = initialMode; │
│ - loadLeaderboard(); │
│ - } │
│ - │
│ - // Bind change event │
│ - document │
│ - .getElementById("leaderboard-mode") │
│ - .addEventListener("change", () => { │
│ - console.log( │
│ - "Leaderboard mode changed to:", │
│ - document.getElementById("leaderbo │
│ ard-mode").value, │
│ - ); │
│ - updateURL(); │
│ - loadLeaderboard(); │
│ - }); │
│ - }); │
│ -</script> │
└──────────────────────────────────────────────┘