@acms-subir/core 0.3.38 → 0.3.39
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 +11 -0
- package/docs/mobile.md +1 -1
- package/mobile-template/recipes/events/files/components/RsvpPanel.tsx +11 -7
- package/mobile-template/recipes/rsvp/files/components/RsvpPanel.tsx +11 -7
- package/package.json +1 -1
- package/src/actions/index.ts +2 -1
- package/src/components/EventPage.astro +14 -5
- package/src/components/MobileApp.tsx +44 -23
- package/src/components/editor/EventPanel.tsx +2 -1
- package/src/components/editor/PostEditor.tsx +1 -1
- package/src/components/editor/event/RegistrationCard.tsx +51 -1
- package/src/components/editor/event/registration.ts +18 -1
- package/src/lib/booking/emails.ts +3 -1
- package/src/lib/booking/service.ts +37 -10
- package/src/lib/calendar/events.ts +11 -0
- package/src/lib/calendar/rsvp-words.ts +31 -2
- package/src/lib/content-service.ts +26 -14
- package/src/lib/drafts.ts +2 -0
- package/src/lib/editor/documents.ts +3 -1
- package/src/lib/editor/schemas.ts +1 -1
- package/src/lib/email/booking-words.ts +10 -1
- package/src/lib/mobile/bookings.ts +1 -1
- package/src/lib/mobile/http.ts +1 -1
- package/src/lib/mobile/openapi.ts +1 -1
- package/src/lib/mobile/rsvp.ts +14 -8
- package/src/lib/mobile/schemas.ts +5 -2
- package/src/lib/push/api.ts +7 -8
- package/src/lib/push/service.ts +21 -16
- package/src/lib/push/types.ts +18 -8
- package/src/lib/repo/sql.ts +3 -0
- package/src/lib/repo/types.ts +3 -2
- package/src/lib/schema.ts +5 -1
- package/src/pages/events/[slug].astro +10 -3
- package/src/styles/editor.css +4 -0
- package/src/styles/mobile-app.css +7 -0
- package/src/styles/site.css +3 -0
- package/src/theme-sdk/index.ts +1 -1
package/src/lib/repo/types.ts
CHANGED
|
@@ -187,7 +187,8 @@ export interface ContentRepository {
|
|
|
187
187
|
|
|
188
188
|
// bookings (+ staff blocks and event RSVPs). createBooking is the only
|
|
189
189
|
// write that can fail on a race: it inserts atomically only if the held
|
|
190
|
-
// range is still free (resource bookings) or seats remain (RSVPs
|
|
190
|
+
// range is still free (resource bookings) or seats remain (RSVPs; with
|
|
191
|
+
// oncePerEmail, and the address has no active RSVP for that date), and
|
|
191
192
|
// returns null when it lost.
|
|
192
193
|
/** from/to: overlaps the window; endedBy: ended at or before; default order start ascending, at most 5000 rows */
|
|
193
194
|
listBookings(filter?: { resourceId?: string; eventId?: string; email?: string; from?: number; to?: number; endedBy?: number; statuses?: BookingStatus[]; kinds?: Booking['kind'][]; newestFirst?: boolean; limit?: number; offset?: number }): Promise<Booking[]>;
|
|
@@ -198,7 +199,7 @@ export interface ContentRepository {
|
|
|
198
199
|
// locale: the booker's language; '' (the main language) when left out
|
|
199
200
|
createBooking(
|
|
200
201
|
data: Omit<Booking, 'id' | 'createdAt' | 'updatedAt' | 'reminderSentAt' | 'notificationVersion' | 'locale'> & { locale?: string },
|
|
201
|
-
guard?: { capacity?: number; idempotency?: { keyHash: string; requestHash: string } }
|
|
202
|
+
guard?: { capacity?: number; oncePerEmail?: boolean; idempotency?: { keyHash: string; requestHash: string } }
|
|
202
203
|
): Promise<Booking | null>;
|
|
203
204
|
// moves a booking only if the new held range is free (reschedule)
|
|
204
205
|
moveBooking(id: string, times: { start: number; end: number; holdStart: number; holdEnd: number }): Promise<boolean>;
|
package/src/lib/schema.ts
CHANGED
|
@@ -415,12 +415,16 @@ export const CalEvent = z.object({
|
|
|
415
415
|
// UID of the feed event this was imported from ('' = authored here)
|
|
416
416
|
sourceUid: z.string().default(''),
|
|
417
417
|
// RSVP: seats per occurrence (0 = unlimited) and whether RSVPs confirm at
|
|
418
|
-
// once; RSVPs are stored as bookings of kind 'rsvp'
|
|
418
|
+
// once; RSVPs are stored as bookings of kind 'rsvp'. They close at closesAt
|
|
419
|
+
// (wall time in the event's zone, like start; every date), else closeBefore
|
|
420
|
+
// minutes before each date's start, else when the date ends.
|
|
419
421
|
rsvp: z.object({
|
|
420
422
|
enabled: z.boolean().default(false),
|
|
421
423
|
capacity: z.number().int().default(0),
|
|
422
424
|
autoConfirm: z.boolean().default(true),
|
|
423
425
|
maxSeats: z.number().int().default(4),
|
|
426
|
+
closesAt: z.string().nullable().default(null),
|
|
427
|
+
closeBefore: z.number().int().default(0),
|
|
424
428
|
form: FormDefinition.optional(),
|
|
425
429
|
}).default({}),
|
|
426
430
|
// rooms/people this event uses: they can't be booked while it happens (or
|
|
@@ -19,7 +19,7 @@ import { effectiveSettings } from '../../lib/designPreview';
|
|
|
19
19
|
import { resolveDesign } from '../../lib/theme';
|
|
20
20
|
import { siteOrigin } from '../../lib/seo';
|
|
21
21
|
import { CALENDAR, eventUrl, withoutPrivateFields } from '../../lib/calendar/config';
|
|
22
|
-
import { eventIsPublic, featuredOccurrence } from '../../lib/calendar/events';
|
|
22
|
+
import { eventIsPublic, featuredOccurrence, rsvpDeadline } from '../../lib/calendar/events';
|
|
23
23
|
import { eventJsonLd } from '../../lib/calendar/seo';
|
|
24
24
|
import { upcomingEvents } from '../../lib/calendar/cards';
|
|
25
25
|
import { seatsTaken, BookingError } from '../../lib/booking/service';
|
|
@@ -50,6 +50,7 @@ const origin = siteOrigin(settings, Astro.url);
|
|
|
50
50
|
// RSVP: "Save my seat" posts back here and lands on the manage page, in the
|
|
51
51
|
// visitor's language (its emails speak it too)
|
|
52
52
|
let rsvpError = '';
|
|
53
|
+
let rsvpDuplicate = false;
|
|
53
54
|
let formValues: Record<string,string> = {};
|
|
54
55
|
let coreValues = { name: '', email: '', seats: '1' };
|
|
55
56
|
let requestId: string = crypto.randomUUID();
|
|
@@ -66,13 +67,19 @@ if (Astro.request.method === 'POST' && event.rsvp.enabled) {
|
|
|
66
67
|
const booking = await submitEventForm(repo, event, occurrence, form, requestId, { origin, locals: Astro.locals, waitUntil: waitUntilFrom(Astro.locals) }, locale);
|
|
67
68
|
return Astro.redirect(`${localePath(manageUrl(booking.manageToken), locale)}?new=1`, 303);
|
|
68
69
|
} catch (err) {
|
|
69
|
-
|
|
70
|
+
const w = rsvpStrings((I18N.enabled ? locale : '') || siteLang(settings));
|
|
71
|
+
// the address already has one: a notice in the page's words (templates that don't know it show it as the error)
|
|
72
|
+
rsvpDuplicate = err instanceof BookingError && err.code === 'duplicate';
|
|
73
|
+
rsvpError = rsvpDuplicate ? `${w.duplicate} ${w.duplicateNote}` : err instanceof BookingError || err instanceof ApiError ? err.message : w.saveError;
|
|
70
74
|
}
|
|
71
75
|
}
|
|
72
76
|
const formHtml = hiddenFields(await formVersion(definition), requestId) + `<input type="hidden" name="occurrence" value="${occurrence.start}">`;
|
|
73
77
|
const taken = event.rsvp.enabled ? await seatsTaken(repo, event.id, occurrence.start) : 0;
|
|
78
|
+
// the event's RSVP deadline for this date (null: open until it ends)
|
|
79
|
+
const closesAt = event.rsvp.enabled ? rsvpDeadline(occurrence, settings) : null;
|
|
74
80
|
const rsvp = event.rsvp.enabled
|
|
75
|
-
? { going: taken, left: event.rsvp.capacity ? Math.max(0, event.rsvp.capacity - taken) : null, maxSeats: event.rsvp.maxSeats, error: rsvpError, formHtml, fieldsHtml: renderFields(definition, formValues, 'event-registration'), values: coreValues,
|
|
81
|
+
? { going: taken, left: event.rsvp.capacity ? Math.max(0, event.rsvp.capacity - taken) : null, maxSeats: event.rsvp.maxSeats, error: rsvpError, duplicate: rsvpDuplicate, formHtml, fieldsHtml: renderFields(definition, formValues, 'event-registration'), values: coreValues,
|
|
82
|
+
open: occurrence.end > now && !occurrence.cancelled && !(closesAt !== null && closesAt <= now), closesAt }
|
|
76
83
|
: null;
|
|
77
84
|
const calendars = event.calendarIds.map((id) => calMap.get(id)).filter((c): c is NonNullable<typeof c> => Boolean(c) && c!.visibility !== 'private');
|
|
78
85
|
const html = highlightCodeHtml(await expandDynamicBlocks(event.bodyHtml, repo));
|
package/src/styles/editor.css
CHANGED
|
@@ -1168,6 +1168,10 @@
|
|
|
1168
1168
|
.pe-evr-modes + .pe-evs-fields { margin-top: 22px; }
|
|
1169
1169
|
.pe-evr-link { grid-template-columns: minmax(0, 1fr) 150px; }
|
|
1170
1170
|
.pe-evr-seats { grid-template-columns: repeat(2, minmax(0, 200px)); }
|
|
1171
|
+
.pe-evr-when { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; }
|
|
1172
|
+
.pe-evr-when > span { font-size: 13px; color: var(--muted); }
|
|
1173
|
+
.pe-field .pe-evr-when input, .pe-field .pe-evr-when select { width: auto; }
|
|
1174
|
+
.pe-field .pe-evr-when input[type='number'] { width: 84px; }
|
|
1171
1175
|
.pe-evr-form { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 18px; padding: 12px 14px; border: 1px solid var(--line); border-radius: var(--r-md); background: var(--panel); }
|
|
1172
1176
|
.pe-evr-form-t { flex: 1 1 240px; min-width: 0; }
|
|
1173
1177
|
.pe-evr-form-t b { display: block; font-size: 13.5px; font-weight: 600; color: var(--text); }
|
|
@@ -88,6 +88,13 @@
|
|
|
88
88
|
.ma-tag { display: inline-flex; align-items: center; gap: 5px; background: var(--sel); color: var(--text-2); font-size: 12px; font-weight: 600; padding: 2px 8px; border-radius: var(--r-full); }
|
|
89
89
|
.ma-tag button { background: none; border: 0; color: inherit; cursor: pointer; font-size: 10px; padding: 0; }
|
|
90
90
|
.ma-tags input { flex: 1; min-width: 70px; background: none !important; border: 0 !important; box-shadow: none !important; padding: 2px !important; }
|
|
91
|
+
/* "when an event changes": a card per rule */
|
|
92
|
+
.ma-subrules { display: grid; gap: 8px; margin-top: 10px; }
|
|
93
|
+
.ma-subrule { padding: 6px 12px 12px; border: 1px solid var(--line); border-radius: var(--r-md); min-width: 0; }
|
|
94
|
+
.ma-subrule-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 30px; }
|
|
95
|
+
.ma-subrule-head b { font-size: 12.5px; color: var(--text-2); }
|
|
96
|
+
.ma-subrule .ma-choices { margin-top: 2px; }
|
|
97
|
+
.ma-add { margin-top: 10px; }
|
|
91
98
|
.ma .form-actions { margin-top: 6px; }
|
|
92
99
|
.ma-saved { color: var(--green); font-size: 12.5px; font-weight: 500; }
|
|
93
100
|
|
package/src/styles/site.css
CHANGED
|
@@ -1147,6 +1147,9 @@ li::marker { color: var(--muted); }
|
|
|
1147
1147
|
.pc-ev-rsvpform[open] > summary { display: none; }
|
|
1148
1148
|
.pc-ev-rsvpform form { margin-top: 4px; }
|
|
1149
1149
|
.pc-ev-full { font-size: 14px; font-weight: 600; color: var(--muted); }
|
|
1150
|
+
.pc-ev-closes { font-size: 13.5px; color: var(--muted); margin: 0 0 10px; }
|
|
1151
|
+
.pc-ev-full + .pc-ev-closes { margin-top: 4px; }
|
|
1152
|
+
.pc-ev-notice { font-size: 14px; line-height: 1.5; background: var(--soft); border-radius: var(--r-sm); padding: 9px 12px; margin: 0 0 14px; }
|
|
1150
1153
|
|
|
1151
1154
|
@media (max-width: 860px) {
|
|
1152
1155
|
.pc-bk { grid-template-columns: 1fr; }
|
package/src/theme-sdk/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface HomeProps { settings: Settings; page: Page | null; posts: Post[
|
|
|
11
11
|
export interface PageProps { settings: Settings; page: Page; html: string }
|
|
12
12
|
export interface PostProps { settings: Settings; post: Post; html: string; authors: User[]; related: Post[] }
|
|
13
13
|
import type { Occurrence } from '../lib/calendar/events';
|
|
14
|
-
export interface EventProps { settings: Settings; event: CalEvent; html: string; calendars: Calendar[]; origin: string; occurrence: Occurrence; upcoming: Pick<Occurrence, 'event' | 'start' | 'end' | 'allDay' | 'days'>[]; preview?: boolean; rsvp?: {going:number;left:number|null;maxSeats:number;error:string;open:boolean}|null }
|
|
14
|
+
export interface EventProps { settings: Settings; event: CalEvent; html: string; calendars: Calendar[]; origin: string; occurrence: Occurrence; upcoming: Pick<Occurrence, 'event' | 'start' | 'end' | 'allDay' | 'days'>[]; preview?: boolean; rsvp?: {going:number;left:number|null;maxSeats:number;error:string;open:boolean;closesAt?:number|null;duplicate?:boolean}|null }
|
|
15
15
|
/**
|
|
16
16
|
* Props for a coded section. `data` holds resolved values: link fields arrive
|
|
17
17
|
* as LinkValue, lists as row arrays, source fields as fresh item arrays
|