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:
parent
aebc1c302b
commit
5ed7994a64
11 changed files with 500 additions and 3 deletions
|
|
@ -31,6 +31,7 @@ import {
|
|||
import { awardHref, personHref, refHref } from '../lib/hrefs.ts'
|
||||
import { personPhoto } from '../lib/media.ts'
|
||||
import { eventTypeLabel } from '../lib/eventTypes.ts'
|
||||
import { occurrenceLabel, seriesLabel, seriesTimes, upcomingOccurrences } from '../lib/eventSeries.ts'
|
||||
|
||||
const TEAL = '#138ba0'
|
||||
const BODY = '#4a6b72'
|
||||
|
|
@ -127,6 +128,46 @@ export default function EventDetail() {
|
|||
},
|
||||
]
|
||||
|
||||
// A cancelled series has no next meeting, whatever its dates say.
|
||||
const upcoming =
|
||||
event.status === 'cancelled'
|
||||
? []
|
||||
: upcomingOccurrences(event.series, event.starts_on, event.ends_on)
|
||||
|
||||
if (upcoming.length > 0) {
|
||||
const times = seriesTimes(event.series)
|
||||
|
||||
sections.push({
|
||||
id: 'dates',
|
||||
title: 'Upcoming dates',
|
||||
blurb: seriesLabel(event.series, event.starts_on) ?? undefined,
|
||||
accent,
|
||||
background: '#eef9fb',
|
||||
content: (
|
||||
<div className="max-w-6xl mx-auto px-6">
|
||||
<ul className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{upcoming.map((date) => (
|
||||
<li
|
||||
key={date}
|
||||
className="rounded-xl border-l-4 bg-white px-5 py-3"
|
||||
style={{ borderColor: accent }}
|
||||
>
|
||||
<p className="font-semibold" style={{ color: accent }}>
|
||||
{occurrenceLabel(date)}
|
||||
</p>
|
||||
{times && (
|
||||
<p className="text-sm" style={{ color: BODY }}>
|
||||
{times}
|
||||
</p>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
if (groups.length > 0) {
|
||||
sections.push({
|
||||
id: 'people',
|
||||
|
|
@ -197,7 +238,12 @@ function Facts({ event, accent }: { event: EventRecord; accent: string }) {
|
|||
[event.locality, event.state_code].filter(Boolean).join(', ') ||
|
||||
(event.is_online ? 'Online' : null)
|
||||
|
||||
const when = event.date_label || dateRange(event.starts_on, event.ends_on)
|
||||
const schedule = seriesLabel(event.series, event.starts_on)
|
||||
// A series with no label of its own is described by its schedule
|
||||
// rather than by a start-to-end range that reads like one long
|
||||
// gathering.
|
||||
const when =
|
||||
event.date_label || (schedule ? null : dateRange(event.starts_on, event.ends_on))
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-x-6 gap-y-3 text-sm">
|
||||
|
|
@ -227,6 +273,7 @@ function Facts({ event, accent }: { event: EventRecord; accent: string }) {
|
|||
)}
|
||||
|
||||
{when && <span style={{ color: BODY }}>{when}</span>}
|
||||
{schedule && <span style={{ color: BODY }}>{schedule}</span>}
|
||||
{where && <span style={{ color: BODY }}>{where}</span>}
|
||||
{event.is_online && where !== 'Online' && (
|
||||
<span style={{ color: BODY }}>Online too</span>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue