NGU-Web/src/data/eventData.ts
Zaldimmar 25e592bfea Clean up the schema: drop unused tables, rename scopes, publishable awards
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>
2026-09-26 17:09:58 -05:00

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));
}