@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.
@@ -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() });
@@ -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(), search: SEARCH.enabled,
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
- source: z.enum(['web', 'admin', 'api', 'agent']).default('web'),
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(),
@@ -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();
@@ -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