From 83b6a2069d40dceddb2b48ae498b2caeefd6c2ab Mon Sep 17 00:00:00 2001 From: mandalore Date: Fri, 4 Sep 2026 16:26:51 -0500 Subject: [PATCH] Add RTT/GT scope tag and a format/scope filter bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BCP has no structured scope field (eventType/eventSubType are always empty), and name-matching against RTT/GT/Major only covered ~55% of events while systematically missing well-known majors that don't literally say "GT" (ATC, 8TC, the Gateway Open...). Instead: RTT vs GT is inferred from event duration (single day vs multi-day), Singles events only, no Major tag — per team captain's call. Duration has to be computed in each event's own timezone, not raw UTC or the viewer's browser timezone: BCP end-times often land on the next UTC calendar date purely from offset (a Chicago RTT running 9am-9pm local reports as spanning two UTC dates). Verified against real RTT- and GT-named events before shipping. The existing date-range display had the same latent bug — fixed alongside this using the same timezone-aware day comparison, so the badge and the displayed date range can never visually contradict each other. Also adds a filter bar (Format: Singles/Team Event, Scope: RTT/GT) — toggling narrows the list per circuit without refetching, since the already-fetched events are now kept around and just re-filtered. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SBzcNkW7JcYipnAX6HfgmK --- sites/events/index.html | 10 ++++ sites/events/src/main.js | 123 ++++++++++++++++++++++++++++++--------- 2 files changed, 104 insertions(+), 29 deletions(-) diff --git a/sites/events/index.html b/sites/events/index.html index d7d7c4c..ea8c77e 100644 --- a/sites/events/index.html +++ b/sites/events/index.html @@ -18,6 +18,16 @@

Across the circuits Gateway Gamers follows

+ +
+ Format + + + Scope + + +
+
diff --git a/sites/events/src/main.js b/sites/events/src/main.js index bbfa59f..46f5fb0 100644 --- a/sites/events/src/main.js +++ b/sites/events/src/main.js @@ -112,28 +112,46 @@ function esc(s) { return String(s).replace(/[&<>"]/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); } -function fmtDateRange(startIso, endIso) { - const start = new Date(startIso); - const end = endIso ? new Date(endIso) : null; +// BCP end-times often land on the next UTC calendar date purely from +// timezone offset — a Chicago RTT running 9am-9pm local reports as +// spanning two UTC dates. Comparing calendar days in the *event's own* +// timezone (not the viewer's browser timezone, not raw UTC) is the only +// way to get a stable answer regardless of who's looking at the page. +function localDateKey(date, timeZone) { + return new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).format(date); +} + +function dateParts(date, timeZone) { + const parts = new Intl.DateTimeFormat("en-US", { timeZone, year: "numeric", month: "short", day: "numeric", weekday: "short" }).formatToParts(date); + return Object.fromEntries(parts.map(p => [p.type, p.value])); +} + +function sameCalendarDay(ev) { + if (!ev.eventEndDate) return true; + const tz = ev.timeZone || undefined; + return localDateKey(new Date(ev.eventDate), tz) === localDateKey(new Date(ev.eventEndDate), tz); +} + +function fmtDateRange(ev) { + const tz = ev.timeZone || undefined; + const start = new Date(ev.eventDate); + const sp = dateParts(start, tz); // Single day: "Sat, Oct 17, 2026" - if (!end || start.toDateString() === end.toDateString()) { - return start.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric", year: "numeric" }); + if (!ev.eventEndDate || sameCalendarDay(ev)) { + return `${sp.weekday}, ${sp.month} ${sp.day}, ${sp.year}`; } - const sameYear = start.getFullYear() === end.getFullYear(); - const sameMonth = sameYear && start.getMonth() === end.getMonth(); + const ep = dateParts(new Date(ev.eventEndDate), tz); + const sameMonth = sp.month === ep.month && sp.year === ep.year; // Same month: "Oct 10–11, 2026" - if (sameMonth) { - const month = start.toLocaleDateString(undefined, { month: "short" }); - return `${month} ${start.getDate()}–${end.getDate()}, ${end.getFullYear()}`; - } + if (sameMonth) return `${sp.month} ${sp.day}–${ep.day}, ${ep.year}`; // Different month (or year): "Oct 30 – Nov 1, 2026" / "Dec 30, 2026 – Jan 2, 2027" - const startStr = start.toLocaleDateString(undefined, { month: "short", day: "numeric", year: sameYear ? undefined : "numeric" }); - const endStr = end.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }); - return `${startStr} – ${endStr}`; + const sameYear = sp.year === ep.year; + const startStr = sameYear ? `${sp.month} ${sp.day}` : `${sp.month} ${sp.day}, ${sp.year}`; + return `${startStr} – ${ep.month} ${ep.day}, ${ep.year}`; } function locationOf(ev) { @@ -151,6 +169,40 @@ function formatOf(ev) { return ev.teamEvent ? "Team Event" : "Singles"; } +// BCP has no structured field for event scope (RTT/GT/Major) — name- +// matching only covers ~55% of events and systematically misses +// well-known majors whose names don't say "GT" (ATC, 8TC, the Gateway +// Open...). Instead: RTT vs GT is inferred from duration (single day vs +// multi-day, in the event's own timezone — see sameCalendarDay), and +// only applies to Singles events. Team events don't get a scope tag, +// and there's no "Major" tag — both per team captain's call. +function scopeOf(ev) { + if (ev.teamEvent) return null; + return sameCalendarDay(ev) ? "RTT" : "GT"; +} + +/* ---- Filters ---- */ +const activeFormats = new Set(["Singles", "Team Event"]); +const activeScopes = new Set(["RTT", "GT"]); + +function passesFilter(ev) { + const scope = scopeOf(ev); + return activeFormats.has(formatOf(ev)) && (scope === null || activeScopes.has(scope)); +} + +function setupFilters() { + document.querySelectorAll("#filters button[data-filter-group]").forEach(btn => { + btn.addEventListener("click", () => { + const set = btn.dataset.filterGroup === "format" ? activeFormats : activeScopes; + const nowActive = !btn.classList.contains("btn-active"); + btn.classList.toggle("btn-active", nowActive); + if (nowActive) set.add(btn.dataset.filterValue); + else set.delete(btn.dataset.filterValue); + renderCircuits(); + }); + }); +} + function eventCard(ev) { const capacity = ev.numTickets ? `${ev.checkedInPlayers ?? 0}/${ev.numTickets} checked in` @@ -159,6 +211,8 @@ function eventCard(ev) { ? `${esc(ev.gameSystemName)}` : ""; const format = `${formatOf(ev)}`; + const scope = scopeOf(ev); + const scopeBadge = scope ? `${scope}` : ""; return ` @@ -166,19 +220,24 @@ function eventCard(ev) {
${esc(ev.name)}
${esc(locationOf(ev))}
-
${format}${system}${capacity}
+
${format}${scopeBadge}${system}${capacity}
-
${fmtDateRange(ev.eventDate, ev.eventEndDate)}
+
${fmtDateRange(ev)}
`; } -function circuitSection(circuit, events) { - const body = events.length - ? events.map(eventCard).join("") - : `

No upcoming events for this circuit.

`; +function circuitSection(circuit, events, filtered) { + let body; + if (!events.length) { + body = `

No upcoming events for this circuit.

`; + } else if (!filtered.length) { + body = `

No events match the current filter.

`; + } else { + body = filtered.map(eventCard).join(""); + } return `

@@ -191,8 +250,19 @@ function circuitSection(circuit, events) { } /* ---- Orchestration ---- */ +let allResults = []; + +function renderCircuits() { + $("circuits").innerHTML = allResults.map(({ circuit, events, error }) => + error + ? `

${esc(circuit.name)}

+

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

` + : circuitSection(circuit, events, events.filter(passesFilter)) + ).join(""); +} + async function load() { - const results = await Promise.all( + allResults = await Promise.all( CONFIG.circuits.map(async circuit => { try { return { circuit, events: await fetchUpcomingEvents(circuit.id) }; @@ -204,14 +274,9 @@ async function load() { ); $("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(""); + $("circuits").classList.remove("hidden"); + renderCircuits(); } +setupFilters(); load();