/* ---- Shared: header / footer ---- */ function loadHTML(file, elementId, callback) { fetch(file) .then(r => r.text()) .then(html => { document.getElementById(elementId).innerHTML = html; if (callback) callback(); }) .catch(err => console.error('Error loading:', file, err)); } function highlightActiveNav() { const current = location.pathname.split('/').pop() || 'index.html'; document.querySelectorAll('#header a[href]').forEach(link => { if (link.getAttribute('href') === `/${current}`) { link.classList.add('active'); } }); } function startCarousel() { const slides = document.querySelectorAll('#hero-carousel .carousel-slide'); if (!slides.length) return; let current = 0; slides[current].style.opacity = '1'; setInterval(() => { slides[current].style.opacity = '0'; current = (current + 1) % slides.length; slides[current].style.opacity = '1'; }, 5000); } loadHTML('/header.html', 'header', () => { highlightActiveNav(); startCarousel(); }); loadHTML('/footer.html', 'footer'); /* ============================================================ CONFIG — the only block you edit ============================================================ */ const CONFIG = { teamId: "2iGDVMgX0a", leagueId: "RtgcexBzqjCM", regionId: "VgQKgqmTPU", // Fallback roster if the team endpoint requires auth. memberIds: [], // Flip to true to preview the layout with placeholder rows (no network). useSample: false, }; const API = "https://newprod-api.bestcoastpairings.com/v1"; const HEADERS = { "client-id": "web-app", "env": "bcp", "accept": "application/json" }; const CACHE_KEY = "gg_aos_board_v1"; const $ = (id) => document.getElementById(id); const fmtPts = (n) => Number(n || 0).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 }); const fullName = (u = {}) => [u.firstName, u.lastName].filter(Boolean).join(" ").trim(); /* ---- Roster ---- */ async function getMemberIds(force) { try { const r = await fetch(`${API}/teams/${CONFIG.teamId}?expand[]=owner`, { headers: HEADERS, credentials: "omit" }); if (!r.ok) throw new Error("auth/" + r.status); let t = await r.json(); if (Array.isArray(t)) t = t[0] || {}; const ids = new Set(); if (Array.isArray(t.memberIds)) t.memberIds.forEach(x => typeof x === "string" && ids.add(x)); if (typeof t.ownerId === "string") ids.add(t.ownerId); [t.users, t.members, t.teamMembers].forEach(list => { if (Array.isArray(list)) list.forEach(o => o && o.id && ids.add(o.id)); }); if (t.owner && t.owner.id) ids.add(t.owner.id); if (ids.size) return { ids, live: true }; throw new Error("empty"); } catch (e) { if (CONFIG.memberIds.length) return { ids: new Set(CONFIG.memberIds), live: false }; throw e; } } /* ---- Standings (cursor-paginated, circular-cursor safe) ---- */ async function* streamPlacings() { const base = `${API}/placings?placingsType=player&leagueId=${CONFIG.leagueId}` + `®ionId=${CONFIG.regionId}&sortAscending=false&limit=1000`; let nextKey = null; let prevNextKey = undefined; do { const url = nextKey ? `${base}&nextKey=${encodeURIComponent(nextKey)}` : base; const r = await fetch(url, { headers: HEADERS, credentials: "omit" }); if (!r.ok) throw new Error("placings/" + r.status); const p = await r.json(); const page = Array.isArray(p) ? p : (p.data || p.placings || []); yield page; const rawKey = p.nextKey; nextKey = !rawKey ? null : typeof rawKey === "string" ? rawKey : btoa(JSON.stringify(rawKey)); // BCP returns the same cursor when there are no more results — stop when it loops if (nextKey === prevNextKey) break; prevNextKey = nextKey; } while (nextKey); } /* ---- Build leaderboard ---- */ function build(rows, ids) { const kept = []; for (const rec of rows) { const uid = rec.userId || (rec.user && rec.user.id); if (uid && ids.has(uid)) kept.push(rec); } kept.sort((a, b) => Number(b.ITCPoints || 0) - Number(a.ITCPoints || 0)); return kept; } /* ---- Render ---- */ function render(state) { const { kept, rosterSize, updated, live, fromCache } = state; $("dot").className = "w-2 h-2 rounded-full bg-success animate-pulse transition-colors"; $("updated").textContent = (fromCache ? "Cached " : "Updated ") + new Date(updated).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); $("meta").textContent = `${kept.length} of ${rosterSize} members placed` + (live ? "" : " · roster (cached)"); $("banner").className = "alert mb-4 hidden"; const champ = $("champion"), strip = $("strip"), empty = $("empty"), tb = $("rows"); tb.innerHTML = ""; if (!kept.length) { champ.classList.add("hidden"); strip.hidden = true; empty.classList.remove("hidden"); empty.textContent = "No Gateway Gamers players found in this league's standings yet. " + "Once members log games here, they'll appear ranked by ITC points."; return; } empty.classList.add("hidden"); strip.hidden = false; // Champion card const top = kept[0], tu = top.user || {}; champ.classList.remove("hidden"); $("champ-name").textContent = fullName(tu) || tu.nickname || "—"; $("champ-sub").textContent = `${top.wins || 0}–${top.losses || 0}–${top.ties || 0} · ITC #${top.placing ?? "—"}`; $("champ-pts").textContent = fmtPts(top.ITCPoints); // Stat strip const W = kept.reduce((s, r) => s + (+r.wins || 0), 0), L = kept.reduce((s, r) => s + (+r.losses || 0), 0), T = kept.reduce((s, r) => s + (+r.ties || 0), 0), best = Math.min(...kept.map(r => +r.placing || Infinity)); $("s-placed").textContent = kept.length; $("s-top").textContent = fmtPts(top.ITCPoints); $("s-rec").textContent = `${W}–${L}–${T}`; $("s-best").textContent = isFinite(best) ? ("#" + best) : "—"; // Rows kept.forEach((r, i) => { const u = r.user || {}, name = fullName(u) || u.nickname || "Unknown"; const tr = document.createElement("tr"); tr.innerHTML = `${i === 0 ? `1` : (i + 1)}` + `${esc(name)}` + `${u.nickname && fullName(u) ? `"${esc(u.nickname)}"` : ""}` + `${fmtPts(r.ITCPoints)}` + `` + `${r.wins || 0}${r.losses || 0}–${r.ties || 0}` + `#${r.placing ?? "—"}`; tb.appendChild(tr); }); } function esc(s) { return String(s).replace(/[&<>"]/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); } /* ---- Cache ---- */ function paintCache() { try { const c = JSON.parse(localStorage.getItem(CACHE_KEY) || "null"); if (c && c.kept) render({ ...c, fromCache: true }); } catch (e) {} } function saveCache(state) { try { localStorage.setItem(CACHE_KEY, JSON.stringify({ kept: state.kept, rosterSize: state.rosterSize, updated: state.updated, live: state.live })); } catch (e) {} } /* ---- Orchestration ---- */ async function load(force) { const btn = $("refresh"); btn.disabled = true; $("dot").className = "w-2 h-2 rounded-full bg-base-content/30 transition-colors"; $("updated").textContent = "Fetching…"; showSkeleton(); try { if (CONFIG.useSample) { return render({ ...SAMPLE, fromCache: false }); } const { ids, live } = await getMemberIds(force); const allRows = []; for await (const page of streamPlacings()) { allRows.push(...page); // Render incrementally so results appear as pages arrive const kept = build(allRows, ids); if (kept.length) render({ kept, rosterSize: ids.size, updated: Date.now(), live, fromCache: false }); } const kept = build(allRows, ids); const state = { kept, rosterSize: ids.size, updated: Date.now(), live }; render({ ...state, fromCache: false }); saveCache(state); } catch (err) { failure(err); } finally { btn.disabled = false; } } function showSkeleton() { const tb = $("rows"); tb.innerHTML = ""; for (let i = 0; i < 6; i++) { const tr = document.createElement("tr"); tr.innerHTML = `
` + `
` + `
` + `
` + `
`; tb.appendChild(tr); } } function failure(err) { $("dot").className = "w-2 h-2 rounded-full bg-base-content/30 transition-colors"; $("updated").textContent = "Couldn't update"; $("rows").innerHTML = ""; $("champion").classList.add("hidden"); $("strip").hidden = true; const b = $("banner"); const msg = String(err.message || err); b.className = "alert alert-error mb-4"; b.innerHTML = msg.includes("placings") ? "Couldn't reach the BCP standings feed. Try refreshing, or open the page directly in a browser." : `Something went wrong: ${esc(msg)}`; } /* ---- Sample (preview only; CONFIG.useSample = true) ---- */ const SAMPLE = (() => { const mk = (f, l, p, w, ls, t, pl) => ({ user: { firstName: f, lastName: l }, ITCPoints: p, wins: w, losses: ls, ties: t, placing: pl }); const kept = [ mk("Eric", "Darais", 920.7, 19, 4, 0, 453), mk("Jordan", "Vance", 812.4, 16, 6, 1, 712), mk("Mara", "Singh", 770.1, 15, 7, 0, 889), mk("Devon", "Cole", 655.9, 12, 8, 2, 1340), ]; return { kept, rosterSize: 8, updated: Date.now(), live: true }; })(); /* ---- Go ---- */ $("refresh").addEventListener("click", () => load(true)); paintCache(); load(false);