Migrations 020–023, with the code that reads each: - 020 drops people_lists, people_list_members, v_chapters and v_person_affiliations. Nothing queried any of them. - 021 renames event_sections to event_scopes and events.section_id to scope_id, finishing what 015 described. The API sends `scopes` and `scope_id`, and useEvents, EventListCards and EventCalendar take `scope`. It also inserts national/regional/partner, which only the retired seed ever created: a database built from migrations alone had no scope for the Retreats bands. - 022 drops events.sort_order and people.sort_order. Events now sort by date (upcoming soonest first, past latest first, undated last) on /events, org pages and the countdown. People were only ever sorted by sort_name on the site. Every other sort_order stays. - 023 rebuilds teams with created_at and updated_at plus a touch trigger, so the teams editor gets the same optimistic concurrency as the other entities. Awards can be drafted: is_published (added in 011) is on both descriptor halves with a Publishing group, and the award list, award page, org awards and event awards leave drafts out. The migration runner now turns foreign keys off around the per-file transactions and runs foreign_key_check before each commit. PRAGMA foreign_keys is a no-op inside a transaction, so 009's warning was right and a rebuild of a referenced table (023) couldn't be written otherwise. CLAUDE.md is updated to match. Also removes the stray src/App.tsx.save. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
92 lines
3.9 KiB
TypeScript
92 lines
3.9 KiB
TypeScript
/* ═══════════════════════════════════════════════════════════════
|
|
EVENT DATA
|
|
|
|
One request, filtered per band. The Retreats page has three
|
|
bands of events, and all three call this hook — the cache in
|
|
api.ts keys on the path, so they share a single fetch and each
|
|
narrows the result to what it shows.
|
|
|
|
useEvents({ scope: "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
|
|
|
|
`scope` and `type` are different questions and stack rather
|
|
than overlap: the scope is whose gathering an event is, the type
|
|
is what kind of gathering it is. A regional class matches both
|
|
{ scope: "regional" } and { type: "class" }.
|
|
|
|
The event_scopes rows 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.ts";
|
|
|
|
/* One shared empty list, so a memo keyed on `scopes` doesn't
|
|
restart on every render before the data arrives. */
|
|
const NO_SCOPES: any[] = [];
|
|
|
|
export function useEvents({ scope, host, status, type }: any = {}) {
|
|
const { data, error, loading } = useResource("/events");
|
|
|
|
const all = data?.events;
|
|
const scopes = data?.scopes ?? NO_SCOPES;
|
|
|
|
/* 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 (scope) list = list.filter(e => e.scope_id === scope);
|
|
// `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, scope, host, status, typeKey]);
|
|
|
|
return { events, scopes, 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: any[] = []) {
|
|
const upcoming: any[] = [];
|
|
const past: any[] = [];
|
|
|
|
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: any[] = [], declared: any[] = []) {
|
|
const seen = new Set(events.map(e => e.event_type));
|
|
return declared.filter(entry => seen.has(entry.id));
|
|
}
|