@acms-subir/core 0.3.35 → 0.3.36
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 +15 -0
- package/docs/mobile.md +7 -4
- package/docs/theme-authoring.md +5 -4
- package/integration.mjs +3 -1
- package/mobile-template/client/AGENTS.md +2 -1
- package/mobile-template/client/README.md +1 -1
- package/mobile-template/recipes/events/files/app/events/[slug].tsx +50 -22
- package/mobile-template/recipes/events/files/app/events/index.tsx +4 -3
- package/mobile-template/recipes/events/files/components/RsvpPanel.tsx +270 -0
- package/mobile-template/recipes/events/recipe.json +6 -3
- package/mobile-template/recipes/rsvp/files/components/RsvpPanel.tsx +270 -0
- package/mobile-template/recipes/rsvp/recipe.json +15 -0
- package/package.json +1 -1
- package/src/layouts/Site.astro +1 -0
- package/src/lib/cache/policy.ts +2 -2
- package/src/lib/legal/app-privacy.ts +5 -3
- package/src/lib/legal/text-en.ts +1 -0
- package/src/lib/legal/text-es.ts +1 -0
- package/src/lib/mobile/bookings.ts +17 -11
- package/src/lib/mobile/forms.ts +21 -11
- package/src/lib/mobile/manage.ts +2 -0
- package/src/lib/mobile/openapi.ts +8 -3
- package/src/lib/mobile/rsvp.ts +65 -0
- package/src/lib/mobile/schemas.ts +9 -1
- package/src/lib/mobile/site.ts +3 -1
- package/src/lib/schema.ts +2 -1
- package/src/lib/site-chrome.ts +8 -0
- package/src/pages/admin/calendar/bookings.astro +3 -1
- package/src/pages/api/v1/events/[slug]/rsvp.ts +8 -0
- package/src/theme-sdk/index.ts +2 -0
|
@@ -8,7 +8,7 @@ const Palette = z.object({ bg: str, ink: str, muted: str, line: str, accent: str
|
|
|
8
8
|
export const Site = z.object({
|
|
9
9
|
name: str, description: str, logo: str, logoDark: str, icon: str, menu: z.array(z.object({ label: str, href: str })),
|
|
10
10
|
languages: z.object({ default: locale, locales: z.array(z.object({ code: str, name: str })) }),
|
|
11
|
-
features: z.object({ blog: z.array(z.object({ key: str, name: str, url: str, redirectFrom: strings })), calendar: bool, bookings: bool, search: bool, bookingChanges: bool, guestSignIn: bool, forms: bool, links: bool, live: bool, announcements: bool }),
|
|
11
|
+
features: z.object({ blog: z.array(z.object({ key: str, name: str, url: str, redirectFrom: strings })), calendar: bool, bookings: bool, rsvp: bool, search: bool, bookingChanges: bool, guestSignIn: bool, forms: bool, links: bool, live: bool, announcements: bool }),
|
|
12
12
|
legal: z.object({ privacy: str.nullable(), terms: str.nullable() }), apiVersion: z.literal(1),
|
|
13
13
|
});
|
|
14
14
|
export const Theme = z.object({
|
|
@@ -68,6 +68,12 @@ export const Form = z.object({
|
|
|
68
68
|
})),
|
|
69
69
|
});
|
|
70
70
|
export const FormReceipt = z.object({ received: z.literal(true), confirmation: str });
|
|
71
|
+
export const RsvpStatus = z.object({
|
|
72
|
+
event: z.object({ id: str, slug: str, title: str }), start: date, end: date,
|
|
73
|
+
open: bool, reason: z.enum(['disabled', 'ended', 'cancelled', 'full']).nullable(),
|
|
74
|
+
going: z.number().int(), left: z.number().int().nullable(), capacity: z.number().int(), maxSeats: z.number().int(), autoConfirm: bool,
|
|
75
|
+
form: Form.omit({ id: true }).nullable(),
|
|
76
|
+
});
|
|
71
77
|
const GuestChanges = z.object({ cancel: bool, reschedule: bool, cutoffHours: z.number().int(), closed: bool });
|
|
72
78
|
export const ManagedBooking = z.object({
|
|
73
79
|
booking: z.object({
|
|
@@ -79,6 +85,8 @@ export const ManagedBooking = z.object({
|
|
|
79
85
|
link: z.object({ slug: str, name: str, mode: z.enum(['slots', 'range', 'day']), lengths: z.array(z.number()) }).nullable(),
|
|
80
86
|
event: z.object({ slug: str, title: str, url: str }).nullable(),
|
|
81
87
|
manageUrl: str, can: GuestChanges,
|
|
88
|
+
// what the web page says once it's confirmed: the RSVP form's or booking type's message
|
|
89
|
+
confirmation: str.nullable(),
|
|
82
90
|
});
|
|
83
91
|
export const GuestCodeSent = z.object({ sent: z.literal(true), expiresInMinutes: z.number().int() });
|
|
84
92
|
export const GuestToken = z.object({ accessToken: str, tokenType: z.literal('Bearer'), email: str, idleDays: z.number().int() });
|
package/src/lib/mobile/site.ts
CHANGED
|
@@ -37,7 +37,9 @@ export async function getSite(repo: ContentRepository, url: URL, _id = '', local
|
|
|
37
37
|
features: {
|
|
38
38
|
// redirectFrom: old addresses the website 301s to url, so apps can follow them too,
|
|
39
39
|
// minus any another collection now lives at (the website serves that one first)
|
|
40
|
-
blog: Object.entries(CONTENT.collections).map(([key, c]) => ({ key, name: c.plural, url: safeUrl(c.base, origin), redirectFrom: legacyBases(key, c).filter(from => !bases.has(from)).map(from => safeUrl(from, origin)) })), calendar: CALENDAR.enabled, bookings: bookingsEnabled(),
|
|
40
|
+
blog: Object.entries(CONTENT.collections).map(([key, c]) => ({ key, name: c.plural, url: safeUrl(c.base, origin), redirectFrom: legacyBases(key, c).filter(from => !bases.has(from)).map(from => safeUrl(from, origin)) })), calendar: CALENDAR.enabled, bookings: bookingsEnabled(),
|
|
41
|
+
// RSVPs from apps (GET /events/{slug}/rsvp, POST /bookings) come with the calendar
|
|
42
|
+
rsvp: CALENDAR.enabled, search: SEARCH.enabled,
|
|
41
43
|
// Admin → Mobile app → App access
|
|
42
44
|
bookingChanges: CALENDAR.enabled && settings.mobileAccess.bookingChanges, guestSignIn: await guestSignInReady(repo, locals),
|
|
43
45
|
forms: settings.mobileAccess.forms, links: settings.mobileAccess.links,
|
package/src/lib/schema.ts
CHANGED
|
@@ -672,7 +672,8 @@ export const Booking = z.object({
|
|
|
672
672
|
formResponse: FormResponse.optional(),
|
|
673
673
|
notes: z.string().default(''), // staff-only
|
|
674
674
|
manageToken: z.string().default(''),
|
|
675
|
-
|
|
675
|
+
// web: the website; app: the public app API (/api/v1/bookings)
|
|
676
|
+
source: z.enum(['web', 'app', 'admin', 'api', 'agent']).default('web'),
|
|
676
677
|
reminderSentAt: z.number().nullable().default(null),
|
|
677
678
|
notificationVersion: z.number().default(0),
|
|
678
679
|
createdAt: z.number(),
|
package/src/lib/site-chrome.ts
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
// [data-scheme-toggle] flips light/dark (when site.schemeToggle is true)
|
|
9
9
|
// [data-menu-open] opens the phone menu sheet (built from site.links)
|
|
10
10
|
// [data-live-header] (hidden) becomes the "Live" link during a stream
|
|
11
|
+
//
|
|
12
|
+
// `words` are core's own labels in the page's language (skip link, menu and
|
|
13
|
+
// dark-mode buttons, landmark and footer labels), so a custom header labels
|
|
14
|
+
// its controls the same way the built-in one does.
|
|
15
|
+
import type { SiteStrings } from './site-words';
|
|
16
|
+
|
|
11
17
|
export interface ChromeLink {
|
|
12
18
|
label: string;
|
|
13
19
|
url: string;
|
|
@@ -32,4 +38,6 @@ export interface SiteChrome {
|
|
|
32
38
|
liveHeader: boolean;
|
|
33
39
|
headerStyle: string; // theme token `header`
|
|
34
40
|
footerStyle: string; // theme token `footer`
|
|
41
|
+
lang: string; // the page's language, as on <html lang>
|
|
42
|
+
words: SiteStrings;
|
|
35
43
|
}
|
|
@@ -185,6 +185,8 @@ const emailNotice = !emailConfig.config.enabled || emailConfig.config.provider =
|
|
|
185
185
|
: emailStatus.linkError;
|
|
186
186
|
const TABS = [['upcoming', 'Upcoming'], ['unconfirmed', 'Needs approval'], ['past', 'Past'], ['cancelled', 'Cancelled']] as const;
|
|
187
187
|
const statusLabel = (b: Booking) => (b.kind === 'block' ? 'Blocked' : b.status[0].toUpperCase() + b.status.slice(1));
|
|
188
|
+
// where it was made: the website, an app (/api/v1/bookings), staff, an API key or the booking agent
|
|
189
|
+
const SOURCES: Record<Booking['source'], string> = { web: 'Website', app: 'App', admin: 'Staff', api: 'API', agent: 'Agent' };
|
|
188
190
|
const whenOf = (b: Booking) => (b.end - b.start >= 86_400_000 - 3_600_000 && ymd(b.start, tz) !== ymd(b.end - 1, tz)
|
|
189
191
|
? `${f.day.format(b.start)} – ${f.day.format(b.end - 1)}`
|
|
190
192
|
: b.end - b.start >= 86_400_000 - 3_600_000 ? 'All day' : `${f.time.format(b.start)} – ${f.time.format(b.end)}`);
|
|
@@ -327,7 +329,7 @@ const activeLinks = links.filter((l) => l.active && resById.has(l.resourceId));
|
|
|
327
329
|
<div><dt>{ev ? 'Event' : 'Where'}</dt><dd>{ev ? <a href={eventUrl(ev.slug)} target="_blank" rel="noopener">{ev.title}</a> : r?.name ?? '—'}{l ? ` · ${l.name}` : ''}</dd></div>
|
|
328
330
|
{b.seats > 1 && <div><dt>Seats</dt><dd>{b.seats}</dd></div>}
|
|
329
331
|
{Object.entries(b.answers).map(([k, v]) => <div><dt>{k}</dt><dd>{v}</dd></div>)}
|
|
330
|
-
<div><dt>Booked</dt><dd>{f.day.format(b.createdAt)} · {b.source}</dd></div>
|
|
332
|
+
<div><dt>Booked</dt><dd>{f.day.format(b.createdAt)} · {SOURCES[b.source] ?? b.source}</dd></div>
|
|
331
333
|
</dl>
|
|
332
334
|
<form method="post" class="bk-notes">
|
|
333
335
|
<input type="hidden" name="op" value="notes" /><input type="hidden" name="id" value={b.id} />
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { getRsvpStatus } from '../../../../../lib/mobile/rsvp';
|
|
2
|
+
import { readRoute } from '../../../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
|
|
5
|
+
// no-store: live seats; a cached "3 left" would invite RSVPs that fail
|
|
6
|
+
export const GET = readRoute(getRsvpStatus, false);
|
|
7
|
+
export const HEAD = GET;
|
|
8
|
+
export const ALL = () => methodNotAllowed();
|
package/src/theme-sdk/index.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface SectionProps<D = Record<string, unknown>> { data: D; variant: s
|
|
|
22
22
|
export type { LinkValue, EventItem, PostItem, PostAuthor, BookingItem } from '../lib/sections';
|
|
23
23
|
/** Props for templates/header.astro and templates/footer.astro. */
|
|
24
24
|
export type { SiteChrome, ChromeLink } from '../lib/site-chrome';
|
|
25
|
+
// core's chrome labels for any language (a template already gets them as site.words)
|
|
26
|
+
export { siteStrings, type SiteStrings } from '../lib/site-words';
|
|
25
27
|
import type { SiteChrome } from '../lib/site-chrome';
|
|
26
28
|
export interface ChromeProps { site: SiteChrome }
|
|
27
29
|
|