Add a reusable event calendar and put it on the front page

EventCalendar shows events on a month grid or as a list of the
month. Multi-day events are lane-packed bars that break at week
edges, series events appear on every meeting with their time, and
clicking a day lists everything on it. Phones get the list. Visitors
can narrow by scope, type, online only and search, all starting at
"all"; a page can pin section, host or type through props, which
hides that control.

Migration 019 rebuilds front_page_sections to allow a 'calendar'
band and slots it in after the retreats carousel, so it can be
reordered, retitled or hidden from the Front page editor.

useEvents drops its empty fallback so a failed request surfaces as
an error rather than an empty list, and returns the scope list
alongside the events for the calendar's scope filter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Zaldimmar 2026-09-25 06:28:42 -05:00
parent 641ab167b0
commit 2e125b4629
10 changed files with 947 additions and 8 deletions

View file

@ -18,6 +18,13 @@
belongs to, the type is what kind of gathering it is. A regional
class matches both { section: "regional" } and { type: "class" }.
The event_sections rows (the scope list) come back alongside, for
anything that offers a scope filter.
No fallback. An empty list on a failed request would read as
"nothing scheduled" when the truth is "the server is down", so
the error comes back and each caller says so.
Filtering here rather than in the query keeps the endpoint to
one cached response. At a few dozen events that's the right
trade; if the list ever runs to hundreds, move the filters into
@ -28,12 +35,15 @@ import { useMemo } from "react";
import { useResource } from "../lib/useResource.js";
const EMPTY = { events: [] };
/* One shared empty list, so a memo keyed on `sections` doesn't
restart on every render before the data arrives. */
const NO_SECTIONS = [];
export function useEvents({ section, host, status, type } = {}) {
const { data, error, loading } = useResource("/events", { fallback: EMPTY });
const { data, error, loading } = useResource("/events");
const all = data?.events;
const sections = data?.sections ?? NO_SECTIONS;
/* An array prop is a new identity on every render, which would
restart the memo each time. Joining it gives the dependency
@ -55,7 +65,7 @@ export function useEvents({ section, host, status, type } = {}) {
return list;
}, [all, section, host, status, typeKey]);
return { events, loading, error };
return { events, sections, loading, error };
}
/* Past and upcoming, split. `status` arrives already resolved — the