@acms-subir/core 0.3.47 → 0.3.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/cli/migrations.generated.mjs +42 -23
- package/docs/calendar.md +80 -0
- package/docs/server.md +28 -3
- package/docs/theme-authoring.md +1 -1
- package/package.json +1 -1
- package/src/actions/index.ts +76 -11
- package/src/components/EventPage.astro +18 -12
- package/src/components/calendar/CalendarBoard.tsx +43 -19
- package/src/components/editor/event/EventSheet.tsx +2 -0
- package/src/components/editor/event/SingleDates.tsx +123 -0
- package/src/lib/booking/holds.ts +7 -7
- package/src/lib/calendar/api.ts +11 -1
- package/src/lib/calendar/board.ts +7 -3
- package/src/lib/calendar/cards.ts +5 -5
- package/src/lib/calendar/change-migration.ts +40 -15
- package/src/lib/calendar/data.ts +9 -3
- package/src/lib/calendar/event-words.ts +9 -0
- package/src/lib/calendar/events.ts +94 -9
- package/src/lib/calendar/ics-write.ts +25 -2
- package/src/lib/calendar/moves.ts +291 -0
- package/src/lib/calendar/render.ts +10 -6
- package/src/lib/calendar/schedule.ts +73 -61
- package/src/lib/email/booking-words.ts +7 -1
- package/src/lib/email/service.ts +6 -1
- package/src/lib/email/templates.ts +6 -2
- package/src/lib/mobile/calendar.ts +5 -2
- package/src/lib/mobile/schemas.ts +7 -3
- package/src/lib/push/service.ts +44 -10
- package/src/lib/push/types.ts +2 -0
- package/src/lib/repo/migrations.ts +5 -2
- package/src/lib/repo/sql.ts +28 -10
- package/src/lib/repo/types.ts +6 -3
- package/src/lib/schema.ts +21 -0
- package/src/lib/sections.ts +5 -2
- package/src/lib/sync/catalog.ts +1 -0
- package/src/pages/api/calendar/events/index.ts +6 -0
- package/src/pages/members/_Layout.astro +1 -0
- package/src/pages/members/events/index.astro +1 -1
- package/src/server/index.ts +33 -0
- package/src/styles/editor.css +19 -0
- package/src/styles/schedule.css +2 -0
- package/src/styles/site.css +6 -0
- package/src/theme-sdk/calendar.ts +3 -2
package/src/lib/repo/types.ts
CHANGED
|
@@ -48,7 +48,7 @@ export type EventUpdate = Partial<
|
|
|
48
48
|
CalEvent,
|
|
49
49
|
| 'title' | 'slug' | 'bodyJson' | 'bodyHtml' | 'excerpt' | 'cover' | 'coverAlt'
|
|
50
50
|
| 'seoTitle' | 'seoDesc' | 'status' | 'featured' | 'tags' | 'template'
|
|
51
|
-
| 'start' | 'end' | 'allDay' | 'timezone' | 'rrule' | 'exdates' | 'cancelled' | 'cancelledDates'
|
|
51
|
+
| 'start' | 'end' | 'allDay' | 'timezone' | 'rrule' | 'exdates' | 'cancelled' | 'cancelledDates' | 'movedDates'
|
|
52
52
|
| 'location' | 'cta' | 'calendarIds' | 'fields' | 'sourceUid' | 'rsvp' | 'translations' | 'roomHold'
|
|
53
53
|
| 'publishedAt' | 'scheduledAt'
|
|
54
54
|
>
|
|
@@ -168,15 +168,18 @@ export interface ContentRepository {
|
|
|
168
168
|
discardEventDraft(id: string): Promise<void>;
|
|
169
169
|
/** replace an event's cancelled occurrence keys at once, keeping any staged draft */
|
|
170
170
|
setEventCancelledDates(id: string, dates: string[]): Promise<void>;
|
|
171
|
+
/** replace an event's moved dates (and cancelled dates, when given) at once, keeping any staged draft; quiet: no member push for it */
|
|
172
|
+
setEventMovedDates(id: string, moved: CalEvent['movedDates'], cancelledDates?: string[], opts?: { quiet?: boolean }): Promise<void>;
|
|
171
173
|
/** Cancels the RSVPs for one date of an event (they get the "cancelled" email). Returns how many. */
|
|
172
174
|
cancelOccurrenceRsvps(id: string, start: number): Promise<number>;
|
|
173
175
|
/**
|
|
174
176
|
* Moves the RSVPs for one date of an event (those starting at `start`) to a
|
|
175
177
|
* new time, and to another event when `to.eventId` is set. Pending and
|
|
176
|
-
* confirmed ones get the "new time" email
|
|
178
|
+
* confirmed ones get the "new time" email (with `message`, when given);
|
|
179
|
+
* places in line move quietly.
|
|
177
180
|
* Returns how many moved.
|
|
178
181
|
*/
|
|
179
|
-
moveOccurrenceRsvps(id: string, start: number, to: { eventId?: string; start: number; end: number }): Promise<number>;
|
|
182
|
+
moveOccurrenceRsvps(id: string, start: number, to: { eventId?: string; start: number; end: number }, message?: string): Promise<number>;
|
|
180
183
|
deleteEvent(id: string): Promise<void>;
|
|
181
184
|
// saved-place sync (lib/maps/places.ts): rewrite just the location of the
|
|
182
185
|
// live row and/or the staged draft, leaving every other field — and the
|
package/src/lib/schema.ts
CHANGED
|
@@ -357,6 +357,24 @@ export const RoomHold = z.object({
|
|
|
357
357
|
});
|
|
358
358
|
export type RoomHold = z.infer<typeof RoomHold>;
|
|
359
359
|
|
|
360
|
+
// One date of a repeating event at another time (and maybe in other rooms or
|
|
361
|
+
// another place): CalEvent.movedDates, keyed by the date's ORIGINAL occurrence
|
|
362
|
+
// key. start/end are wall-clock in the event's zone like the event's own
|
|
363
|
+
// (dates for all-day events, end inclusive). Unset roomIds/location = the
|
|
364
|
+
// event's. note is public ("Moved for the youth retreat"); by is who moved it
|
|
365
|
+
// (a user id, or 'google-sync'), at when. lib/calendar/moves.ts writes these.
|
|
366
|
+
export const MovedDate = z.object({
|
|
367
|
+
start: z.string(),
|
|
368
|
+
end: z.string(),
|
|
369
|
+
roomIds: z.array(z.string()).optional(),
|
|
370
|
+
location: EventLocation.optional(),
|
|
371
|
+
note: z.string().max(500).default(''),
|
|
372
|
+
by: z.string().default(''),
|
|
373
|
+
at: z.number().default(0),
|
|
374
|
+
});
|
|
375
|
+
export type MovedDate = z.infer<typeof MovedDate>;
|
|
376
|
+
export const MovedDates = z.record(z.string(), MovedDate).default({});
|
|
377
|
+
|
|
360
378
|
// A member's event request (lib/calendar/requests.ts): a new event waiting
|
|
361
379
|
// for approval (kind 'new', the event stays a draft), or changes to a live
|
|
362
380
|
// event staged in its `draft` (kind 'changes'). Staff approve or decline;
|
|
@@ -407,6 +425,9 @@ export const CalEvent = z.object({
|
|
|
407
425
|
// cancelled: shown struck through, but they don't hold rooms. Keys that no
|
|
408
426
|
// longer match an occurrence (the series moved) are ignored.
|
|
409
427
|
cancelledDates: z.array(z.string()).default([]),
|
|
428
|
+
// single dates at another time (MovedDate above); a date is moved or
|
|
429
|
+
// cancelled, never both. Keys that no longer match an occurrence are ignored.
|
|
430
|
+
movedDates: MovedDates,
|
|
410
431
|
location: EventLocation.default({}),
|
|
411
432
|
cta: EventCta.default({}),
|
|
412
433
|
calendarIds: z.array(z.string()).default([]),
|
package/src/lib/sections.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface LinkValue { label: string; href: string; external: boolean }
|
|
|
32
32
|
export interface EventItem {
|
|
33
33
|
title: string; url: string; summary: string; image: string; imageAlt: string; location: string;
|
|
34
34
|
start: string; end: string; allDay: boolean; cancelled: boolean;
|
|
35
|
+
/** this date was moved here: from "Thu, Oct 1 · 6:00 PM" (where it was), with staff's public note; null when not moved */
|
|
36
|
+
moved: { from: string; note: string } | null;
|
|
35
37
|
/** "Sat, Oct 3 · 11:00 AM" */ when: string; /** "Saturday, October 3, 2026" */ date: string;
|
|
36
38
|
/** "11:00 AM – 1:00 PM EDT" */ time: string; month: string; day: string; weekday: string;
|
|
37
39
|
cta: { label: string; href: string } | null;
|
|
@@ -141,11 +143,12 @@ async function resolveSource(field: Extract<FieldDef, { type: 'source' }>, value
|
|
|
141
143
|
return (await upcomingEvents(repo, settings, now, { calendarIds, limit, tag: cfg.tag, excludeTag: cfg.excludeTag })).map((o): EventItem => {
|
|
142
144
|
const e = lang ? localizeDoc(o.event, lang) : o.event;
|
|
143
145
|
const f = formatOccurrence(o, tz, locale);
|
|
146
|
+
const was = o.moved ? formatOccurrence({ ...o, start: o.moved.from.start, end: o.moved.from.end }, tz, locale) : null;
|
|
144
147
|
const ctaHref = safeUrl(e.cta.href);
|
|
145
148
|
return {
|
|
146
149
|
title: e.title || 'Untitled event', url: lp(eventUrl(e.slug)), summary: e.excerpt, image: safeUrl(e.cover), imageAlt: e.coverAlt,
|
|
147
|
-
location: locationLine(e.location), start: new Date(o.start).toISOString(), end: new Date(o.end).toISOString(),
|
|
148
|
-
allDay: o.allDay, cancelled: o.cancelled, when: f.range, date: f.date, time: f.time, month: f.month, day: f.day, weekday: f.weekday,
|
|
150
|
+
location: locationLine(o.moved?.location ?? e.location), start: new Date(o.start).toISOString(), end: new Date(o.end).toISOString(),
|
|
151
|
+
allDay: o.allDay, cancelled: o.cancelled, moved: was && o.moved ? { from: o.allDay ? was.date : was.range, note: o.moved.note } : null, when: f.range, date: f.date, time: f.time, month: f.month, day: f.day, weekday: f.weekday,
|
|
149
152
|
cta: e.cta.label && ctaHref && !o.cancelled ? { label: e.cta.label, href: ctaHref } : null,
|
|
150
153
|
};
|
|
151
154
|
});
|
package/src/lib/sync/catalog.ts
CHANGED
|
@@ -100,6 +100,12 @@ export const GET: APIRoute = async ({ request, locals, url }) => {
|
|
|
100
100
|
end: o.allDay ? isoWithOffset(o.end - 86_400_000, etz).slice(0, 10) : isoWithOffset(o.end, etz),
|
|
101
101
|
allDay: o.allDay,
|
|
102
102
|
cancelled: o.cancelled,
|
|
103
|
+
// the date's key (a moved date keeps its original one), and where it moved from
|
|
104
|
+
key: o.key,
|
|
105
|
+
moved: o.moved ? {
|
|
106
|
+
from: { start: o.allDay ? isoWithOffset(o.moved.from.start, etz).slice(0, 10) : isoWithOffset(o.moved.from.start, etz), end: o.allDay ? isoWithOffset(o.moved.from.end - 86_400_000, etz).slice(0, 10) : isoWithOffset(o.moved.from.end, etz) },
|
|
107
|
+
note: o.moved.note, location: o.moved.location ?? null,
|
|
108
|
+
} : null,
|
|
103
109
|
// that date's rooms
|
|
104
110
|
rooms: roomsToJson(o.roomIds, resources, full),
|
|
105
111
|
event: toJson(o.event.id),
|
|
@@ -62,6 +62,7 @@ const { title, user } = Astro.props;
|
|
|
62
62
|
.pc-mem-dates li { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 42px; padding: 5px 0; border-top: 1px solid var(--line); font-variant-numeric: tabular-nums; }
|
|
63
63
|
.pc-mem-dates li.is-cancelled > span { color: var(--muted); }
|
|
64
64
|
.pc-mem-dates em { font-style: normal; font-size: 12.5px; font-weight: 600; color: var(--danger); margin-left: 8px; }
|
|
65
|
+
.pc-mem-dates em.is-moved { color: var(--accent); font-weight: 500; }
|
|
65
66
|
.pc-mem-past > summary { cursor: pointer; font-weight: 600; font-size: 15px; }
|
|
66
67
|
.pc-mem-past .pc-mem-list { margin-top: 10px; }
|
|
67
68
|
/* the request form */
|
|
@@ -175,7 +175,7 @@ const pastShown = past.slice(0, 10);
|
|
|
175
175
|
const off = e.cancelledDates.includes(o.key);
|
|
176
176
|
return (
|
|
177
177
|
<li class={o.cancelled ? 'is-cancelled' : undefined}>
|
|
178
|
-
<span>{o.cancelled ? <s>{when(o)}</s> : when(o)}{o.cancelled && <em>Cancelled</em>}</span>
|
|
178
|
+
<span>{o.cancelled ? <s>{when(o)}</s> : when(o)}{o.cancelled && <em>Cancelled</em>}{o.moved && !o.cancelled && <em class="is-moved" title={o.moved.note || undefined}>Moved from {when({ ...o, start: o.moved.from.start, end: o.moved.from.end })}</em>}</span>
|
|
179
179
|
{e.rrule && !e.cancelled && o.start > now && (
|
|
180
180
|
<form method="post" onsubmit={off ? undefined : "return confirm('Cancel this date? It shows as cancelled straight away.')"}>
|
|
181
181
|
<input type="hidden" name="op" value={off ? 'restore-date' : 'cancel-date'} />
|
package/src/server/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ export interface ServerUser {
|
|
|
37
37
|
export type { CalEvent, Occurrence };
|
|
38
38
|
export type { EventRequestInput } from '../lib/calendar/requests';
|
|
39
39
|
export type { EventClash, BusySlot, Hold } from '../lib/booking/holds';
|
|
40
|
+
export type { MoveInput, MoveResult } from '../lib/calendar/moves';
|
|
40
41
|
|
|
41
42
|
export interface ListEventsOptions {
|
|
42
43
|
/** only published events (default: every event except archived ones) */
|
|
@@ -172,6 +173,38 @@ export async function restoreOccurrence(repo: Repo, eventId: string, occurrenceK
|
|
|
172
173
|
return restore(internal(repo), eventId, occurrenceKey, { url: context.url, locals: context.locals });
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
// ---- moved dates -------------------------------------------------------------
|
|
177
|
+
// One date of a repeating event at another time (and maybe other rooms or
|
|
178
|
+
// another place), keeping its key, its RSVPs (moved with it; each person gets
|
|
179
|
+
// the "new time" email with their cancel link) and its hosts. These check who
|
|
180
|
+
// is acting: staff ("Create and edit content") and GOOGLE_SYNC may move any
|
|
181
|
+
// date, and a room taken at the new time comes back in `clashes` as a
|
|
182
|
+
// warning; a host of the event may move its dates only while the rooms are
|
|
183
|
+
// free (409 otherwise), and staff are emailed about it.
|
|
184
|
+
|
|
185
|
+
/** The actor for a calendar sync acting with staff rights (a change already made elsewhere): clashes only warn. */
|
|
186
|
+
export const GOOGLE_SYNC = 'google-sync';
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Move one date (by its occurrence key) to `input.start` (and `end`, else it
|
|
190
|
+
* keeps its length; wall-clock in the event's timezone, like `start`).
|
|
191
|
+
* `roomIds`/`location`/`note` replace that date's own (omit to keep them).
|
|
192
|
+
* Applies at once, even on a published event. `quiet` skips the app notice
|
|
193
|
+
* and the staff email (a sync catching up); RSVP emails still go. Errors have
|
|
194
|
+
* a `status`: 400 (not a repeating event, not one of its dates, started or a
|
|
195
|
+
* past time), 401, 404, 409 (a cancelled date, or a taken room for a host).
|
|
196
|
+
*/
|
|
197
|
+
export async function moveOccurrence(repo: Repo, who: Who | typeof GOOGLE_SYNC, eventId: string, occurrenceKey: string, input: import('../lib/calendar/moves').MoveInput, context: WriteContext, opts: { quiet?: boolean } = {}): Promise<import('../lib/calendar/moves').MoveResult> {
|
|
198
|
+
const { moveOccurrence: move } = await import('../lib/calendar/moves');
|
|
199
|
+
return move(internal(repo), who, eventId, occurrenceKey, input, { url: context.url, locals: context.locals }, opts);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Put a moved date back where the series has it. Same rules; idempotent. */
|
|
203
|
+
export async function moveOccurrenceBack(repo: Repo, who: Who | typeof GOOGLE_SYNC, eventId: string, occurrenceKey: string, context: WriteContext, opts: { quiet?: boolean } = {}): Promise<import('../lib/calendar/moves').MoveResult> {
|
|
204
|
+
const { moveOccurrenceBack: back } = await import('../lib/calendar/moves');
|
|
205
|
+
return back(internal(repo), who, eventId, occurrenceKey, { url: context.url, locals: context.locals }, opts);
|
|
206
|
+
}
|
|
207
|
+
|
|
175
208
|
// ---- rooms -----------------------------------------------------------------
|
|
176
209
|
// Events hold rooms (CalEvent.roomHold) while they happen. These find where
|
|
177
210
|
// two things want the same room. They don't check permissions, and their
|
package/src/styles/editor.css
CHANGED
|
@@ -1328,3 +1328,22 @@ html.pe-evr-lock { overflow: hidden; }
|
|
|
1328
1328
|
.pe-evs-wrap { --evs-pad: 14px; padding-top: 18px; }
|
|
1329
1329
|
.pe-evsec input:where(:not([type='checkbox'], [type='radio'])), .pe-evsec select, .pe-evsec textarea, .pe-loc-search input, .pe-evsec input.mono { font-size: 16px; }
|
|
1330
1330
|
}
|
|
1331
|
+
|
|
1332
|
+
/* Event settings → Single dates (SingleDates.tsx): move or cancel one date */
|
|
1333
|
+
.pe-evd { list-style: none; margin: 0; padding: 0; }
|
|
1334
|
+
.pe-evd-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px 12px; padding: 8px 0; border-top: 1px solid var(--line); }
|
|
1335
|
+
.pe-evd-row:first-child { border-top: 0; }
|
|
1336
|
+
.pe-evd-main { display: flex; flex-direction: column; min-width: 0; font-size: 13.5px; font-variant-numeric: tabular-nums; }
|
|
1337
|
+
.pe-evd-state { font-size: 12px; color: var(--text-2); }
|
|
1338
|
+
.pe-evd-row.is-cancelled .pe-evd-when { color: var(--muted); }
|
|
1339
|
+
.pe-evd-row.is-cancelled .pe-evd-state { color: var(--red); }
|
|
1340
|
+
.pe-evd-row.is-moved .pe-evd-state { color: var(--accent); }
|
|
1341
|
+
.pe-evd-acts { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
1342
|
+
.pe-evd-form { flex-basis: 100%; display: flex; flex-direction: column; gap: 10px; margin: 4px 0 6px; padding: 12px; border-radius: 10px; background: var(--panel-2, var(--soft)); }
|
|
1343
|
+
.pe-evd-fields { display: flex; flex-wrap: wrap; gap: 10px; }
|
|
1344
|
+
.pe-evd-fields .pe-field { flex: 1 1 130px; margin: 0; }
|
|
1345
|
+
.pe-evd-rooms { border: 0; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: 13px; }
|
|
1346
|
+
.pe-evd-rooms legend { font-size: 12px; color: var(--text-2); margin-bottom: 4px; padding: 0; }
|
|
1347
|
+
.pe-evd-room { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
|
|
1348
|
+
.pe-evd-formacts { display: flex; gap: 8px; }
|
|
1349
|
+
.pe-ev-note.is-error { color: var(--red); }
|
package/src/styles/schedule.css
CHANGED
|
@@ -205,6 +205,8 @@ kbd { font: 10.5px ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--
|
|
|
205
205
|
.sch-toast button { border: 0; background: none; color: var(--accent); font: inherit; font-weight: 650; cursor: pointer; padding: 0; }
|
|
206
206
|
:root[data-theme='dark'] .sch-toast { background: #2e2b27; color: var(--text); border: 1px solid var(--line-strong); }
|
|
207
207
|
.sch-toast.err { background: var(--red); color: #fff; }
|
|
208
|
+
/* a room clash after a move: both events keep the room */
|
|
209
|
+
.sch-toast-warn { display: block; max-width: 340px; font-size: 12px; color: var(--amber, #c98a16); }
|
|
208
210
|
|
|
209
211
|
/* ---- quick add ---- */
|
|
210
212
|
.qa-layer { position: fixed; inset: 0; z-index: 75; }
|
package/src/styles/site.css
CHANGED
|
@@ -806,6 +806,10 @@ li::marker { color: var(--muted); }
|
|
|
806
806
|
@layer base {
|
|
807
807
|
.pc-cal-event.is-cancelled b, .pc-cal-row.is-cancelled .ti { text-decoration: line-through; color: var(--cal-muted); }
|
|
808
808
|
.pc-cal-pop-flag { display: inline-block; font-size: 11px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--danger); margin-bottom: 3px; }
|
|
809
|
+
/* a moved date: flagged, never struck through */
|
|
810
|
+
.pc-cal-pop-flag.is-moved, .pc-ev-flag.is-moved { color: var(--accent); }
|
|
811
|
+
.pc-cal-pop-moved { font-size: 12.5px; color: var(--cal-muted); margin-top: 2px; }
|
|
812
|
+
.pc-cal-event.is-moved b::before { content: '↷ '; }
|
|
809
813
|
.pc-cal-pop-link { display: inline-block; margin-top: 9px; font-weight: 600; font-size: 12.5px; color: var(--cal-accent); text-decoration: none; }
|
|
810
814
|
.pc-cal-pop-link:hover { text-decoration: underline; }
|
|
811
815
|
|
|
@@ -867,6 +871,8 @@ li::marker { color: var(--muted); }
|
|
|
867
871
|
.pc-ev-page.is-cancelled .pc-ev-title { text-decoration: line-through; text-decoration-thickness: 2px; color: var(--muted); }
|
|
868
872
|
.pc-ev-cancelled { background: color-mix(in srgb, var(--danger) 10%, var(--panel)); border-radius: var(--r-sm); padding: 10px 14px; font-size: 14.5px; margin: 0 0 16px; }
|
|
869
873
|
.pc-ev-cancelled b { color: var(--danger); }
|
|
874
|
+
.pc-ev-moved { background: color-mix(in srgb, var(--accent) 10%, var(--panel)); border-radius: var(--r-sm); padding: 10px 14px; font-size: 14.5px; margin: 0 0 16px; }
|
|
875
|
+
.pc-ev-moved b { color: var(--accent); }
|
|
870
876
|
.pc-ev-lede { font-size: 19px; line-height: 1.55; color: var(--muted); margin: 0 0 22px; }
|
|
871
877
|
.pc-ev-side {
|
|
872
878
|
position: sticky; top: 96px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg);
|
|
@@ -29,9 +29,10 @@ export interface UpcomingEventsOptions {
|
|
|
29
29
|
/**
|
|
30
30
|
* Occurrence start/end are Unix milliseconds; event retains its authored
|
|
31
31
|
* fields except private custom fields. `cancelled` is true when the whole
|
|
32
|
-
* event or just this date is cancelled.
|
|
32
|
+
* event or just this date is cancelled. `moved` is set when this date of a
|
|
33
|
+
* series was moved here: `from` (its old start and end) and staff's `note`.
|
|
33
34
|
*/
|
|
34
|
-
export type UpcomingEvent = Pick<Occurrence, 'event' | 'start' | 'end' | 'allDay' | 'days' | 'cancelled'>;
|
|
35
|
+
export type UpcomingEvent = Pick<Occurrence, 'event' | 'start' | 'end' | 'allDay' | 'days' | 'cancelled'> & Pick<Partial<Occurrence>, 'moved'>;
|
|
35
36
|
|
|
36
37
|
/** Server-side helper for Astro templates and endpoints. No repository access needed. */
|
|
37
38
|
export async function upcomingEvents(
|