Add recurring series to events

A Series checkbox under When opens a panel for the schedule:
weekly on chosen weekdays, monthly by date, or monthly by weekday
position, every N weeks or months, with meeting times and an
optional meeting count. starts_on anchors the schedule and ends_on
bounds it, so effective_status needs no change.

Occurrences are derived, not stored. src/lib/eventSeries.ts builds
the schedule label shown on cards and the event page, and the
upcoming dates listed on the event page.

Migration 016 adds the columns; the CRUD engine gains a time type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Zaldimmar 2026-09-25 04:46:49 -05:00
parent aebc1c302b
commit 5ed7994a64
11 changed files with 500 additions and 3 deletions

View file

@ -40,6 +40,7 @@ const int = (name, opts = {}) => ({ name, type: "int", ...opts });
const real = (name, opts = {}) => ({ name, type: "real", ...opts });
const bool = (name, opts = {}) => ({ name, type: "bool", ...opts });
const date = (name, opts = {}) => ({ name, type: "date", ...opts });
const time = (name, opts = {}) => ({ name, type: "time", ...opts });
const enumeration = (name, values, opts = {}) => ({
name,
type: "enum",
@ -281,6 +282,10 @@ const organizations = {
/* ── Events ──────────────────────────────────────────────────── */
/* Column suffixes for the series weekday flags, Sunday first to
match Date#getDay. */
const SERIES_WEEKDAYS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
const events = {
key: "events",
table: "events",
@ -339,6 +344,18 @@ const events = {
bool("is_published"),
int("sort_order"),
bool("in_timeline"),
// A repeating schedule. Columns rather than a side table: the
// schedule is always exactly one per event, and the public view
// is SELECT e.*, so it reaches the site with no join. Ignored
// while is_series is 0. See migration 016 for what each means.
bool("is_series"),
enumeration("series_frequency", ["weekly", "monthly_date", "monthly_weekday"]),
int("series_interval"),
...SERIES_WEEKDAYS.map((day) => bool(`series_${day}`)),
time("series_start_time"),
time("series_end_time"),
int("series_count"),
],
extensions: [timelineExtension("event")],