The anonymous surface an event was always for: GET /public/events, /public/events/:slug and /public/events/series/:slug, plus GET /player/events/history, and the four screens over them. Four org-lead decisions taken up front: split Phase 14 into 14a (website) and 14b (the app); add a `listed` flag rather than letting `state` mean both schedulable and announced; put the `events` capability string in the version block rather than publishing core as a pseudo-module; and drop "venue" from the spec rather than adding a field nothing had ever built. `listed` is announcement, not permission. Publishing is what makes a definition runnable, so without a separate flag a surprise event would have to be advertised in order to be allowed to happen. It is a column, a switch in Phase 13's editor, and three SQL predicates -- never a filter applied after a read, which works exactly as well until the first caller that forgets. The public shapes are a projection, and the projection is the security boundary: nothing is spread, so a column added to event_runs next year does not ride out through it. The spec, health, cleanup, claims, errors and member_key are all absent by construction. The six public event triggers gained `eventUrl` (version 1 -> 2), carrying ?run= because the page lives at the definition's slug while every trigger is about one occurrence. notify.event-started gained the button, at seedVersion 2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
79 lines
3.0 KiB
JavaScript
79 lines
3.0 KiB
JavaScript
// One arc (EVENTS.md §I, Phase 14a).
|
|
//
|
|
// **The arc is the thing the tooling this replaces could not express at all.**
|
|
// A WordPress calendar plugin has no series field, so "Royal Spy Mission → Risky
|
|
// Partner → Message From the Void" existed only in a GM's head and in whatever
|
|
// the forum post said. This page is that continuity, in the order an editor
|
|
// arranged it — which is why the events are numbered rather than dated: an arc
|
|
// has an order, and its parts may be months apart or run out of sequence.
|
|
|
|
import { useParams, Link } from 'react-router-dom'
|
|
import PublicLayout from '../../components/PublicLayout.jsx'
|
|
import PageHeader from '../../components/PageHeader.jsx'
|
|
import { Loading, ErrorState } from '../../components/PageState.jsx'
|
|
import { useAsync } from '../../lib/useAsync.js'
|
|
import { api } from '../../api/client.js'
|
|
|
|
export default function EventSeries() {
|
|
const { slug } = useParams()
|
|
const { loading, error, data } = useAsync(() => api.publicEventSeries(slug), [slug])
|
|
|
|
// Error first, then loading — the order Phase 13 had to fix on the admin
|
|
// editor, where a failed load sat behind a spinner that never stopped.
|
|
if (error) {
|
|
return (
|
|
<PublicLayout section="website">
|
|
<div className="shell-mid page-body">
|
|
<ErrorState message="That series could not be found." />
|
|
<p style={{ marginTop: 16 }}>
|
|
<Link to="/site/events">Back to the calendar</Link>
|
|
</p>
|
|
</div>
|
|
</PublicLayout>
|
|
)
|
|
}
|
|
if (loading || !data) {
|
|
return (
|
|
<PublicLayout section="website">
|
|
<div className="shell-mid page-body">
|
|
<Loading />
|
|
</div>
|
|
</PublicLayout>
|
|
)
|
|
}
|
|
|
|
const series = data.series
|
|
return (
|
|
<PublicLayout section="website">
|
|
<div className="shell-mid page-body">
|
|
<PageHeader eyebrow="Series" title={series.name} lead={series.description || ''} />
|
|
<ol style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: 14 }}>
|
|
{series.events.map((e, i) => (
|
|
<li key={e.slug}>
|
|
<Link to={`/site/events/${e.slug}`} style={{ textDecoration: 'none' }}>
|
|
<div className="panel" style={{ padding: '18px 22px', display: 'flex', gap: 18 }}>
|
|
<span
|
|
className="display"
|
|
style={{ color: 'var(--accent)', fontSize: '1.4rem', minWidth: 36, textAlign: 'right' }}
|
|
>
|
|
{i + 1}
|
|
</span>
|
|
<span>
|
|
<span className="display" style={{ fontSize: '1.15rem', color: 'var(--head)' }}>
|
|
{e.title}
|
|
</span>
|
|
{e.summary && <p style={{ margin: '6px 0 0', color: 'var(--text)' }}>{e.summary}</p>}
|
|
</span>
|
|
</div>
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
<p className="sans" style={{ marginTop: 24 }}>
|
|
<Link to="/site/events">Back to the calendar</Link>
|
|
</p>
|
|
</div>
|
|
</PublicLayout>
|
|
)
|
|
}
|