/* ---- 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 = `
${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);