Consolidate into one chronological list, tag each event by circuit

Replaces the one-section-per-circuit layout with a single flat,
deduped, date-sorted feed. Events belonging to more than one watched
circuit (real and not rare — verified 2 current events span both a
regional circuit and the national league) get one card carrying every
circuit badge that matched, rather than appearing once per section.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBzcNkW7JcYipnAX6HfgmK
This commit is contained in:
2026-09-04 17:08:11 -05:00
co-authored by Claude Sonnet 5
parent 463371d199
commit 4bb5dc8ed8
2 changed files with 44 additions and 31 deletions
+42 -29
View File
@@ -240,12 +240,17 @@ function setupFilters() {
btn.classList.toggle("opacity-40", !nowActive);
if (nowActive) set.add(btn.dataset.filterValue);
else set.delete(btn.dataset.filterValue);
renderCircuits();
renderEvents();
});
});
}
function eventCard(ev) {
// A single event can belong to more than one watched circuit (BCP events
// carry their own list of leagues, and it's common for e.g. a national
// league and a regional circuit to both include the same event) — badge
// it with every circuit it matched under rather than showing it once
// per circuit.
function eventCard(ev, circuits) {
const capacity = ev.numTickets
? `<span class="badge badge-ghost badge-sm">${ev.checkedInPlayers ?? 0}/${ev.numTickets} checked in</span>`
: "";
@@ -255,6 +260,7 @@ function eventCard(ev) {
const format = `<span class="badge badge-warning badge-sm">${formatOf(ev)}</span>`;
const scope = scopeOf(ev);
const scopeBadge = scope ? `<span class="badge badge-info badge-sm">${scope}</span>` : "";
const circuitBadges = circuits.map(c => `<span class="badge badge-accent badge-sm">${esc(c.name)}</span>`).join("");
return `
<a href="${BCP_ORIGIN}/event/${esc(ev.id)}" target="_blank" rel="noopener"
class="card card-border bg-base-200 hover:border-warning transition-colors">
@@ -262,7 +268,7 @@ function eventCard(ev) {
<div class="min-w-0">
<div class="font-semibold truncate">${esc(ev.name)}</div>
<div class="text-sm opacity-60 mt-1">${esc(locationOf(ev))}</div>
<div class="flex gap-2 mt-2">${format}${scopeBadge}${system}${capacity}</div>
<div class="flex gap-2 mt-2 flex-wrap">${format}${scopeBadge}${circuitBadges}${system}${capacity}</div>
</div>
<div class="text-right shrink-0">
<div class="font-mono text-sm text-warning">${fmtDateRange(ev)}</div>
@@ -271,36 +277,43 @@ function eventCard(ev) {
</a>`;
}
function circuitSection(circuit, events, filtered) {
let body;
if (!events.length) {
body = `<p class="text-sm text-base-content/50 py-4">No upcoming events for this circuit.</p>`;
} else if (!filtered.length) {
body = `<p class="text-sm text-base-content/50 py-4">No events match the current filter.</p>`;
} else {
body = filtered.map(eventCard).join("");
// Dedupe by event id across all circuits, collecting every circuit an
// event matched under, then sort the merged list chronologically —
// replaces the old one-section-per-circuit layout with a single feed.
function mergeEvents(results) {
const byId = new Map();
for (const { circuit, events, error } of results) {
if (error) continue;
for (const ev of events) {
if (!byId.has(ev.id)) byId.set(ev.id, { event: ev, circuits: [] });
byId.get(ev.id).circuits.push(circuit);
}
}
return `
<section>
<h2 class="text-2xl font-bold uppercase tracking-wide mb-3">
<a href="${BCP_ORIGIN}/circuit/${esc(circuit.id)}/calendar" target="_blank" rel="noopener" class="hover:text-warning">
${esc(circuit.name)}
</a>
</h2>
<div class="space-y-3">${body}</div>
</section>`;
return [...byId.values()].sort((a, b) => Date.parse(a.event.eventDate) - Date.parse(b.event.eventDate));
}
/* ---- Orchestration ---- */
let allResults = [];
function renderCircuits() {
$("circuits").innerHTML = allResults.map(({ circuit, events, error }) =>
error
? `<section><h2 class="text-2xl font-bold uppercase tracking-wide mb-3">${esc(circuit.name)}</h2>
<p class="text-sm text-error py-4">Couldn't load events for this circuit — try refreshing.</p></section>`
: circuitSection(circuit, events, events.filter(passesFilter))
).join("");
function renderEvents() {
const merged = mergeEvents(allResults);
const filtered = merged.filter(({ event }) => passesFilter(event));
const failed = allResults.filter(r => r.error).map(r => r.circuit.name);
const errorBanner = failed.length
? `<p class="text-sm text-error mb-4">Couldn't load: ${failed.map(esc).join(", ")} — try refreshing.</p>`
: "";
let body;
if (!merged.length) {
body = `<p class="text-sm text-base-content/50 py-4">No upcoming events found.</p>`;
} else if (!filtered.length) {
body = `<p class="text-sm text-base-content/50 py-4">No events match the current filter.</p>`;
} else {
body = `<div class="space-y-3">${filtered.map(({ event, circuits }) => eventCard(event, circuits)).join("")}</div>`;
}
$("events").innerHTML = errorBanner + body;
}
async function load() {
@@ -317,8 +330,8 @@ async function load() {
);
$("loading").classList.add("hidden");
$("circuits").classList.remove("hidden");
renderCircuits();
$("events").classList.remove("hidden");
renderEvents();
}
setupFilters();