// The public event calendar (EVENTS.md §I, Phase 14a). // // **A list, not a month grid.** The admin calendar draws a grid because an // operator's question is "what does this month look like" — coverage, clashes, // the gap on the third weekend. A visitor's question is "what is on, and when is // the next one", which a chronological list answers in one glance and a grid // answers by making them count squares. Same data, different question. // // **A projection is drawn differently from a run, and the reason is the // operator's reason one tier along.** Past the materialisation horizon there is // no row: nothing is committed to, nothing can be cancelled, and a forecast // rendered identically to a booking would be the page promising something the // server has not. It is dashed and labelled "expected". // // The date heading is the READER's day and the time beside each entry is the // EVENT's own zone. That split is §I's: the shard's evening is what "8pm" means // to everyone reading it, but "this month" is the month the reader is living in. import { Link } from 'react-router-dom' import PublicLayout from '../../components/PublicLayout.jsx' import PageHeader from '../../components/PageHeader.jsx' import { Loading, ErrorState, EmptyState } from '../../components/PageState.jsx' import { useAsync } from '../../lib/useAsync.js' import { api } from '../../api/client.js' import { eventTime, readerDayLabel, statusWord } from '../../lib/eventCalendar.js' export default function Events() { const { loading, error, data } = useAsync(() => api.publicEvents()) const entries = data?.entries || [] // Grouped by the reader's own day, in order. The server already sorted by // instant, so this preserves that order rather than re-sorting. const days = [] for (const entry of entries) { const label = readerDayLabel(entry.scheduledFor) const last = days[days.length - 1] if (last && last.label === label) last.entries.push(entry) else days.push({ label, entries: [entry] }) } return (
{loading && } {error && } {!loading && !error && entries.length === 0 && ( Nothing on the calendar just yet — check back soon. )} {days.map((day) => (

{day.label}

{day.entries.map((entry) => ( ))}
))}
) } function EventRow({ entry }) { const projected = entry.kind === 'projected' const body = (
{eventTime(entry.scheduledFor, entry.timezone)} {entry.title} {entry.seriesName && ( {entry.seriesName} )} {projected ? 'Expected' : statusWord(entry.status, entry.scheduledFor)}
) // A projection has no page of its own worth linking to any differently — the // event page IS the definition's — so both link to the same place. It is the // OCCURRENCE that does not exist yet, not the event. return {body} }