┌─ nix/apps/challenge-mode-parser.nix ───────────────────────────────────────┐
│ diff --git a/nix/apps/challenge-mode-parser.nix b/nix/apps/challenge-mode-parser.n │
│ ix │
│ new file mode 100644 │
│ index 0000000..a17b0a1 │
│ --- /dev/null │
│ +++ b/nix/apps/challenge-mode-parser.nix │
│ @@ -0,0 +1,173 @@ │
│ +{ │
│ + writers, │
│ + python3Packages, │
│ + ... │
│ +}: let │
│ + parserScript = │
│ + writers.writePython3Bin "cm-leaderboard-parser" { │
│ + libraries = [python3Packages.requests]; │
│ + doCheck = false; │
│ + } │
│ + '' │
│ + import os │
│ + import json │
│ + import glob │
│ + from pathlib import Path │
│ + import sys │
│ + │
│ + # The root directory where the fetcher script saves its data. │
│ + INPUT_ROOT = "./web/public/data/challenge-mode" │
│ + # A new directory to store the processed and ranked leaderboards. │
│ + OUTPUT_ROOT = "./web/public/data/leaderboards" │
│ + # The number of top runs to collect from each realm's file. │
│ + TOP_N_PER_REALM = 50 │
│ + │
│ + def parse_and_aggregate_data(): │
│ + # finds all fetched leaderboard files, parses them, and aggregates the │
│ data │
│ + # by dungeon and region │
│ + print("Starting data aggregation...") │
│ + dungeon_data = {} │
│ + │
│ + search_path = os.path.join(INPUT_ROOT, "**", "*.json") │
│ + leaderboard_files = glob.glob(search_path, recursive=True) │
│ + │
│ + if not leaderboard_files: │
│ + print(f"FATAL: No leaderboard JSON files found in {os.path.abspath( │
│ INPUT_ROOT)}", file=sys.stderr) │
│ + print("Please run the fetcher script first.", file=sys.stderr) │
│ + sys.exit(1) │
│ + │
│ + print(f"Found {len(leaderboard_files)} leaderboard files to process.") │
│ + │
│ + for file_path in leaderboard_files: │
│ + path = Path(file_path) │
│ + parts = path.parts │
│ + try: │
│ + region = parts[-4] │
│ + realm_slug = parts[-3] │
│ + dungeon_slug = parts[-2] │
│ + except IndexError: │
│ + print(f"Warning: Could not parse path structure for {file_path} │
│ . Skipping.") │
│ + continue │
│ + │
│ + try: │
│ + with open(file_path, 'r', encoding='utf-8') as f: │
│ + data = json.load(f) │
│ + except (json.JSONDecodeError, IOError) as e: │
│ + print(f"Warning: Could not read or parse {file_path}. Skipping. │
│ Error: {e}") │
│ + continue │
│ + │
│ + if dungeon_slug not in dungeon_data: │
│ + dungeon_data[dungeon_slug] = { │
│ + "dungeon_name": data.get("map", {}).get("name", {}).get("en │
│ _US", dungeon_slug), │
│ + "runs": {"us": [], "eu": [], "kr": []} │
│ + } │
│ + │
│ + # slice the list to only get the top N runs from this file. │
│ + top_runs = data.get("leading_groups", [])[:TOP_N_PER_REALM] │
│ + │
│ + for group in top_runs: │
│ + group["realm_slug"] = realm_slug │
│ + group["region"] = region │
│ + dungeon_data[dungeon_slug]["runs"][region].append(group) │
│ + │
│ + return dungeon_data │
│ + │
│ + │
│ + def deduplicate_runs(runs): │
│ + # Remove duplicate runs caused by cross-realm groups appearing on multi │
│ ple realm leaderboards │
│ + seen = set() │
│ + deduplicated = [] │
│ + │
│ + for run in runs: │
│ + # Create a unique identifier for each run using timestamp, duration │
│ , and sorted player IDs │
│ + player_ids = sorted([member["profile"]["id"] for member in run["mem │
│ bers"]]) │
│ + unique_key = (run["completed_timestamp"], run["duration"], tuple(pl │
│ ayer_ids)) │
│ + │
│ + if unique_key not in seen: │
│ + seen.add(unique_key) │
│ + deduplicated.append(run) │
│ + else: │
│ + print(f" Removing duplicate run: {run['duration']}ms at {run │
│ ['completed_timestamp']}") │
│ + │
│ + return deduplicated │
│ + │
│ + def rank_and_save_leaderboards(dungeon_data): │
│ + # sorts the aggregated data to create regional and global leaderboards, │
│ + # then saves them to new JSON files │
│ + if not dungeon_data: │
│ + print("No data was aggregated. Exiting.") │
│ + return │
│ + │
│ + print("\nRanking leaderboards and generating output files...") │
│ + os.makedirs(OUTPUT_ROOT, exist_ok=True) │
│ + sort_key = lambda run: run["duration"] │
│ + │
│ + for dungeon_slug, data in dungeon_data.items(): │
│ + print(f" Processing dungeon: {data['dungeon_name']}") │
│ + all_regional_runs = [] │
│ + │
│ + # 1. Generate and save REGIONAL leaderboards │
│ + regional_path = os.path.join(OUTPUT_ROOT, "regional") │
│ + for region, runs in data["runs"].items(): │
│ + if not runs: │
│ + continue │
│ + │
│ + # Deduplicate and sort regional runs │
│ + print(f" Deduplicating {region.upper()} runs ({len(runs)} -> │
│ ", end="") │
│ + deduplicated_runs = deduplicate_runs(runs) │
│ + print(f"{len(deduplicated_runs)})") │
│ + deduplicated_runs.sort(key=sort_key) │
│ + │
│ + # Re-rank runs for regional leaderboard │
│ + for i, run in enumerate(deduplicated_runs): │
│ + run["ranking"] = i + 1 │
│ + │
│ + all_regional_runs.extend(deduplicated_runs) │
│ + output_dir = os.path.join(regional_path, region, dungeon_slug) │
│ + os.makedirs(output_dir, exist_ok=True) │
│ + output_file = os.path.join(output_dir, "leaderboard.json") │
│ + │
│ + with open(output_file, 'w', encoding='utf-8') as f: │
│ + json.dump({ │
│ + "dungeon_name": data["dungeon_name"], │
│ + "dungeon_slug": dungeon_slug, │
│ + "region": region, │
│ + "leaderboard": deduplicated_runs, │
│ + }, f, indent=2) │
│ + │
│ + # 2. Generate and save GLOBAL leaderboard │
│ + if not all_regional_runs: │
│ + continue │
│ + │
│ + # Deduplicate global runs (cross-region duplicates) │
│ + print(f" Deduplicating global runs ({len(all_regional_runs)} -> │
│ ", end="") │
│ + deduplicated_global = deduplicate_runs(all_regional_runs) │
│ + print(f"{len(deduplicated_global)})") │
│ + deduplicated_global.sort(key=sort_key) │
│ + │
│ + # Re-rank runs for global leaderboard │
│ + for i, run in enumerate(deduplicated_global): │
│ + run["ranking"] = i + 1 │
│ + │
│ + global_path = os.path.join(OUTPUT_ROOT, "global", dungeon_slug) │
│ + os.makedirs(global_path, exist_ok=True) │
│ + global_output_file = os.path.join(global_path, "leaderboard.json") │
│ + │
│ + with open(global_output_file, 'w', encoding='utf-8') as f: │
│ + json.dump({ │
│ + "dungeon_name": data["dungeon_name"], │
│ + "dungeon_slug": dungeon_slug, │
│ + "leaderboard": deduplicated_global, │
│ + }, f, indent=2) │
│ + │
│ + def main(): │
│ + aggregated_data = parse_and_aggregate_data() │
│ + rank_and_save_leaderboards(aggregated_data) │
│ + print(f"\nDone. Ranked leaderboards are available in: {os.path.abspath( │
│ OUTPUT_ROOT)}") │
│ + │
│ + │
│ + if __name__ == "__main__": │
│ + main() │
│ + ''; │
│ +in │
│ + parserScript │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...pps/challenge-mode-parser.nix ───┐
│ diff --git a/nix/apps/challenge-mode-parser. │
│ nix b/nix/apps/challenge-mode-parser.nix │
│ new file mode 100644 │
│ index 0000000..a17b0a1 │
│ --- /dev/null │
│ +++ b/nix/apps/challenge-mode-parser.nix │
│ @@ -0,0 +1,173 @@ │
│ +{ │
│ + writers, │
│ + python3Packages, │
│ + ... │
│ +}: let │
│ + parserScript = │
│ + writers.writePython3Bin "cm-leaderboard │
│ -parser" { │
│ + libraries = [python3Packages.requests │
│ ]; │
│ + doCheck = false; │
│ + } │
│ + '' │
│ + import os │
│ + import json │
│ + import glob │
│ + from pathlib import Path │
│ + import sys │
│ + │
│ + # The root directory where the fetche │
│ r script saves its data. │
│ + INPUT_ROOT = "./web/public/data/chall │
│ enge-mode" │
│ + # A new directory to store the proces │
│ sed and ranked leaderboards. │
│ + OUTPUT_ROOT = "./web/public/data/lead │
│ erboards" │
│ + # The number of top runs to collect f │
│ rom each realm's file. │
│ + TOP_N_PER_REALM = 50 │
│ + │
│ + def parse_and_aggregate_data(): │
│ + # finds all fetched leaderboard f │
│ iles, parses them, and aggregates the data │
│ + # by dungeon and region │
│ + print("Starting data aggregation. │
│ ..") │
│ + dungeon_data = {} │
│ + │
│ + search_path = os.path.join(INPUT_ │
│ ROOT, "**", "*.json") │
│ + leaderboard_files = glob.glob(sea │
│ rch_path, recursive=True) │
│ + │
│ + if not leaderboard_files: │
│ + print(f"FATAL: No leaderboard │
│ JSON files found in {os.path.abspath(INPUT_ │
│ ROOT)}", file=sys.stderr) │
│ + print("Please run the fetcher │
│ script first.", file=sys.stderr) │
│ + sys.exit(1) │
│ + │
│ + print(f"Found {len(leaderboard_fi │
│ les)} leaderboard files to process.") │
│ + │
│ + for file_path in leaderboard_file │
│ s: │
│ + path = Path(file_path) │
│ + parts = path.parts │
│ + try: │
│ + region = parts[-4] │
│ + realm_slug = parts[-3] │
│ + dungeon_slug = parts[-2] │
│ + except IndexError: │
│ + print(f"Warning: Could no │
│ t parse path structure for {file_path}. Skip │
│ ping.") │
│ + continue │
│ + │
│ + try: │
│ + with open(file_path, 'r', │
│ encoding='utf-8') as f: │
│ + data = json.load(f) │
│ + except (json.JSONDecodeError, │
│ IOError) as e: │
│ + print(f"Warning: Could no │
│ t read or parse {file_path}. Skipping. Error │
│ : {e}") │
│ + continue │
│ + │
│ + if dungeon_slug not in dungeo │
│ n_data: │
│ + dungeon_data[dungeon_slug │
│ ] = { │
│ + "dungeon_name": data. │
│ get("map", {}).get("name", {}).get("en_US", │
│ dungeon_slug), │
│ + "runs": {"us": [], "e │
│ u": [], "kr": []} │
│ + } │
│ + │
│ + # slice the list to only get │
│ the top N runs from this file. │
│ + top_runs = data.get("leading_ │
│ groups", [])[:TOP_N_PER_REALM] │
│ + │
│ + for group in top_runs: │
│ + group["realm_slug"] = rea │
│ lm_slug │
│ + group["region"] = region │
│ + dungeon_data[dungeon_slug │
│ ]["runs"][region].append(group) │
│ + │
│ + return dungeon_data │
│ + │
│ + │
│ + def deduplicate_runs(runs): │
│ + # Remove duplicate runs caused by │
│ cross-realm groups appearing on multiple re │
│ alm leaderboards │
│ + seen = set() │
│ + deduplicated = [] │
│ + │
│ + for run in runs: │
│ + # Create a unique identifier │
│ for each run using timestamp, duration, and │
│ sorted player IDs │
│ + player_ids = sorted([member[" │
│ profile"]["id"] for member in run["members"] │
│ ]) │
│ + unique_key = (run["completed_ │
│ timestamp"], run["duration"], tuple(player_i │
│ ds)) │
│ + │
│ + if unique_key not in seen: │
│ + seen.add(unique_key) │
│ + deduplicated.append(run) │
│ + else: │
│ + print(f" Removing dupl │
│ icate run: {run['duration']}ms at {run['comp │
│ leted_timestamp']}") │
│ + │
│ + return deduplicated │
│ + │
│ + def rank_and_save_leaderboards(dungeo │
│ n_data): │
│ + # sorts the aggregated data to cr │
│ eate regional and global leaderboards, │
│ + # then saves them to new JSON fil │
│ es │
│ + if not dungeon_data: │
│ + print("No data was aggregated │
│ . Exiting.") │
│ + return │
│ + │
│ + print("\nRanking leaderboards and │
│ generating output files...") │
│ + os.makedirs(OUTPUT_ROOT, exist_ok │
│ =True) │
│ + sort_key = lambda run: run["durat │
│ ion"] │
│ + │
│ + for dungeon_slug, data in dungeon │
│ _data.items(): │
│ + print(f" Processing dungeon: │
│ {data['dungeon_name']}") │
│ + all_regional_runs = [] │
│ + │
│ + # 1. Generate and save REGION │
│ AL leaderboards │
│ + regional_path = os.path.join( │
│ OUTPUT_ROOT, "regional") │
│ + for region, runs in data["run │
│ s"].items(): │
│ + if not runs: │
│ + continue │
│ + │
│ + # Deduplicate and sort re │
│ gional runs │
│ + print(f" Deduplicating │
│ {region.upper()} runs ({len(runs)} -> ", en │
│ d="") │
│ + deduplicated_runs = dedup │
│ licate_runs(runs) │
│ + print(f"{len(deduplicated │
│ _runs)})") │
│ + deduplicated_runs.sort(ke │
│ y=sort_key) │
│ + │
│ + # Re-rank runs for region │
│ al leaderboard │
│ + for i, run in enumerate(d │
│ eduplicated_runs): │
│ + run["ranking"] = i + │
│ 1 │
│ + │
│ + all_regional_runs.extend( │
│ deduplicated_runs) │
│ + output_dir = os.path.join │
│ (regional_path, region, dungeon_slug) │
│ + os.makedirs(output_dir, e │
│ xist_ok=True) │
│ + output_file = os.path.joi │
│ n(output_dir, "leaderboard.json") │
│ + │
│ + with open(output_file, 'w │
│ ', encoding='utf-8') as f: │
│ + json.dump({ │
│ + "dungeon_name": d │
│ ata["dungeon_name"], │
│ + "dungeon_slug": d │
│ ungeon_slug, │
│ + "region": region, │
│ + "leaderboard": de │
│ duplicated_runs, │
│ + }, f, indent=2) │
│ + │
│ + # 2. Generate and save GLOBAL │
│ leaderboard │
│ + if not all_regional_runs: │
│ + continue │
│ + │
│ + # Deduplicate global runs (cr │
│ oss-region duplicates) │
│ + print(f" Deduplicating glo │
│ bal runs ({len(all_regional_runs)} -> ", end │
│ ="") │
│ + deduplicated_global = dedupli │
│ cate_runs(all_regional_runs) │
│ + print(f"{len(deduplicated_glo │
│ bal)})") │
│ + deduplicated_global.sort(key= │
│ sort_key) │
│ + │
│ + # Re-rank runs for global lea │
│ derboard │
│ + for i, run in enumerate(dedup │
│ licated_global): │
│ + run["ranking"] = i + 1 │
│ + │
│ + global_path = os.path.join(OU │
│ TPUT_ROOT, "global", dungeon_slug) │
│ + os.makedirs(global_path, exis │
│ t_ok=True) │
│ + global_output_file = os.path. │
│ join(global_path, "leaderboard.json") │
│ + │
│ + with open(global_output_file, │
│ 'w', encoding='utf-8') as f: │
│ + json.dump({ │
│ + "dungeon_name": data[ │
│ "dungeon_name"], │
│ + "dungeon_slug": dunge │
│ on_slug, │
│ + "leaderboard": dedupl │
│ icated_global, │
│ + }, f, indent=2) │
│ + │
│ + def main(): │
│ + aggregated_data = parse_and_aggre │
│ gate_data() │
│ + rank_and_save_leaderboards(aggreg │
│ ated_data) │
│ + print(f"\nDone. Ranked leaderboar │
│ ds are available in: {os.path.abspath(OUTPUT │
│ _ROOT)}") │
│ + │
│ + │
│ + if __name__ == "__main__": │
│ + main() │
│ + ''; │
│ +in │
│ + parserScript │
└──────────────────────────────────────────────┘
┌─ web/src/pages/challenge-mode/index.astro ─────────────────────────────────┐
│ diff --git a/web/src/pages/challenge-mode/index.astro b/web/src/pages/challenge-mo │
│ de/index.astro │
│ index b3116dd..75d129c 100644 │
│ --- a/web/src/pages/challenge-mode/index.astro │
│ +++ b/web/src/pages/challenge-mode/index.astro │
│ @@ -8,9 +8,9 @@ import WoWClassColors from "../../components/WoWClassColors.astro" │
│ ; │
│ const baseUrl = import.meta.env.BASE_URL; │
│ │
│ // No default selections for the base challenge-mode page │
│ -const initialRegion = ''; │
│ -const initialRealm = ''; │
│ -const initialDungeon = ''; │
│ +const initialRegion = ""; │
│ +const initialRealm = ""; │
│ +const initialDungeon = ""; │
│ --- │
│ │
│ <PageLayout │
│ @@ -235,9 +235,14 @@ const initialDungeon = ''; │
│ { value: "us", label: "US" }, │
│ { value: "eu", label: "EU" }, │
│ { value: "kr", label: "KR" }, │
│ + { value: "global", label: "Global" }, │
│ ]} │
│ /> │
│ - <FormSelect id="realm" label="Realm" options={[{ value: "", label: "Select Re │
│ gion First", selected: true }]} /> │
│ + <FormSelect │
│ + id="realm" │
│ + label="Realm" │
│ + options={[{ value: "", label: "Select Region First", selected: true }]} │
│ + /> │
│ <FormSelect │
│ id="dungeon" │
│ label="Dungeon" │
│ @@ -300,6 +305,13 @@ const initialDungeon = ''; │
│ <span class="info-value" id="period-end">-</span> │
│ </div> │
│ </div> │
│ + │
│ + <div id="cross-realm-note" style="display: none; margin-top: 15px; padding: │
│ 10px; background-color: rgba(255, 107, 107, 0.1); border-left: 3px solid #ff6b6b; │
│ border-radius: 4px;"> │
│ + <p style="margin: 0; font-size: 0.85em; color: var(--text-secondary);"> │
│ + <span style="color: #ff6b6b; font-weight: bold;">*</span> indicates pla │
│ yers from other realms. │
│ + Times with all local players represent the true realm record for Feat o │
│ f Strength achievement. │
│ + </p> │
│ + </div> │
│ </div> │
│ │
│ <div class="leaderboard-container"> │
│ @@ -383,7 +395,10 @@ const initialDungeon = ''; │
│ } │
│ </style> │
│ │
│ -<script is:inline define:vars={{ baseUrl, initialRegion, initialRealm, initialDun │
│ geon }}> │
│ +<script │
│ + is:inline │
│ + define:vars={{ baseUrl, initialRegion, initialRealm, initialDungeon }} │
│ +> │
│ const DATA_MAP = { │
│ us: { │
│ name: "US", │
│ @@ -489,6 +504,11 @@ const initialDungeon = ''; │
│ const row = document.createElement("div"); │
│ row.className = "leaderboard-row"; │
│ │
│ + // Get current realm selection to check for cross-realm players │
│ + const currentRegion = document.getElementById("region").value; │
│ + const currentRealm = document.getElementById("realm").value; │
│ + const isIndividualRealmView = currentRegion && currentRealm && currentRealm ! │
│ == "all" && currentRegion !== "global"; │
│ + │
│ let teamHtml = ""; │
│ group.members.forEach((member) => { │
│ // Handle missing specialization data (deleted/transferred characters) │
│ @@ -502,22 +522,49 @@ const initialDungeon = ''; │
│ }; │
│ const iconUrl = window.WoW.getSpecIcon(spec.class, spec.spec); │
│ 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;">' │
│ + ? '<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;">' │
│ : ""; │
│ │
│ - teamHtml += '<span style="display: inline-flex; align-items: center; margin │
│ -right: 8px; gap: 4px;">' + │
│ + // Check if player is from a different realm (for individual realm views on │
│ ly) │
│ + let playerName = member.profile.name; │
│ + if (isIndividualRealmView && member.profile.realm?.slug && member.profile.r │
│ ealm.slug !== currentRealm) { │
│ + playerName += '<span style="color: #ff6b6b; font-weight: bold; margin-lef │
│ t: 2px;">*</span>'; │
│ + } │
│ + │
│ + teamHtml += │
│ + '<span style="display: inline-flex; align-items: center; margin-right: 8p │
│ x; gap: 4px;">' + │
│ iconHtml + │
│ - '<span style="color: ' + window.WoW.getClassColor(spec.class) + '; font-w │
│ eight: 600; font-size: 0.9em;">' + member.profile.name + '</span>' + │
│ - '</span>'; │
│ + '<span style="color: ' + │
│ + window.WoW.getClassColor(spec.class) + │
│ + '; font-weight: 600; font-size: 0.9em;">' + │
│ + playerName + │
│ + "</span>" + │
│ + "</span>"; │
│ }); │
│ │
│ - row.innerHTML = '<div style="display: table-cell; padding: 15px 10px; vertica │
│ l-align: middle; font-size: 1.2em; font-weight: bold; color: #d8a657; text-align: │
│ center; width: 80px;">#' + group.ranking + '</div>' + │
│ - '<div style="display: table-cell; padding: 15px 10px; vertical-align: m │
│ iddle; font-family: \'Courier New\', monospace; font-weight: bold; color: #ffffff; │
│ text-align: right; width: 120px;">' + formatDuration(group.duration) + '</div>' + │
│ - '<div style="display: table-cell; padding: 15px 10px; vertical-align: m │
│ iddle;">' + teamHtml + '</div>' + │
│ - '<div style="display: table-cell; padding: 15px 10px; vertical-align: m │
│ iddle; color: #aaaaaa; font-size: 0.9em; text-align: center; width: 140px;">' + fo │
│ rmatTimestamp(group.completed_timestamp) + '</div>'; │
│ + row.innerHTML = │
│ + '<div style="display: table-cell; padding: 15px 10px; vertical-align: middl │
│ e; font-size: 1.2em; font-weight: bold; color: #d8a657; text-align: center; width: │
│ 80px;">#' + │
│ + group.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(group.duration) + │
│ + "</div>" + │
│ + '<div style="display: table-cell; padding: 15px 10px; vertical-align: middl │
│ e;">' + │
│ + teamHtml + │
│ + "</div>" + │
│ + '<div style="display: table-cell; padding: 15px 10px; vertical-align: middl │
│ e; color: #aaaaaa; font-size: 0.9em; text-align: center; width: 140px;">' + │
│ + formatTimestamp(group.completed_timestamp) + │
│ + "</div>"; │
│ │
│ // Apply table-row styles │
│ - row.style.cssText = 'display: table-row !important; border-bottom: 1px solid │
│ #4a4a4a !important; background-color: #32302f; transition: background-color 0.2s e │
│ ase;'; │
│ + row.style.cssText = │
│ + "display: table-row !important; border-bottom: 1px solid #4a4a4a !important │
│ ; background-color: #32302f; transition: background-color 0.2s ease;"; │
│ │
│ // Add hover effect │
│ row.addEventListener("mouseenter", () => { │
│ @@ -534,37 +581,66 @@ const initialDungeon = ''; │
│ function displayLeaderboard(data) { │
│ console.log("Displaying leaderboard data:", data); │
│ │
│ - // Check if we have no data (missing or empty leading_groups) │
│ - if (!data.leading_groups || data.leading_groups.length === 0) { │
│ + // Handle both old format (individual realm) and new format (aggregated) │
│ + const leaderboardData = data.leaderboard || data.leading_groups; │
│ + │
│ + // Check if we have no data │
│ + if (!leaderboardData || leaderboardData.length === 0) { │
│ showNoData(); │
│ return; │
│ } │
│ │
│ - // Handle localized dungeon names │
│ - const dungeonName = │
│ - typeof data.map.name === "object" │
│ + // Handle dungeon name from both formats │
│ + let dungeonName; │
│ + if (data.dungeon_name) { │
│ + // New aggregated format │
│ + dungeonName = data.dungeon_name; │
│ + } else if (data.map) { │
│ + // Old individual realm format │
│ + dungeonName = typeof data.map.name === "object" │
│ ? data.map.name.en_US || data.map.name │
│ : data.map.name; │
│ - document.getElementById("dungeon-name").textContent = dungeonName; │
│ - document.getElementById("period").textContent = data.period; │
│ - document.getElementById("period-start").textContent = formatTimestamp( │
│ - data.period_start_timestamp, │
│ - ); │
│ - document.getElementById("period-end").textContent = formatTimestamp( │
│ - data.period_end_timestamp, │
│ - ); │
│ + } │
│ + document.getElementById("dungeon-name").textContent = dungeonName || "Unknown │
│ Dungeon"; │
│ + │
│ + // Handle period info (only available in individual realm format) │
│ + if (data.period) { │
│ + document.getElementById("period").textContent = data.period; │
│ + document.getElementById("period-start").textContent = formatTimestamp(data. │
│ period_start_timestamp); │
│ + document.getElementById("period-end").textContent = formatTimestamp(data.pe │
│ riod_end_timestamp); │
│ + } else { │
│ + document.getElementById("period").textContent = "Multiple Periods"; │
│ + document.getElementById("period-start").textContent = "-"; │
│ + document.getElementById("period-end").textContent = "-"; │
│ + } │
│ │
│ + // Handle realm name display │
│ const currentRegion = document.getElementById("region").value; │
│ const currentRealm = document.getElementById("realm").value; │
│ - const regionData = DATA_MAP[currentRegion]; │
│ - const realmName = regionData?.realms?.[currentRealm] || currentRealm; │
│ - document.getElementById("realm-name").textContent = realmName; │
│ + let realmDisplayName; │
│ + │
│ + if (currentRegion === "global") { │
│ + realmDisplayName = "All Regions"; │
│ + } else if (currentRealm === "all") { │
│ + realmDisplayName = "All " + currentRegion.toUpperCase() + " Realms"; │
│ + } else { │
│ + const regionData = DATA_MAP[currentRegion]; │
│ + realmDisplayName = regionData?.realms?.[currentRealm] || currentRealm; │
│ + } │
│ + document.getElementById("realm-name").textContent = realmDisplayName; │
│ + │
│ + // Show/hide cross-realm indicator explanation │
│ + const crossRealmNote = document.getElementById("cross-realm-note"); │
│ + const isIndividualRealmView = currentRegion && currentRealm && currentRealm ! │
│ == "all" && currentRegion !== "global"; │
│ + if (crossRealmNote) { │
│ + crossRealmNote.style.display = isIndividualRealmView ? "block" : "none"; │
│ + } │
│ │
│ const rowsContainer = document.getElementById("leaderboard-rows"); │
│ rowsContainer.innerHTML = ""; │
│ │
│ - console.log("Creating rows for", data.leading_groups.length, "groups"); │
│ - data.leading_groups.forEach((group, index) => { │
│ + console.log("Creating rows for", leaderboardData.length, "groups"); │
│ + leaderboardData.forEach((group, index) => { │
│ console.log("Creating row for group", index + 1); │
│ const row = createLeaderboardRow(group); │
│ rowsContainer.appendChild(row); │
│ @@ -593,7 +669,10 @@ const initialDungeon = ''; │
│ loadingMessage.textContent = message; │
│ } else { │
│ // Fallback if LoadingState structure isn't found │
│ - errorDiv.innerHTML = '<div class="loading-state error"><p class="loading-me │
│ ssage">' + message + '</p></div>'; │
│ + errorDiv.innerHTML = │
│ + '<div class="loading-state error"><p class="loading-message">' + │
│ + message + │
│ + "</p></div>"; │
│ } │
│ errorDiv.classList.remove("hidden"); │
│ document.getElementById("loading").classList.add("hidden"); │
│ @@ -611,11 +690,9 @@ const initialDungeon = ''; │
│ function updateRealmOptions() { │
│ const region = document.getElementById("region").value; │
│ const realmSelect = document.getElementById("realm"); │
│ - │
│ + │
│ console.log("=== updateRealmOptions START ==="); │
│ console.log("Region:", region); │
│ - console.log("Realm select element:", realmSelect); │
│ - console.log("DATA_MAP keys:", Object.keys(DATA_MAP)); │
│ │
│ // Clear existing options │
│ realmSelect.innerHTML = ""; │
│ @@ -627,47 +704,50 @@ const initialDungeon = ''; │
│ option.textContent = "Select Region First"; │
│ option.selected = true; │
│ realmSelect.appendChild(option); │
│ + realmSelect.disabled = true; │
│ console.log("No region selected, showing placeholder"); │
│ return; │
│ } │
│ │
│ + if (region === "global") { │
│ + // Global selected - disable realm selection and show "N/A" │
│ + const option = document.createElement("option"); │
│ + option.value = "global"; │
│ + option.textContent = "N/A (Global View)"; │
│ + option.selected = true; │
│ + realmSelect.appendChild(option); │
│ + realmSelect.disabled = true; │
│ + console.log("Global region selected, disabled realm options"); │
│ + return; │
│ + } │
│ + │
│ + // Enable realm selection for regional selections │
│ + realmSelect.disabled = false; │
│ + │
│ + // Add "All Realms" option for regional view │
│ + const allRealmsOption = document.createElement("option"); │
│ + allRealmsOption.value = "all"; │
│ + allRealmsOption.textContent = "All " + region.toUpperCase() + " Realms"; │
│ + realmSelect.appendChild(allRealmsOption); │
│ + │
│ // Add default option │
│ const defaultOption = document.createElement("option"); │
│ defaultOption.value = ""; │
│ defaultOption.textContent = "Select Realm"; │
│ defaultOption.selected = true; │
│ realmSelect.appendChild(defaultOption); │
│ - console.log("Added default option"); │
│ - │
│ - // Test with hardcoded US realms first │
│ - if (region === 'us') { │
│ - console.log("Adding hardcoded US realms for testing"); │
│ - const testRealms = ['angerforge', 'arugal', 'ashkandi']; │
│ - testRealms.forEach(realm => { │
│ - const option = document.createElement("option"); │
│ - option.value = realm; │
│ - option.textContent = realm.charAt(0).toUpperCase() + realm.slice(1); │
│ - realmSelect.appendChild(option); │
│ - }); │
│ - } │
│ │
│ - // Add realms for selected region │
│ + // Add individual realms for selected region │
│ const regionData = DATA_MAP[region]; │
│ - console.log("Region data:", regionData); │
│ if (regionData && regionData.realms) { │
│ - console.log("Adding realms:", Object.keys(regionData.realms)); │
│ Object.entries(regionData.realms).forEach(([slug, name]) => { │
│ const option = document.createElement("option"); │
│ option.value = slug; │
│ option.textContent = name; │
│ realmSelect.appendChild(option); │
│ - console.log("Added realm: " + slug + " -> " + name); │
│ }); │
│ - } else { │
│ - console.log("No realm data found for region:", region); │
│ } │
│ - │
│ - console.log("Final realm select options:", realmSelect.options.length); │
│ + │
│ console.log("=== updateRealmOptions END ==="); │
│ } │
│ │
│ @@ -676,14 +756,27 @@ const initialDungeon = ''; │
│ const realm = document.getElementById("realm").value; │
│ const dungeon = document.getElementById("dungeon").value; │
│ │
│ - console.log('Getting data filename with:', { region, realm, dungeon }); │
│ - console.log('Dungeon name lookup:', dungeonNames[dungeon]); │
│ + console.log("Getting data filename with:", { region, realm, dungeon }); │
│ + console.log("Dungeon name lookup:", dungeonNames[dungeon]); │
│ │
│ - const dungeonName = dungeonNames[dungeon] │
│ + const dungeonSlug = dungeonNames[dungeon] │
│ .toLowerCase() │
│ .replace(/[^a-z0-9]/g, "-"); │
│ - const fileName = region + "/" + realm + "/" + dungeonName + "/" + realm + "-" │
│ + dungeonName + "-leaderboard.json"; │
│ - console.log('Generated filename:', fileName); │
│ + │
│ + let fileName; │
│ + │
│ + if (region === "global") { │
│ + // Global leaderboard path │
│ + fileName = "leaderboards/global/" + dungeonSlug + "/leaderboard.json"; │
│ + } else if (realm === "all") { │
│ + // Regional aggregated leaderboard path │
│ + fileName = "leaderboards/regional/" + region + "/" + dungeonSlug + "/leader │
│ board.json"; │
│ + } else { │
│ + // Individual realm leaderboard path (existing system) │
│ + fileName = "challenge-mode/" + region + "/" + realm + "/" + dungeonSlug + " │
│ /" + realm + "-" + dungeonSlug + "-leaderboard.json"; │
│ + } │
│ + │
│ + console.log("Generated filename:", fileName); │
│ return fileName; │
│ } │
│ │
│ @@ -691,7 +784,13 @@ const initialDungeon = ''; │
│ const region = document.getElementById("region").value; │
│ const realm = document.getElementById("realm").value; │
│ const dungeon = document.getElementById("dungeon").value; │
│ + │
│ + // For global, we only need region and dungeon │
│ + if (region === "global") { │
│ + return region && dungeon; │
│ + } │
│ │
│ + // For regional/individual realms, we need all three │
│ return region && realm && dungeon; │
│ } │
│ │
│ @@ -708,12 +807,14 @@ const initialDungeon = ''; │
│ const fileName = getDataFileName(); │
│ console.log("Loading file:", fileName); │
│ // Use absolute path for GitHub Pages compatibility │
│ - const fullUrl = "/data/challenge-mode/" + fileName; │
│ + const fullUrl = "/data/" + fileName; │
│ console.log("Full fetch URL:", fullUrl); │
│ const response = await fetch(fullUrl); │
│ │
│ if (!response.ok) { │
│ - throw new Error("Failed to load leaderboard data: " + response.statusText │
│ ); │
│ + throw new Error( │
│ + "Failed to load leaderboard data: " + response.statusText, │
│ + ); │
│ } │
│ │
│ const data = await response.json(); │
│ @@ -726,21 +827,22 @@ const initialDungeon = ''; │
│ } │
│ │
│ function updateURL() { │
│ - // Only update URL if all selections are made │
│ - if (!canLoadLeaderboard()) { │
│ - return; │
│ - } │
│ - │
│ + // Only update URL for individual realm selections (not global/regional) │
│ const region = document.getElementById("region").value; │
│ const realm = document.getElementById("realm").value; │
│ const dungeon = document.getElementById("dungeon").value; │
│ │
│ + // Don't update URL for global or "all realms" selections │
│ + if (region === "global" || realm === "all" || !canLoadLeaderboard()) { │
│ + return; │
│ + } │
│ + │
│ const dungeonName = dungeonNames[dungeon] │
│ .toLowerCase() │
│ .replace(/[^a-z0-9]/g, "-"); │
│ - │
│ + │
│ const newURL = "/challenge-mode/" + region + "/" + realm + "/" + dungeonName; │
│ - window.history.pushState({}, '', newURL); │
│ + window.history.pushState({}, "", newURL); │
│ } │
│ │
│ // Event listeners │
│ @@ -750,7 +852,10 @@ const initialDungeon = ''; │
│ │
│ // Bind change events │
│ document.getElementById("region").addEventListener("change", () => { │
│ - console.log("Region changed to:", document.getElementById("region").value); │
│ + console.log( │
│ + "Region changed to:", │
│ + document.getElementById("region").value, │
│ + ); │
│ updateRealmOptions(); │
│ // Reset realm selection when region changes │
│ document.getElementById("realm").value = ""; │
│ @@ -765,9 +870,13 @@ const initialDungeon = ''; │
│ loadLeaderboard(); │
│ }); │
│ document.getElementById("dungeon").addEventListener("change", () => { │
│ - console.log("Dungeon changed to:", document.getElementById("dungeon").value │
│ ); │
│ + console.log( │
│ + "Dungeon changed to:", │
│ + document.getElementById("dungeon").value, │
│ + ); │
│ updateURL(); │
│ loadLeaderboard(); │
│ }); │
│ }); │
│ -</script> │
│ \ No newline at end of file │
│ +</script> │
│ + │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...es/challenge-mode/index.astro ───┐
│ diff --git a/web/src/pages/challenge-mode/in │
│ dex.astro b/web/src/pages/challenge-mode/ind │
│ ex.astro │
│ index b3116dd..75d129c 100644 │
│ --- a/web/src/pages/challenge-mode/index.ast │
│ ro │
│ +++ b/web/src/pages/challenge-mode/index.ast │
│ ro │
│ @@ -8,9 +8,9 @@ import WoWClassColors from " │
│ ../../components/WoWClassColors.astro"; │
│ const baseUrl = import.meta.env.BASE_URL; │
│ │
│ // No default selections for the base chall │
│ enge-mode page │
│ -const initialRegion = ''; │
│ -const initialRealm = ''; │
│ -const initialDungeon = ''; │
│ +const initialRegion = ""; │
│ +const initialRealm = ""; │
│ +const initialDungeon = ""; │
│ --- │
│ │
│ <PageLayout │
│ @@ -235,9 +235,14 @@ const initialDungeon = │
│ ''; │
│ { value: "us", label: "US" }, │
│ { value: "eu", label: "EU" }, │
│ { value: "kr", label: "KR" }, │
│ + { value: "global", label: "Global" │
│ }, │
│ ]} │
│ /> │
│ - <FormSelect id="realm" label="Realm" op │
│ tions={[{ value: "", label: "Select Region F │
│ irst", selected: true }]} /> │
│ + <FormSelect │
│ + id="realm" │
│ + label="Realm" │
│ + options={[{ value: "", label: "Select │
│ Region First", selected: true }]} │
│ + /> │
│ <FormSelect │
│ id="dungeon" │
│ label="Dungeon" │
│ @@ -300,6 +305,13 @@ const initialDungeon = │
│ ''; │
│ <span class="info-value" id="peri │
│ od-end">-</span> │
│ </div> │
│ </div> │
│ + │
│ + <div id="cross-realm-note" style="dis │
│ play: none; margin-top: 15px; padding: 10px; │
│ background-color: rgba(255, 107, 107, 0.1); │
│ border-left: 3px solid #ff6b6b; border-radi │
│ us: 4px;"> │
│ + <p style="margin: 0; font-size: 0.8 │
│ 5em; color: var(--text-secondary);"> │
│ + <span style="color: #ff6b6b; font │
│ -weight: bold;">*</span> indicates players f │
│ rom other realms. │
│ + Times with all local players repr │
│ esent the true realm record for Feat of Stre │
│ ngth achievement. │
│ + </p> │
│ + </div> │
│ </div> │
│ │
│ <div class="leaderboard-container"> │
│ @@ -383,7 +395,10 @@ const initialDungeon = │
│ ''; │
│ } │
│ </style> │
│ │
│ -<script is:inline define:vars={{ baseUrl, i │
│ nitialRegion, initialRealm, initialDungeon } │
│ }> │
│ +<script │
│ + is:inline │
│ + define:vars={{ baseUrl, initialRegion, in │
│ itialRealm, initialDungeon }} │
│ +> │
│ const DATA_MAP = { │
│ us: { │
│ name: "US", │
│ @@ -489,6 +504,11 @@ const initialDungeon = │
│ ''; │
│ const row = document.createElement("div │
│ "); │
│ row.className = "leaderboard-row"; │
│ │
│ + // Get current realm selection to check │
│ for cross-realm players │
│ + const currentRegion = document.getEleme │
│ ntById("region").value; │
│ + const currentRealm = document.getElemen │
│ tById("realm").value; │
│ + const isIndividualRealmView = currentRe │
│ gion && currentRealm && currentRealm !== "al │
│ l" && currentRegion !== "global"; │
│ + │
│ let teamHtml = ""; │
│ group.members.forEach((member) => { │
│ // Handle missing specialization data │
│ (deleted/transferred characters) │
│ @@ -502,22 +522,49 @@ const initialDungeon = │
│ ''; │
│ }; │
│ const iconUrl = window.WoW.getSpecIco │
│ n(spec.class, spec.spec); │
│ const iconHtml = iconUrl │
│ - ? '<img src="' + iconUrl + '" alt=" │
│ ' + spec.spec + ' ' + spec.class + '" style= │
│ "width: 16px; height: 16px; border-radius: 2 │
│ px; margin-right: 4px; vertical-align: middl │
│ e; flex-shrink: 0;">' │
│ + ? '<img src="' + │
│ + iconUrl + │
│ + '" alt="' + │
│ + spec.spec + │
│ + " " + │
│ + spec.class + │
│ + '" style="width: 16px; height: 16 │
│ px; border-radius: 2px; margin-right: 4px; v │
│ ertical-align: middle; flex-shrink: 0;">' │
│ : ""; │
│ │
│ - teamHtml += '<span style="display: in │
│ line-flex; align-items: center; margin-right │
│ : 8px; gap: 4px;">' + │
│ + // Check if player is from a differen │
│ t realm (for individual realm views only) │
│ + let playerName = member.profile.name; │
│ + if (isIndividualRealmView && member.p │
│ rofile.realm?.slug && member.profile.realm.s │
│ lug !== currentRealm) { │
│ + playerName += '<span style="color: │
│ #ff6b6b; font-weight: bold; margin-left: 2px │
│ ;">*</span>'; │
│ + } │
│ + │
│ + teamHtml += │
│ + '<span style="display: inline-flex; │
│ align-items: center; margin-right: 8px; gap │
│ : 4px;">' + │
│ iconHtml + │
│ - '<span style="color: ' + window.WoW │
│ .getClassColor(spec.class) + '; font-weight: │
│ 600; font-size: 0.9em;">' + member.profile. │
│ name + '</span>' + │
│ - '</span>'; │
│ + '<span style="color: ' + │
│ + window.WoW.getClassColor(spec.class │
│ ) + │
│ + '; font-weight: 600; font-size: 0.9 │
│ em;">' + │
│ + playerName + │
│ + "</span>" + │
│ + "</span>"; │
│ }); │
│ │
│ - row.innerHTML = '<div style="display: t │
│ able-cell; padding: 15px 10px; vertical-alig │
│ n: middle; font-size: 1.2em; font-weight: bo │
│ ld; color: #d8a657; text-align: center; widt │
│ h: 80px;">#' + group.ranking + '</div>' + │
│ - '<div style="display: table-cell; │
│ padding: 15px 10px; vertical-align: middle; │
│ font-family: \'Courier New\', monospace; fo │
│ nt-weight: bold; color: #ffffff; text-align: │
│ right; width: 120px;">' + formatDuration(gr │
│ oup.duration) + '</div>' + │
│ - '<div style="display: table-cell; │
│ padding: 15px 10px; vertical-align: middle; │
│ ">' + teamHtml + '</div>' + │
│ - '<div style="display: table-cell; │
│ padding: 15px 10px; vertical-align: middle; │
│ color: #aaaaaa; font-size: 0.9em; text-alig │
│ n: center; width: 140px;">' + formatTimestam │
│ p(group.completed_timestamp) + '</div>'; │
│ + row.innerHTML = │
│ + '<div style="display: table-cell; pad │
│ ding: 15px 10px; vertical-align: middle; fon │
│ t-size: 1.2em; font-weight: bold; color: #d8 │
│ a657; text-align: center; width: 80px;">#' + │
│ + group.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(group.duration) + │
│ + "</div>" + │
│ + '<div style="display: table-cell; pad │
│ ding: 15px 10px; vertical-align: middle;">' │
│ + │
│ + teamHtml + │
│ + "</div>" + │
│ + '<div style="display: table-cell; pad │
│ ding: 15px 10px; vertical-align: middle; col │
│ or: #aaaaaa; font-size: 0.9em; text-align: c │
│ enter; width: 140px;">' + │
│ + formatTimestamp(group.completed_times │
│ tamp) + │
│ + "</div>"; │
│ │
│ // Apply table-row styles │
│ - row.style.cssText = 'display: table-row │
│ !important; border-bottom: 1px solid #4a4a4 │
│ a !important; background-color: #32302f; tra │
│ nsition: background-color 0.2s ease;'; │
│ + row.style.cssText = │
│ + "display: table-row !important; borde │
│ r-bottom: 1px solid #4a4a4a !important; back │
│ ground-color: #32302f; transition: backgroun │
│ d-color 0.2s ease;"; │
│ │
│ // Add hover effect │
│ row.addEventListener("mouseenter", () = │
│ > { │
│ @@ -534,37 +581,66 @@ const initialDungeon = │
│ ''; │
│ function displayLeaderboard(data) { │
│ console.log("Displaying leaderboard dat │
│ a:", data); │
│ │
│ - // Check if we have no data (missing or │
│ empty leading_groups) │
│ - if (!data.leading_groups || data.leadin │
│ g_groups.length === 0) { │
│ + // Handle both old format (individual r │
│ ealm) and new format (aggregated) │
│ + const leaderboardData = data.leaderboar │
│ d || data.leading_groups; │
│ + │
│ + // Check if we have no data │
│ + if (!leaderboardData || leaderboardData │
│ .length === 0) { │
│ showNoData(); │
│ return; │
│ } │
│ │
│ - // Handle localized dungeon names │
│ - const dungeonName = │
│ - typeof data.map.name === "object" │
│ + // Handle dungeon name from both format │
│ s │
│ + let dungeonName; │
│ + if (data.dungeon_name) { │
│ + // New aggregated format │
│ + dungeonName = data.dungeon_name; │
│ + } else if (data.map) { │
│ + // Old individual realm format │
│ + dungeonName = typeof data.map.name == │
│ = "object" │
│ ? data.map.name.en_US || data.map.n │
│ ame │
│ : data.map.name; │
│ - document.getElementById("dungeon-name") │
│ .textContent = dungeonName; │
│ - document.getElementById("period").textC │
│ ontent = data.period; │
│ - document.getElementById("period-start") │
│ .textContent = formatTimestamp( │
│ - data.period_start_timestamp, │
│ - ); │
│ - document.getElementById("period-end").t │
│ extContent = formatTimestamp( │
│ - data.period_end_timestamp, │
│ - ); │
│ + } │
│ + document.getElementById("dungeon-name") │
│ .textContent = dungeonName || "Unknown Dunge │
│ on"; │
│ + │
│ + // Handle period info (only available i │
│ n individual realm format) │
│ + if (data.period) { │
│ + document.getElementById("period").tex │
│ tContent = data.period; │
│ + document.getElementById("period-start │
│ ").textContent = formatTimestamp(data.period │
│ _start_timestamp); │
│ + document.getElementById("period-end") │
│ .textContent = formatTimestamp(data.period_e │
│ nd_timestamp); │
│ + } else { │
│ + document.getElementById("period").tex │
│ tContent = "Multiple Periods"; │
│ + document.getElementById("period-start │
│ ").textContent = "-"; │
│ + document.getElementById("period-end") │
│ .textContent = "-"; │
│ + } │
│ │
│ + // Handle realm name display │
│ const currentRegion = document.getEleme │
│ ntById("region").value; │
│ const currentRealm = document.getElemen │
│ tById("realm").value; │
│ - const regionData = DATA_MAP[currentRegi │
│ on]; │
│ - const realmName = regionData?.realms?.[ │
│ currentRealm] || currentRealm; │
│ - document.getElementById("realm-name").t │
│ extContent = realmName; │
│ + let realmDisplayName; │
│ + │
│ + if (currentRegion === "global") { │
│ + realmDisplayName = "All Regions"; │
│ + } else if (currentRealm === "all") { │
│ + realmDisplayName = "All " + currentRe │
│ gion.toUpperCase() + " Realms"; │
│ + } else { │
│ + const regionData = DATA_MAP[currentRe │
│ gion]; │
│ + realmDisplayName = regionData?.realms │
│ ?.[currentRealm] || currentRealm; │
│ + } │
│ + document.getElementById("realm-name").t │
│ extContent = realmDisplayName; │
│ + │
│ + // Show/hide cross-realm indicator expl │
│ anation │
│ + const crossRealmNote = document.getElem │
│ entById("cross-realm-note"); │
│ + const isIndividualRealmView = currentRe │
│ gion && currentRealm && currentRealm !== "al │
│ l" && currentRegion !== "global"; │
│ + if (crossRealmNote) { │
│ + crossRealmNote.style.display = isIndi │
│ vidualRealmView ? "block" : "none"; │
│ + } │
│ │
│ const rowsContainer = document.getEleme │
│ ntById("leaderboard-rows"); │
│ rowsContainer.innerHTML = ""; │
│ │
│ - console.log("Creating rows for", data.l │
│ eading_groups.length, "groups"); │
│ - data.leading_groups.forEach((group, ind │
│ ex) => { │
│ + console.log("Creating rows for", leader │
│ boardData.length, "groups"); │
│ + leaderboardData.forEach((group, index) │
│ => { │
│ console.log("Creating row for group", │
│ index + 1); │
│ const row = createLeaderboardRow(grou │
│ p); │
│ rowsContainer.appendChild(row); │
│ @@ -593,7 +669,10 @@ const initialDungeon = │
│ ''; │
│ loadingMessage.textContent = message; │
│ } else { │
│ // Fallback if LoadingState structure │
│ isn't found │
│ - errorDiv.innerHTML = '<div class="loa │
│ ding-state error"><p class="loading-message" │
│ >' + message + '</p></div>'; │
│ + errorDiv.innerHTML = │
│ + '<div class="loading-state error">< │
│ p class="loading-message">' + │
│ + message + │
│ + "</p></div>"; │
│ } │
│ errorDiv.classList.remove("hidden"); │
│ document.getElementById("loading").clas │
│ sList.add("hidden"); │
│ @@ -611,11 +690,9 @@ const initialDungeon = │
│ ''; │
│ function updateRealmOptions() { │
│ const region = document.getElementById( │
│ "region").value; │
│ const realmSelect = document.getElement │
│ ById("realm"); │
│ - │
│ + │
│ console.log("=== updateRealmOptions STA │
│ RT ==="); │
│ console.log("Region:", region); │
│ - console.log("Realm select element:", re │
│ almSelect); │
│ - console.log("DATA_MAP keys:", Object.ke │
│ ys(DATA_MAP)); │
│ │
│ // Clear existing options │
│ realmSelect.innerHTML = ""; │
│ @@ -627,47 +704,50 @@ const initialDungeon = │
│ ''; │
│ option.textContent = "Select Region F │
│ irst"; │
│ option.selected = true; │
│ realmSelect.appendChild(option); │
│ + realmSelect.disabled = true; │
│ console.log("No region selected, show │
│ ing placeholder"); │
│ return; │
│ } │
│ │
│ + if (region === "global") { │
│ + // Global selected - disable realm se │
│ lection and show "N/A" │
│ + const option = document.createElement │
│ ("option"); │
│ + option.value = "global"; │
│ + option.textContent = "N/A (Global Vie │
│ w)"; │
│ + option.selected = true; │
│ + realmSelect.appendChild(option); │
│ + realmSelect.disabled = true; │
│ + console.log("Global region selected, │
│ disabled realm options"); │
│ + return; │
│ + } │
│ + │
│ + // Enable realm selection for regional │
│ selections │
│ + realmSelect.disabled = false; │
│ + │
│ + // Add "All Realms" option for regional │
│ view │
│ + const allRealmsOption = document.create │
│ Element("option"); │
│ + allRealmsOption.value = "all"; │
│ + allRealmsOption.textContent = "All " + │
│ region.toUpperCase() + " Realms"; │
│ + realmSelect.appendChild(allRealmsOption │
│ ); │
│ + │
│ // Add default option │
│ const defaultOption = document.createEl │
│ ement("option"); │
│ defaultOption.value = ""; │
│ defaultOption.textContent = "Select Rea │
│ lm"; │
│ defaultOption.selected = true; │
│ realmSelect.appendChild(defaultOption); │
│ - console.log("Added default option"); │
│ - │
│ - // Test with hardcoded US realms first │
│ - if (region === 'us') { │
│ - console.log("Adding hardcoded US real │
│ ms for testing"); │
│ - const testRealms = ['angerforge', 'ar │
│ ugal', 'ashkandi']; │
│ - testRealms.forEach(realm => { │
│ - const option = document.createEleme │
│ nt("option"); │
│ - option.value = realm; │
│ - option.textContent = realm.charAt(0 │
│ ).toUpperCase() + realm.slice(1); │
│ - realmSelect.appendChild(option); │
│ - }); │
│ - } │
│ │
│ - // Add realms for selected region │
│ + // Add individual realms for selected r │
│ egion │
│ const regionData = DATA_MAP[region]; │
│ - console.log("Region data:", regionData) │
│ ; │
│ if (regionData && regionData.realms) { │
│ - console.log("Adding realms:", Object. │
│ keys(regionData.realms)); │
│ Object.entries(regionData.realms).for │
│ Each(([slug, name]) => { │
│ const option = document.createEleme │
│ nt("option"); │
│ option.value = slug; │
│ option.textContent = name; │
│ realmSelect.appendChild(option); │
│ - console.log("Added realm: " + slug │
│ + " -> " + name); │
│ }); │
│ - } else { │
│ - console.log("No realm data found for │
│ region:", region); │
│ } │
│ - │
│ - console.log("Final realm select options │
│ :", realmSelect.options.length); │
│ + │
│ console.log("=== updateRealmOptions END │
│ ==="); │
│ } │
│ │
│ @@ -676,14 +756,27 @@ const initialDungeon = │
│ ''; │
│ const realm = document.getElementById(" │
│ realm").value; │
│ const dungeon = document.getElementById │
│ ("dungeon").value; │
│ │
│ - console.log('Getting data filename with │
│ :', { region, realm, dungeon }); │
│ - console.log('Dungeon name lookup:', dun │
│ geonNames[dungeon]); │
│ + console.log("Getting data filename with │
│ :", { region, realm, dungeon }); │
│ + console.log("Dungeon name lookup:", dun │
│ geonNames[dungeon]); │
│ │
│ - const dungeonName = dungeonNames[dungeo │
│ n] │
│ + const dungeonSlug = dungeonNames[dungeo │
│ n] │
│ .toLowerCase() │
│ .replace(/[^a-z0-9]/g, "-"); │
│ - const fileName = region + "/" + realm + │
│ "/" + dungeonName + "/" + realm + "-" + dun │
│ geonName + "-leaderboard.json"; │
│ - console.log('Generated filename:', file │
│ Name); │
│ + │
│ + let fileName; │
│ + │
│ + if (region === "global") { │
│ + // Global leaderboard path │
│ + fileName = "leaderboards/global/" + d │
│ ungeonSlug + "/leaderboard.json"; │
│ + } else if (realm === "all") { │
│ + // Regional aggregated leaderboard pa │
│ th │
│ + fileName = "leaderboards/regional/" + │
│ region + "/" + dungeonSlug + "/leaderboard. │
│ json"; │
│ + } else { │
│ + // Individual realm leaderboard path │
│ (existing system) │
│ + fileName = "challenge-mode/" + region │
│ + "/" + realm + "/" + dungeonSlug + "/" + r │
│ ealm + "-" + dungeonSlug + "-leaderboard.jso │
│ n"; │
│ + } │
│ + │
│ + console.log("Generated filename:", file │
│ Name); │
│ return fileName; │
│ } │
│ │
│ @@ -691,7 +784,13 @@ const initialDungeon = │
│ ''; │
│ const region = document.getElementById( │
│ "region").value; │
│ const realm = document.getElementById(" │
│ realm").value; │
│ const dungeon = document.getElementById │
│ ("dungeon").value; │
│ + │
│ + // For global, we only need region and │
│ dungeon │
│ + if (region === "global") { │
│ + return region && dungeon; │
│ + } │
│ │
│ + // For regional/individual realms, we n │
│ eed all three │
│ return region && realm && dungeon; │
│ } │
│ │
│ @@ -708,12 +807,14 @@ const initialDungeon = │
│ ''; │
│ const fileName = getDataFileName(); │
│ console.log("Loading file:", fileName │
│ ); │
│ // Use absolute path for GitHub Pages │
│ compatibility │
│ - const fullUrl = "/data/challenge-mode │
│ /" + fileName; │
│ + const fullUrl = "/data/" + fileName; │
│ console.log("Full fetch URL:", fullUr │
│ l); │
│ const response = await fetch(fullUrl) │
│ ; │
│ │
│ if (!response.ok) { │
│ - throw new Error("Failed to load lea │
│ derboard data: " + response.statusText); │
│ + throw new Error( │
│ + "Failed to load leaderboard data: │
│ " + response.statusText, │
│ + ); │
│ } │
│ │
│ const data = await response.json(); │
│ @@ -726,21 +827,22 @@ const initialDungeon = │
│ ''; │
│ } │
│ │
│ function updateURL() { │
│ - // Only update URL if all selections ar │
│ e made │
│ - if (!canLoadLeaderboard()) { │
│ - return; │
│ - } │
│ - │
│ + // Only update URL for individual realm │
│ selections (not global/regional) │
│ const region = document.getElementById( │
│ "region").value; │
│ const realm = document.getElementById(" │
│ realm").value; │
│ const dungeon = document.getElementById │
│ ("dungeon").value; │
│ │
│ + // Don't update URL for global or "all │
│ realms" selections │
│ + if (region === "global" || realm === "a │
│ ll" || !canLoadLeaderboard()) { │
│ + return; │
│ + } │
│ + │
│ const dungeonName = dungeonNames[dungeo │
│ n] │
│ .toLowerCase() │
│ .replace(/[^a-z0-9]/g, "-"); │
│ - │
│ + │
│ const newURL = "/challenge-mode/" + reg │
│ ion + "/" + realm + "/" + dungeonName; │
│ - window.history.pushState({}, '', newURL │
│ ); │
│ + window.history.pushState({}, "", newURL │
│ ); │
│ } │
│ │
│ // Event listeners │
│ @@ -750,7 +852,10 @@ const initialDungeon = │
│ ''; │
│ │
│ // Bind change events │
│ document.getElementById("region").addEv │
│ entListener("change", () => { │
│ - console.log("Region changed to:", doc │
│ ument.getElementById("region").value); │
│ + console.log( │
│ + "Region changed to:", │
│ + document.getElementById("region").v │
│ alue, │
│ + ); │
│ updateRealmOptions(); │
│ // Reset realm selection when region │
│ changes │
│ document.getElementById("realm").valu │
│ e = ""; │
│ @@ -765,9 +870,13 @@ const initialDungeon = │
│ ''; │
│ loadLeaderboard(); │
│ }); │
│ document.getElementById("dungeon").addE │
│ ventListener("change", () => { │
│ - console.log("Dungeon changed to:", do │
│ cument.getElementById("dungeon").value); │
│ + console.log( │
│ + "Dungeon changed to:", │
│ + document.getElementById("dungeon"). │
│ value, │
│ + ); │
│ updateURL(); │
│ loadLeaderboard(); │
│ }); │
│ }); │
│ -</script> │
│ \ No newline at end of file │
│ +</script> │
│ + │
└──────────────────────────────────────────────┘