Add a reusable event calendar and put it on the front page

EventCalendar shows events on a month grid or as a list of the
month. Multi-day events are lane-packed bars that break at week
edges, series events appear on every meeting with their time, and
clicking a day lists everything on it. Phones get the list. Visitors
can narrow by scope, type, online only and search, all starting at
"all"; a page can pin section, host or type through props, which
hides that control.

Migration 019 rebuilds front_page_sections to allow a 'calendar'
band and slots it in after the retreats carousel, so it can be
reordered, retitled or hidden from the Front page editor.

useEvents drops its empty fallback so a failed request surfaces as
an error rather than an empty list, and returns the scope list
alongside the events for the calendar's scope filter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Zaldimmar 2026-09-25 06:28:42 -05:00
parent 641ab167b0
commit 2e125b4629
10 changed files with 947 additions and 8 deletions

View file

@ -716,7 +716,7 @@ const frontPage = {
columns: [
enumeration(
"section",
["countdown", "retreats", "stats", "timeline", "connect"],
["countdown", "retreats", "calendar", "stats", "timeline", "connect"],
{ required: true },
),
text("title"),

View file

@ -0,0 +1,61 @@
-- ═══════════════════════════════════════════════════════════════
-- FRONT PAGE: calendar band
--
-- Adds 'calendar' to the sections the front page can draw. The key
-- is a CHECK, and SQLite can't alter a CHECK in place, so the table
-- is rebuilt: new table, copy, drop, rename.
--
-- No PRAGMA foreign_keys dance. front_page_sections only points out
-- (at front_page); nothing points in, so dropping the old table
-- cascades into nothing, and the copy keeps every page_id valid.
--
-- The new band is inserted straight after the retreats carousel,
-- where "what's on" reads naturally, by shifting everything below it
-- down one. If retreats was removed on this box, it goes last.
--
-- Adding another section later is the same three steps: this CHECK,
-- the enum in both descriptor halves, and SECTIONS in Home.tsx.
--
-- No BEGIN...END in this file.
-- ═══════════════════════════════════════════════════════════════
CREATE TABLE front_page_sections_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
page_id TEXT NOT NULL REFERENCES front_page (id) ON DELETE CASCADE,
sort_order INTEGER NOT NULL DEFAULT 0,
section TEXT NOT NULL
CHECK (section IN ('countdown', 'retreats', 'calendar', 'stats',
'timeline', 'connect')),
title TEXT,
blurb TEXT,
is_hidden INTEGER NOT NULL DEFAULT 0 CHECK (is_hidden IN (0, 1)),
UNIQUE (page_id, section)
) STRICT;
INSERT INTO front_page_sections_new (id, page_id, sort_order, section, title, blurb, is_hidden)
SELECT id, page_id, sort_order, section, title, blurb, is_hidden
FROM front_page_sections;
DROP TABLE front_page_sections;
ALTER TABLE front_page_sections_new RENAME TO front_page_sections;
UPDATE front_page_sections
SET sort_order = sort_order + 1
WHERE page_id = 'home'
AND sort_order > COALESCE(
(SELECT sort_order FROM front_page_sections
WHERE page_id = 'home' AND section = 'retreats'),
(SELECT MAX(sort_order) FROM front_page_sections WHERE page_id = 'home'));
INSERT INTO front_page_sections (page_id, sort_order, section, title, blurb)
SELECT 'home',
COALESCE(
(SELECT sort_order + 1 FROM front_page_sections
WHERE page_id = 'home' AND section = 'retreats'),
(SELECT COALESCE(MAX(sort_order), -1) + 1 FROM front_page_sections
WHERE page_id = 'home')),
'calendar',
'What''s on',
'Every gathering, class and meeting in one place.'
WHERE EXISTS (SELECT 1 FROM front_page WHERE id = 'home');