/* ═══════════════════════════════════════════════════════════════ EVENT DATA One request, filtered per section. The Retreats page has three bands of events, and all three call this hook — the cache in api.js keys on the path, so they share a single fetch and each narrows the result to what it shows. useEvents({ section: "national" }) one band useEvents({ host: "northwest" }) one host's events useEvents({ status: "upcoming" }) a home page strip useEvents({ type: "workshop" }) one kind, wherever it is useEvents({ type: ["class", "workshop"] }) useEvents() everything `section` and `type` are different questions and stack rather than overlap: the section is which band of the page an event 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 the URL and let each become its own cache entry. ═══════════════════════════════════════════════════════════════ */ import { useMemo } from "react"; import { useResource } from "../lib/useResource.js"; /* 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"); 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 list something stable to compare. */ const typeKey = Array.isArray(type) ? type.join(",") : (type ?? ""); const events = useMemo(() => { let list = all ?? []; if (section) list = list.filter(e => e.section_id === section); // `host` is an organization or a person slug, and an event can // have several of either — co-hosting puts one event on both // hosts' lists, which is the point. if (host) list = list.filter(e => e.hosts?.some(h => h.id === host)); if (status) list = list.filter(e => e.status === status); if (typeKey) { const wanted = new Set(typeKey.split(",")); list = list.filter(e => wanted.has(e.event_type)); } return list; }, [all, section, host, status, typeKey]); return { events, sections, loading, error }; } /* Past and upcoming, split. `status` arrives already resolved — the explicit value when there is one, otherwise derived from ends_on — so nothing here needs to know which of the two it got. */ export function splitByStatus(events = []) { const upcoming = []; const past = []; for (const event of events) { (event.status === "past" ? past : upcoming).push(event); } return { upcoming, past }; } /* Which of the declared types a list actually contains, in EVENT_TYPES order rather than whatever order the rows arrived in. A section with one type has nothing to filter, which is what lets the chip bar hide itself. */ export function typesPresent(events = [], declared = []) { const seen = new Set(events.map(e => e.event_type)); return declared.filter(entry => seen.has(entry.id)); }