/* ---- 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 circuits shown on the page. Add/remove/edit entries here; the page builds one section per circuit. `id` is the leagueId from a circuit's BCP calendar URL, e.g. bestcoastpairings.com/circuit//calendar ============================================================ */ const CONFIG = { circuits: [ { id: "Teqdr6p0aGvO", name: "Away Games 2025/26 ITC Season" }, { id: "t5kk3NvGDMXc", name: "National Tabletop League (NTL) 8-Player Circuit" }, { id: "m4OIU24N0IZO", name: "TEO 2026 Circuit" }, { id: "cY0LeCAPfyiG", name: "2026 Lord Marshal Conference" }, ], }; /* ---- BCP API ---- */ const API = "https://newprod-api.bestcoastpairings.com/v1"; const HEADERS = { "client-id": "web-app", "env": "bcp", "accept": "application/json" }; const BCP_ORIGIN = "https://www.bestcoastpairings.com"; // Cursor-paginated events for a league/circuit, newest-date-first (BCP // returns the same cursor when there are no more results — stop when it // loops, same guard as the other sites' streamPlacings). async function* streamEvents(leagueId) { const base = `${API}/events?leagueId=${leagueId}&sortAscending=false&limit=50`; 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("events/" + r.status); const p = await r.json(); const page = Array.isArray(p) ? p : (p.data || []); yield page; const rawKey = p.nextKey; nextKey = !rawKey ? null : typeof rawKey === "string" ? rawKey : btoa(JSON.stringify(rawKey)); if (nextKey === prevNextKey) break; prevNextKey = nextKey; } while (nextKey); } // Sorted newest-first, so we can stop as soon as we cross into the past // instead of paging through a circuit's entire history every time. async function fetchUpcomingEvents(leagueId) { const now = Date.now(); const upcoming = []; for await (const page of streamEvents(leagueId)) { if (!page.length) break; for (const ev of page) { const end = Date.parse(ev.eventEndDate || ev.eventDate); if (Number.isFinite(end) && end >= now) upcoming.push(ev); } // Page is sorted by start date descending, so once the oldest (last) // event here has already ended, every event on later pages — all with // even older start dates — will have ended too. Stop paging. const last = page[page.length - 1]; const lastEnd = Date.parse(last.eventEndDate || last.eventDate); if (!Number.isFinite(lastEnd) || lastEnd < now) break; } return upcoming.reverse(); // soonest first } /* ---- Render ---- */ const $ = id => document.getElementById(id); function esc(s) { return String(s).replace(/[&<>"]/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); } function fmtDateRange(startIso, endIso) { const start = new Date(startIso); const opts = { weekday: "short", month: "short", day: "numeric", year: "numeric" }; const startStr = start.toLocaleDateString(undefined, opts); if (!endIso) return startStr; const end = new Date(endIso); if (start.toDateString() === end.toDateString()) return startStr; const sameMonth = start.getMonth() === end.getMonth() && start.getFullYear() === end.getFullYear(); const endStr = sameMonth ? end.toLocaleDateString(undefined, { day: "numeric" }) : end.toLocaleDateString(undefined, opts); return `${startStr} – ${endStr}`; } function locationOf(ev) { const parts = [ev.locationName, ev.city, ev.country].filter(Boolean); return parts.length ? parts.join(", ") : (ev.formatted_address || "Location TBD"); } function eventCard(ev) { const capacity = ev.numTickets ? `${ev.checkedInPlayers ?? 0}/${ev.numTickets} checked in` : ""; const system = ev.gameSystemName ? `${esc(ev.gameSystemName)}` : ""; return `
${esc(ev.name)}
${esc(locationOf(ev))}
${system}${capacity}
${fmtDateRange(ev.eventDate, ev.eventEndDate)}
`; } function circuitSection(circuit, events) { const body = events.length ? events.map(eventCard).join("") : `

No upcoming events for this circuit.

`; return `

${esc(circuit.name)}

${body}
`; } /* ---- Orchestration ---- */ async function load() { const results = await Promise.all( CONFIG.circuits.map(async circuit => { try { return { circuit, events: await fetchUpcomingEvents(circuit.id) }; } catch (err) { console.error("Failed to load circuit", circuit.id, err); return { circuit, events: [], error: true }; } }) ); $("loading").classList.add("hidden"); const container = $("circuits"); container.classList.remove("hidden"); container.innerHTML = results.map(({ circuit, events, error }) => error ? `

${esc(circuit.name)}

Couldn't load events for this circuit — try refreshing.

` : circuitSection(circuit, events) ).join(""); } load();