// 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 (
{day.label}