@acms-subir/core 0.3.3-next.20260929113209 → 0.3.4
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 +105 -0
- package/cli/common.mjs +2 -2
- package/cli/legacy.mjs +1 -1
- package/cli/main.mjs +82 -8
- package/cli/mobile-doctor.mjs +430 -0
- package/cli/mobile.mjs +315 -0
- package/cli/native-sections.mjs +168 -0
- package/cli/preview.mjs +8 -4
- package/cli/sections.mjs +188 -0
- package/cli/themes.mjs +52 -8
- package/docs/mobile.md +527 -0
- package/docs/sections.md +203 -0
- package/docs/theme-authoring.md +19 -3
- package/integration.mjs +76 -5
- package/mobile-template/client/AGENTS.md +97 -0
- package/mobile-template/client/README.md +65 -0
- package/mobile-template/client/app/_layout.tsx +56 -0
- package/mobile-template/client/app/index.tsx +21 -0
- package/mobile-template/client/app.json +21 -0
- package/mobile-template/client/components/LanguageSwitch.tsx +31 -0
- package/mobile-template/client/eas.json +21 -0
- package/mobile-template/client/env.example +3 -0
- package/mobile-template/client/gitignore +42 -0
- package/mobile-template/client/package.json +38 -0
- package/mobile-template/client/sections/index.ts +11 -0
- package/mobile-template/client/tsconfig.json +7 -0
- package/mobile-template/editor/AGENTS.md +66 -0
- package/mobile-template/editor/README.md +73 -0
- package/mobile-template/editor/app/_layout.tsx +76 -0
- package/mobile-template/editor/app/auth.tsx +13 -0
- package/mobile-template/editor/app/index.tsx +55 -0
- package/mobile-template/editor/app.json +21 -0
- package/mobile-template/editor/components/Screen.tsx +51 -0
- package/mobile-template/editor/components/SignIn.tsx +36 -0
- package/mobile-template/editor/components/TextTabs.tsx +27 -0
- package/mobile-template/editor/eas.json +21 -0
- package/mobile-template/editor/env.example +5 -0
- package/mobile-template/editor/gitignore +42 -0
- package/mobile-template/editor/lib/appearance.tsx +12 -0
- package/mobile-template/editor/lib/config.ts +17 -0
- package/mobile-template/editor/lib/format.ts +24 -0
- package/mobile-template/editor/lib/tools.ts +16 -0
- package/mobile-template/editor/package.json +40 -0
- package/mobile-template/editor/tools/README.md +2 -0
- package/mobile-template/editor/tsconfig.json +7 -0
- package/mobile-template/recipes/booking/files/app/book/[resource].tsx +57 -0
- package/mobile-template/recipes/booking/files/app/book/details.tsx +105 -0
- package/mobile-template/recipes/booking/files/app/book/index.tsx +38 -0
- package/mobile-template/recipes/booking/recipe.json +17 -0
- package/mobile-template/recipes/editor-bookings/files/app/bookings/index.tsx +113 -0
- package/mobile-template/recipes/editor-bookings/files/tools/bookings.ts +1 -0
- package/mobile-template/recipes/editor-bookings/recipe.json +8 -0
- package/mobile-template/recipes/editor-content/files/app/content/[kind]/[id].tsx +13 -0
- package/mobile-template/recipes/editor-content/files/app/content/index.tsx +76 -0
- package/mobile-template/recipes/editor-content/files/components/DocumentDetail.tsx +55 -0
- package/mobile-template/recipes/editor-content/files/tools/content.ts +1 -0
- package/mobile-template/recipes/editor-content/recipe.json +8 -0
- package/mobile-template/recipes/editor-media/files/app/media/index.tsx +73 -0
- package/mobile-template/recipes/editor-media/files/tools/media.ts +1 -0
- package/mobile-template/recipes/editor-media/recipe.json +8 -0
- package/mobile-template/recipes/editor-sections/files/app/sections/[pageId]/[sectionId].tsx +12 -0
- package/mobile-template/recipes/editor-sections/files/app/sections/index.tsx +48 -0
- package/mobile-template/recipes/editor-sections/files/components/SectionEditorPane.tsx +77 -0
- package/mobile-template/recipes/editor-sections/files/tools/sections.ts +1 -0
- package/mobile-template/recipes/editor-sections/recipe.json +8 -0
- package/mobile-template/recipes/editor-translations/files/app/translations/index.tsx +63 -0
- package/mobile-template/recipes/editor-translations/files/app/translations/review.tsx +14 -0
- package/mobile-template/recipes/editor-translations/files/components/ReviewPane.tsx +72 -0
- package/mobile-template/recipes/editor-translations/files/tools/translations.ts +1 -0
- package/mobile-template/recipes/editor-translations/recipe.json +8 -0
- package/mobile-template/recipes/editor-writer/files/app/write/[kind]/[id].tsx +132 -0
- package/mobile-template/recipes/editor-writer/files/app/write/new.tsx +61 -0
- package/mobile-template/recipes/editor-writer/files/tools/writer.ts +1 -0
- package/mobile-template/recipes/editor-writer/recipe.json +8 -0
- package/mobile-template/recipes/events/files/app/events/[slug].tsx +41 -0
- package/mobile-template/recipes/events/files/app/events/index.tsx +50 -0
- package/mobile-template/recipes/events/files/lib/addToCalendar.ts +27 -0
- package/mobile-template/recipes/events/recipe.json +15 -0
- package/mobile-template/recipes/news/files/app/news/[slug].tsx +25 -0
- package/mobile-template/recipes/news/files/app/news/index.tsx +50 -0
- package/mobile-template/recipes/news/recipe.json +13 -0
- package/mobile-template/recipes/push/files/app/notifications.tsx +64 -0
- package/mobile-template/recipes/push/files/components/PushSetup.tsx +15 -0
- package/mobile-template/recipes/push/files/lib/push.ts +15 -0
- package/mobile-template/recipes/push/recipe.json +20 -0
- package/package.json +17 -13
- package/section-library/README.md +16 -0
- package/section-library/contact/Contact.astro +49 -0
- package/section-library/contact/section.json +106 -0
- package/section-library/cta-band/CtaBand.astro +28 -0
- package/section-library/cta-band/section.json +51 -0
- package/section-library/feature-grid/FeatureGrid.astro +54 -0
- package/section-library/feature-grid/section.json +107 -0
- package/section-library/gallery/Gallery.astro +29 -0
- package/section-library/gallery/section.json +51 -0
- package/section-library/hero-banner/HeroBanner.astro +42 -0
- package/section-library/hero-banner/section.json +81 -0
- package/section-library/latest-posts/LatestPosts.astro +22 -0
- package/section-library/latest-posts/section.json +37 -0
- package/section-library/pricing/Pricing.astro +40 -0
- package/section-library/pricing/section.json +117 -0
- package/section-library/questions/Questions.astro +38 -0
- package/section-library/questions/section.json +69 -0
- package/section-library/service-times/ServiceTimes.astro +42 -0
- package/section-library/service-times/section.json +127 -0
- package/section-library/team/Team.astro +45 -0
- package/section-library/team/section.json +88 -0
- package/section-library/upcoming-events/UpcomingEvents.astro +54 -0
- package/section-library/upcoming-events/section.json +51 -0
- package/src/actions/index.ts +205 -174
- package/src/components/AiSettings.tsx +45 -5
- package/src/components/DesignManager.tsx +112 -16
- package/src/components/MobileApp.tsx +392 -0
- package/src/components/ThemeSections.astro +40 -21
- package/src/components/ThemeSectionsEditor.tsx +757 -229
- package/src/components/booking/BookingFlow.astro +6 -3
- package/src/components/calendar/BookingsHead.astro +51 -0
- package/src/components/calendar/CalendarBoard.tsx +618 -0
- package/src/components/calendar/MiniMonth.tsx +54 -0
- package/src/components/calendar/NewEventDialog.tsx +44 -0
- package/src/components/calendar/QuickAdd.tsx +263 -0
- package/src/components/calendar/RoomHolds.tsx +203 -0
- package/src/components/calendar/WhereItLands.tsx +51 -0
- package/src/components/editor/EventPanel.tsx +23 -2
- package/src/components/editor/PostEditor.tsx +206 -14
- package/src/components/import/ImportExport.tsx +4 -1
- package/src/components/import/runner.ts +1 -0
- package/src/components/sections/SectionFields.tsx +560 -0
- package/src/components/sections/SectionPicker.tsx +120 -0
- package/src/components/sections/TranslationPanel.tsx +143 -0
- package/src/components/sections/checks.ts +64 -0
- package/src/layouts/Admin.astro +11 -7
- package/src/layouts/Site.astro +136 -21
- package/src/lib/agent-discovery.ts +11 -7
- package/src/lib/agent.ts +35 -2
- package/src/lib/ai-schema.ts +5 -0
- package/src/lib/ai.ts +5 -0
- package/src/lib/api.ts +12 -0
- package/src/lib/backup/content.ts +6 -1
- package/src/lib/booking/agent.ts +3 -2
- package/src/lib/booking/approval.ts +70 -0
- package/src/lib/booking/holds.ts +120 -0
- package/src/lib/booking/service.ts +37 -3
- package/src/lib/calendar/agent-schema.ts +9 -1
- package/src/lib/calendar/agent.ts +16 -1
- package/src/lib/calendar/board.ts +171 -0
- package/src/lib/calendar/config.ts +3 -0
- package/src/lib/calendar/grid.ts +169 -0
- package/src/lib/calendar/migration.ts +16 -0
- package/src/lib/calendar/render.ts +9 -7
- package/src/lib/calendar/schedule.ts +166 -0
- package/src/lib/content-service.ts +203 -0
- package/src/lib/design-service.ts +153 -5
- package/src/lib/drafts.ts +3 -3
- package/src/lib/editor/auth.ts +264 -0
- package/src/lib/editor/bookings.ts +60 -0
- package/src/lib/editor/config.ts +25 -0
- package/src/lib/editor/documents.ts +227 -0
- package/src/lib/editor/http.ts +48 -0
- package/src/lib/editor/me.ts +30 -0
- package/src/lib/editor/migration.ts +57 -0
- package/src/lib/editor/next.ts +11 -0
- package/src/lib/editor/openapi.ts +124 -0
- package/src/lib/editor/schemas.ts +91 -0
- package/src/lib/editor/sections.ts +96 -0
- package/src/lib/editor/store.ts +184 -0
- package/src/lib/editor/token-route.ts +74 -0
- package/src/lib/editor/translations.ts +163 -0
- package/src/lib/email/accounts.ts +2 -1
- package/src/lib/email/types.ts +2 -1
- package/src/lib/home-source.ts +34 -0
- package/src/lib/i18n.ts +34 -0
- package/src/lib/import/apply.ts +31 -2
- package/src/lib/import/bundle.ts +4 -0
- package/src/lib/import/export.ts +1 -0
- package/src/lib/import/ghost.ts +1 -0
- package/src/lib/media/upload.ts +83 -0
- package/src/lib/mobile/associations.ts +25 -0
- package/src/lib/mobile/calendar.ts +26 -14
- package/src/lib/mobile/config.mjs +59 -0
- package/src/lib/mobile/content.ts +18 -13
- package/src/lib/mobile/embed.ts +83 -0
- package/src/lib/mobile/http.ts +14 -6
- package/src/lib/mobile/locale.ts +18 -0
- package/src/lib/mobile/openapi.ts +111 -0
- package/src/lib/mobile/pages.ts +71 -0
- package/src/lib/mobile/route.ts +3 -3
- package/src/lib/mobile/schemas.ts +60 -0
- package/src/lib/mobile/site.ts +41 -0
- package/src/lib/push/api.ts +158 -0
- package/src/lib/push/config.ts +20 -0
- package/src/lib/push/expo.ts +76 -0
- package/src/lib/push/migration.ts +60 -0
- package/src/lib/push/service.ts +302 -0
- package/src/lib/push/store.ts +144 -0
- package/src/lib/push/types.ts +43 -0
- package/src/lib/repo/migrations.ts +14 -0
- package/src/lib/repo/sql.ts +65 -20
- package/src/lib/repo/types.ts +10 -4
- package/src/lib/roles.ts +5 -0
- package/src/lib/scheduler.ts +6 -1
- package/src/lib/schema.ts +76 -1
- package/src/lib/section-ai.ts +55 -0
- package/src/lib/section-targets.ts +62 -0
- package/src/lib/sections-agent.ts +146 -0
- package/src/lib/sections.ts +242 -0
- package/src/lib/site-chrome.ts +34 -0
- package/src/lib/theme-assets.ts +16 -0
- package/src/lib/theme.ts +33 -4
- package/src/lib/translate.ts +97 -0
- package/src/lib/translations.ts +39 -0
- package/src/middleware.ts +38 -2
- package/src/pages/.well-known/apple-app-site-association.ts +6 -0
- package/src/pages/.well-known/assetlinks.json.ts +6 -0
- package/src/pages/[...slug].astro +8 -3
- package/src/pages/admin/app-auth.astro +159 -0
- package/src/pages/admin/c/[collection]/[id].astro +9 -0
- package/src/pages/admin/calendar/bookings.astro +32 -26
- package/src/pages/admin/calendar/calendars.astro +4 -263
- package/src/pages/admin/calendar/events/[id].astro +9 -0
- package/src/pages/admin/calendar/events/index.astro +41 -83
- package/src/pages/admin/calendar/index.astro +43 -0
- package/src/pages/admin/calendar/resources/[id].astro +27 -5
- package/src/pages/admin/calendar/resources/index.astro +16 -17
- package/src/pages/admin/calendar/resources/link.astro +52 -12
- package/src/pages/admin/calendar/settings.astro +458 -0
- package/src/pages/admin/integrations.astro +5 -11
- package/src/pages/admin/login.astro +6 -2
- package/src/pages/admin/logout.ts +5 -2
- package/src/pages/admin/mobile.astro +18 -0
- package/src/pages/admin/pages/[id].astro +62 -0
- package/src/pages/admin/section-preview.astro +61 -0
- package/src/pages/admin/users.astro +107 -11
- package/src/pages/api/agent/docs.ts +6 -2
- package/src/pages/api/agent/index.ts +1 -1
- package/src/pages/api/ai/settings.ts +7 -4
- package/src/pages/api/design/ai.ts +15 -0
- package/src/pages/api/design/index.ts +12 -42
- package/src/pages/api/design/translate.ts +15 -0
- package/src/pages/api/live/thumb.ts +1 -1
- package/src/pages/api/mcp.ts +2 -2
- package/src/pages/api/media/upload.ts +10 -78
- package/src/pages/api/push/admin.ts +27 -0
- package/src/pages/api/v1/admin/bookings/[id]/[action].ts +14 -0
- package/src/pages/api/v1/admin/bookings/index.ts +6 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/[action].ts +17 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/index.ts +20 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/translations/[locale]/[action].ts +26 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/translations/[locale]/index.ts +18 -0
- package/src/pages/api/v1/admin/documents/index.ts +13 -0
- package/src/pages/api/v1/admin/me.ts +6 -0
- package/src/pages/api/v1/admin/media/index.ts +32 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/index.ts +14 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/publish.ts +14 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/[action].ts +22 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/index.ts +14 -0
- package/src/pages/api/v1/admin/sections/index.ts +6 -0
- package/src/pages/api/v1/auth/revoke.ts +5 -0
- package/src/pages/api/v1/auth/token.ts +5 -0
- package/src/pages/api/v1/devices/[token].ts +20 -0
- package/src/pages/api/v1/devices/index.ts +17 -0
- package/src/pages/api/v1/openapi.json.ts +7 -0
- package/src/pages/api/v1/pages/[...slug].ts +7 -0
- package/src/pages/api/v1/pages/home.ts +7 -0
- package/src/pages/api/v1/push/topics.ts +7 -0
- package/src/pages/api/v1/site.ts +7 -0
- package/src/pages/api/v1/theme.ts +7 -0
- package/src/pages/blog/[slug].astro +8 -2
- package/src/pages/blog/index.astro +11 -4
- package/src/pages/book/[resource].ics.ts +7 -1
- package/src/pages/embed/section/[...path].astro +42 -0
- package/src/pages/events/[slug].astro +7 -2
- package/src/pages/index.astro +11 -5
- package/src/pages/sitemap.xml.ts +8 -0
- package/src/styles/admin.css +117 -0
- package/src/styles/cards.css +5 -1
- package/src/styles/editor.css +57 -0
- package/src/styles/mobile-app.css +151 -0
- package/src/styles/schedule.css +325 -0
- package/src/styles/sections.css +21 -12
- package/src/styles/theme-editor.css +255 -0
- package/src/theme-sdk/contract.d.mts +2193 -89
- package/src/theme-sdk/contract.mjs +77 -20
- package/src/theme-sdk/fields.d.mts +88 -0
- package/src/theme-sdk/fields.mjs +330 -0
- package/src/theme-sdk/index.ts +14 -2
- package/src/theme-sdk/tokens.d.mts +11 -0
- package/src/virtual.d.ts +4 -0
- package/src/pages/admin/calendar/index.ts +0 -4
package/src/lib/repo/sql.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { LiveStore } from '../live/store';
|
|
2
|
+
import { PushStore } from '../push/store';
|
|
3
|
+
import { EditorStore } from '../editor/store';
|
|
2
4
|
import { LiveConfig } from '../live/types';
|
|
3
5
|
import { ApiError } from '../api';
|
|
4
6
|
import { EmailStore, bookingJobs } from '../email/store';
|
|
5
7
|
import type { ContentRepository, DesignVersion, EventUpdate, ImportRecord, ImportRecordKind, ImportRun, SearchResults, Stats } from './types';
|
|
6
|
-
import { Page, Post, Link, Media, NavItem, Redirect, Settings, Snippet, Tag, User, Calendar, CalEvent, ApiKey, Webhook, WebhookDelivery, Resource, BookingLink, Booking, isInternalTag, normalizeRedirectPath, publicTags, slugify, type WidgetInstance } from '../schema';
|
|
8
|
+
import { DocTranslations, Page, Post, Link, Media, NavItem, Redirect, Settings, Snippet, Tag, User, Calendar, CalEvent, ApiKey, Webhook, WebhookDelivery, Resource, BookingLink, Booking, isInternalTag, normalizeRedirectPath, publicTags, slugify, type WidgetInstance } from '../schema';
|
|
7
9
|
import { designSnapshot, draftNavItems, pickDesignDraft, pickEventDraft, pickPageDraft, pickPostDraft } from '../drafts';
|
|
8
10
|
import { eventInstants, eventTimeZone, normalizeEventTimes } from '../calendar/events';
|
|
9
11
|
import { cleanRrule } from '../calendar/rrule';
|
|
@@ -43,6 +45,11 @@ function parseDraft(raw: unknown): Record<string, unknown> | null {
|
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
// translations column: tolerate old rows / junk (never break a page render)
|
|
49
|
+
function parseTranslations(v: unknown) {
|
|
50
|
+
const parsed = DocTranslations.safeParse(json(v, {}));
|
|
51
|
+
return parsed.success ? parsed.data : {};
|
|
52
|
+
}
|
|
46
53
|
function rowToPage(r: Row): Page {
|
|
47
54
|
return Page.parse({
|
|
48
55
|
id: r.id, slug: r.slug, title: r.title, status: r.status,
|
|
@@ -50,7 +57,7 @@ function rowToPage(r: Row): Page {
|
|
|
50
57
|
blocks: JSON.parse(String(r.blocks ?? '[]')),
|
|
51
58
|
bodyJson: r.body_json ?? '', bodyHtml: r.body_html ?? '',
|
|
52
59
|
seoTitle: r.seo_title ?? '', seoDesc: r.seo_desc ?? '',
|
|
53
|
-
template: r.template ?? '', draft: parseDraft(r.draft),
|
|
60
|
+
template: r.template ?? '', draft: parseDraft(r.draft), translations: parseTranslations(r.translations),
|
|
54
61
|
createdAt: Number(r.created_at), updatedAt: Number(r.updated_at),
|
|
55
62
|
});
|
|
56
63
|
}
|
|
@@ -62,7 +69,7 @@ function rowToPost(r: Row, authorIds?: string[]): Post {
|
|
|
62
69
|
seoTitle: r.seo_title ?? '', seoDesc: r.seo_desc ?? '', status: r.status,
|
|
63
70
|
featured: Boolean(r.featured), authorId: r.author_id ?? null,
|
|
64
71
|
authorIds: authorIds ?? (r.author_id ? [String(r.author_id)] : []),
|
|
65
|
-
template: r.template ?? '', draft: parseDraft(r.draft),
|
|
72
|
+
template: r.template ?? '', draft: parseDraft(r.draft), translations: parseTranslations(r.translations),
|
|
66
73
|
publishedAt: r.published_at === null ? null : Number(r.published_at),
|
|
67
74
|
scheduledAt: r.scheduled_at === null ? null : Number(r.scheduled_at),
|
|
68
75
|
createdAt: Number(r.created_at), updatedAt: Number(r.updated_at),
|
|
@@ -137,8 +144,8 @@ function rowToEvent(r: Row): CalEvent {
|
|
|
137
144
|
start: r.starts, end: r.ends ?? '', allDay: Boolean(r.all_day), timezone: r.timezone ?? '',
|
|
138
145
|
rrule: r.rrule ?? '', exdates: json(r.exdates, []), cancelled: Boolean(r.cancelled),
|
|
139
146
|
location: json(r.location, {}), cta: json(r.cta, {}), calendarIds: json(r.calendar_ids, []),
|
|
140
|
-
fields: json(r.fields, {}), sourceUid: r.source_uid ?? '', rsvp: json(r.rsvp, {}),
|
|
141
|
-
startAt: Number(r.start_at), endAt: Number(r.end_at), draft: parseDraft(r.draft),
|
|
147
|
+
fields: json(r.fields, {}), sourceUid: r.source_uid ?? '', rsvp: json(r.rsvp, {}), roomHold: json(r.room_hold, {}),
|
|
148
|
+
startAt: Number(r.start_at), endAt: Number(r.end_at), draft: parseDraft(r.draft), translations: parseTranslations(r.translations),
|
|
142
149
|
publishedAt: r.published_at === null || r.published_at === undefined ? null : Number(r.published_at),
|
|
143
150
|
scheduledAt: r.scheduled_at === null || r.scheduled_at === undefined ? null : Number(r.scheduled_at),
|
|
144
151
|
createdAt: Number(r.created_at), updatedAt: Number(r.updated_at),
|
|
@@ -154,6 +161,14 @@ function rowToResource(r: Row): Resource {
|
|
|
154
161
|
createdAt: Number(r.created_at), updatedAt: Number(r.updated_at),
|
|
155
162
|
});
|
|
156
163
|
}
|
|
164
|
+
// Callers that still speak the old on/off flag (REST, agent tools, older
|
|
165
|
+
// code) set autoConfirm without an approval mode: map it so "off" really
|
|
166
|
+
// holds bookings. On create, "on" follows the site default (which starts as
|
|
167
|
+
// automatic); on update it pins the link to automatic.
|
|
168
|
+
function legacyApproval<T extends { autoConfirm?: boolean; approval?: BookingLink['approval'] }>(data: T, update = false): T {
|
|
169
|
+
if (data.approval !== undefined || data.autoConfirm === undefined) return data;
|
|
170
|
+
return { ...data, approval: data.autoConfirm ? (update ? 'auto' : 'default') : 'manual' };
|
|
171
|
+
}
|
|
157
172
|
function rowToLink2(r: Row): BookingLink {
|
|
158
173
|
return BookingLink.parse({
|
|
159
174
|
id: r.id, resourceId: r.resource_id, slug: r.slug, name: r.name, description: r.description ?? '', mode: r.mode ?? 'slots',
|
|
@@ -161,6 +176,7 @@ function rowToLink2(r: Row): BookingLink {
|
|
|
161
176
|
step: Number(r.step), bufferBefore: Number(r.buffer_before), bufferAfter: Number(r.buffer_after),
|
|
162
177
|
minNotice: Number(r.min_notice), maxAdvance: Number(r.max_advance), perDayCap: Number(r.per_day_cap),
|
|
163
178
|
hours: r.hours ? json(r.hours, null) : null, autoConfirm: Boolean(r.auto_confirm), visibility: r.visibility ?? 'public',
|
|
179
|
+
approval: r.approval ?? 'default', approvalRules: json(r.approval_rules, {}),
|
|
164
180
|
askPhone: Boolean(r.ask_phone), questions: json(r.questions, []), confirmMessage: r.confirm_message ?? '',
|
|
165
181
|
redirectUrl: r.redirect_url ?? '', active: Boolean(r.active), order: Number(r.ord ?? 0),
|
|
166
182
|
createdAt: Number(r.created_at), updatedAt: Number(r.updated_at),
|
|
@@ -233,10 +249,14 @@ export class SqlRepository implements ContentRepository {
|
|
|
233
249
|
private ready: Promise<void>;
|
|
234
250
|
readonly email: EmailStore;
|
|
235
251
|
readonly live: LiveStore;
|
|
252
|
+
readonly push: PushStore;
|
|
253
|
+
readonly editor: EditorStore;
|
|
236
254
|
|
|
237
255
|
constructor(driver: SqlDriver | (() => Promise<SqlDriver>)) {
|
|
238
256
|
this.ready = this.init(driver);
|
|
239
257
|
this.live = new LiveStore(async () => { await this.ready; return this.driver; });
|
|
258
|
+
this.push = new PushStore(async () => { await this.ready; return this.driver; });
|
|
259
|
+
this.editor = new EditorStore(async () => { await this.ready; return this.driver; });
|
|
240
260
|
this.email = new EmailStore(async () => { await this.ready; return this.driver; });
|
|
241
261
|
}
|
|
242
262
|
|
|
@@ -466,6 +486,7 @@ export class SqlRepository implements ContentRepository {
|
|
|
466
486
|
if (data.seoTitle !== undefined) { sets.push('seo_title = ?'); args.push(data.seoTitle); }
|
|
467
487
|
if (data.seoDesc !== undefined) { sets.push('seo_desc = ?'); args.push(data.seoDesc); }
|
|
468
488
|
if (data.template !== undefined) { sets.push('template = ?'); args.push(data.template); }
|
|
489
|
+
if (data.translations !== undefined) { sets.push('translations = ?'); args.push(JSON.stringify(data.translations)); }
|
|
469
490
|
if (data.blocks !== undefined) { sets.push('blocks = ?'); args.push(JSON.stringify(data.blocks)); }
|
|
470
491
|
if (data.bodyJson !== undefined) { sets.push('body_json = ?'); args.push(data.bodyJson); }
|
|
471
492
|
if (data.bodyHtml !== undefined) { sets.push('body_html = ?'); args.push(data.bodyHtml); }
|
|
@@ -598,6 +619,7 @@ export class SqlRepository implements ContentRepository {
|
|
|
598
619
|
if (data.seoDesc !== undefined) { sets.push('seo_desc = ?'); args.push(data.seoDesc); }
|
|
599
620
|
if (data.featured !== undefined) { sets.push('featured = ?'); args.push(data.featured ? 1 : 0); }
|
|
600
621
|
if (data.template !== undefined) { sets.push('template = ?'); args.push(data.template); }
|
|
622
|
+
if (data.translations !== undefined) { sets.push('translations = ?'); args.push(JSON.stringify(data.translations)); }
|
|
601
623
|
// authorIds is the source of truth; author_id mirrors the first entry.
|
|
602
624
|
// Main row, join rows and webhook notifications share one transaction.
|
|
603
625
|
let authorJoin: string[] | null = null;
|
|
@@ -798,8 +820,8 @@ export class SqlRepository implements ContentRepository {
|
|
|
798
820
|
await this.driver.run(
|
|
799
821
|
`INSERT INTO events (id, slug, title, body_json, body_html, excerpt, cover, cover_alt, seo_title, seo_desc, status, featured, tags, template,
|
|
800
822
|
starts, ends, all_day, timezone, rrule, exdates, cancelled, location, cta, calendar_ids, fields, source_uid, start_at, end_at,
|
|
801
|
-
published_at, scheduled_at, created_at, updated_at, rsvp)
|
|
802
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
823
|
+
published_at, scheduled_at, created_at, updated_at, rsvp, room_hold)
|
|
824
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
803
825
|
[
|
|
804
826
|
id, data.slug, data.title, data.bodyJson ?? '', data.bodyHtml ?? '', data.excerpt ?? '', data.cover ?? '', data.coverAlt ?? '',
|
|
805
827
|
data.seoTitle ?? '', data.seoDesc ?? '', data.status ?? 'draft', data.featured ? 1 : 0, JSON.stringify(data.tags ?? []), data.template ?? '',
|
|
@@ -807,7 +829,7 @@ export class SqlRepository implements ContentRepository {
|
|
|
807
829
|
data.cancelled ? 1 : 0, JSON.stringify(data.location ?? {}), JSON.stringify(data.cta ?? {}), JSON.stringify(data.calendarIds ?? []),
|
|
808
830
|
JSON.stringify(data.fields ?? {}), data.sourceUid ?? '', startAt, endAt,
|
|
809
831
|
data.publishedAt ?? (data.status === 'published' ? now : null), data.scheduledAt ?? null, now, now,
|
|
810
|
-
JSON.stringify(data.rsvp ?? {}),
|
|
832
|
+
JSON.stringify(data.rsvp ?? {}), JSON.stringify(data.roomHold ?? {}),
|
|
811
833
|
]
|
|
812
834
|
);
|
|
813
835
|
await this.syncEventIndex(id);
|
|
@@ -832,7 +854,7 @@ export class SqlRepository implements ContentRepository {
|
|
|
832
854
|
if (v !== undefined) { sets.push(`${col} = ?`); args.push(v); }
|
|
833
855
|
}
|
|
834
856
|
const jsonCols: Record<string, string> = {
|
|
835
|
-
tags: 'tags', exdates: 'exdates', location: 'location', cta: 'cta', calendarIds: 'calendar_ids', fields: 'fields', rsvp: 'rsvp',
|
|
857
|
+
tags: 'tags', exdates: 'exdates', location: 'location', cta: 'cta', calendarIds: 'calendar_ids', fields: 'fields', rsvp: 'rsvp', translations: 'translations', roomHold: 'room_hold',
|
|
836
858
|
};
|
|
837
859
|
for (const [k, col] of Object.entries(jsonCols)) {
|
|
838
860
|
const v = (data as Record<string, unknown>)[k];
|
|
@@ -923,10 +945,11 @@ export class SqlRepository implements ContentRepository {
|
|
|
923
945
|
return {
|
|
924
946
|
sql: `INSERT INTO booking_links (id, resource_id, slug, name, description, mode, duration, lengths, min_days, max_days, step, buffer_before,
|
|
925
947
|
buffer_after, min_notice, max_advance, per_day_cap, hours, auto_confirm, visibility, ask_phone, questions, confirm_message, redirect_url,
|
|
926
|
-
active, ord, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
948
|
+
active, ord, created_at, updated_at, approval, approval_rules) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
927
949
|
args: [l.id, l.resourceId, l.slug, l.name, l.description, l.mode, l.duration, JSON.stringify(l.lengths), l.minDays, l.maxDays, l.step,
|
|
928
950
|
l.bufferBefore, l.bufferAfter, l.minNotice, l.maxAdvance, l.perDayCap, l.hours ? JSON.stringify(l.hours) : null, l.autoConfirm ? 1 : 0,
|
|
929
|
-
l.visibility, l.askPhone ? 1 : 0, JSON.stringify(l.questions), l.confirmMessage, l.redirectUrl, l.active ? 1 : 0, l.order, l.createdAt, l.updatedAt
|
|
951
|
+
l.visibility, l.askPhone ? 1 : 0, JSON.stringify(l.questions), l.confirmMessage, l.redirectUrl, l.active ? 1 : 0, l.order, l.createdAt, l.updatedAt,
|
|
952
|
+
l.approval, JSON.stringify(l.approvalRules)],
|
|
930
953
|
};
|
|
931
954
|
}
|
|
932
955
|
private bookingInsert(b: Booking): SqlStatement {
|
|
@@ -1004,7 +1027,7 @@ export class SqlRepository implements ContentRepository {
|
|
|
1004
1027
|
await this.ready;
|
|
1005
1028
|
const now = Date.now();
|
|
1006
1029
|
const next = await this.first('SELECT COALESCE(MAX(ord), -1) + 1 AS n FROM booking_links WHERE resource_id = ?', [data.resourceId]);
|
|
1007
|
-
const l = BookingLink.parse({ ...data, id: crypto.randomUUID(), order: Number(next?.n ?? 0), createdAt: now, updatedAt: now });
|
|
1030
|
+
const l = BookingLink.parse({ ...legacyApproval(data), id: crypto.randomUUID(), order: Number(next?.n ?? 0), createdAt: now, updatedAt: now });
|
|
1008
1031
|
const st = this.linkInsert(l);
|
|
1009
1032
|
await this.driver.run(st.sql, st.args);
|
|
1010
1033
|
return l;
|
|
@@ -1013,14 +1036,14 @@ export class SqlRepository implements ContentRepository {
|
|
|
1013
1036
|
await this.ready;
|
|
1014
1037
|
const cur = await this.getBookingLink(id);
|
|
1015
1038
|
if (!cur) return;
|
|
1016
|
-
const l = BookingLink.parse({ ...cur, ...data, id, updatedAt: Date.now() });
|
|
1039
|
+
const l = BookingLink.parse({ ...cur, ...legacyApproval(data, true), id, updatedAt: Date.now() });
|
|
1017
1040
|
await this.driver.run(
|
|
1018
1041
|
`UPDATE booking_links SET slug = ?, name = ?, description = ?, mode = ?, duration = ?, lengths = ?, min_days = ?, max_days = ?, step = ?,
|
|
1019
1042
|
buffer_before = ?, buffer_after = ?, min_notice = ?, max_advance = ?, per_day_cap = ?, hours = ?, auto_confirm = ?, visibility = ?,
|
|
1020
|
-
ask_phone = ?, questions = ?, confirm_message = ?, redirect_url = ?, active = ?, ord = ?, updated_at = ? WHERE id = ?`,
|
|
1043
|
+
ask_phone = ?, questions = ?, confirm_message = ?, redirect_url = ?, active = ?, ord = ?, updated_at = ?, approval = ?, approval_rules = ? WHERE id = ?`,
|
|
1021
1044
|
[l.slug, l.name, l.description, l.mode, l.duration, JSON.stringify(l.lengths), l.minDays, l.maxDays, l.step, l.bufferBefore, l.bufferAfter,
|
|
1022
1045
|
l.minNotice, l.maxAdvance, l.perDayCap, l.hours ? JSON.stringify(l.hours) : null, l.autoConfirm ? 1 : 0, l.visibility, l.askPhone ? 1 : 0,
|
|
1023
|
-
JSON.stringify(l.questions), l.confirmMessage, l.redirectUrl, l.active ? 1 : 0, l.order, l.updatedAt, id]
|
|
1046
|
+
JSON.stringify(l.questions), l.confirmMessage, l.redirectUrl, l.active ? 1 : 0, l.order, l.updatedAt, l.approval, JSON.stringify(l.approvalRules), id]
|
|
1024
1047
|
);
|
|
1025
1048
|
}
|
|
1026
1049
|
async deleteBookingLink(id: string) {
|
|
@@ -1035,6 +1058,7 @@ export class SqlRepository implements ContentRepository {
|
|
|
1035
1058
|
const args: unknown[] = [];
|
|
1036
1059
|
if (filter.resourceId) { where.push('resource_id = ?'); args.push(filter.resourceId); }
|
|
1037
1060
|
if (filter.eventId) { where.push('event_id = ?'); args.push(filter.eventId); }
|
|
1061
|
+
if (filter.email) { where.push('email = ?'); args.push(filter.email.toLowerCase()); }
|
|
1038
1062
|
if (filter.from !== undefined) { where.push('end_at > ?'); args.push(filter.from); }
|
|
1039
1063
|
if (filter.to !== undefined) { where.push('start_at < ?'); args.push(filter.to); }
|
|
1040
1064
|
if (filter.statuses?.length) { where.push(`status IN (${filter.statuses.map(() => '?').join(', ')})`); args.push(...filter.statuses); }
|
|
@@ -1601,14 +1625,14 @@ export class SqlRepository implements ContentRepository {
|
|
|
1601
1625
|
...(content.events ?? []).map((e) => ({
|
|
1602
1626
|
sql: `INSERT INTO events (id, slug, title, body_json, body_html, excerpt, cover, cover_alt, seo_title, seo_desc, status, featured, tags, template,
|
|
1603
1627
|
starts, ends, all_day, timezone, rrule, exdates, cancelled, location, cta, calendar_ids, fields, source_uid, start_at, end_at,
|
|
1604
|
-
draft, published_at, scheduled_at, created_at, updated_at, rsvp)
|
|
1605
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1628
|
+
draft, published_at, scheduled_at, created_at, updated_at, rsvp, room_hold)
|
|
1629
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1606
1630
|
args: [
|
|
1607
1631
|
e.id, e.slug, e.title, e.bodyJson, e.bodyHtml, e.excerpt, e.cover, e.coverAlt, e.seoTitle, e.seoDesc, e.status, e.featured ? 1 : 0,
|
|
1608
1632
|
JSON.stringify(e.tags), e.template, e.start, e.end, e.allDay ? 1 : 0, e.timezone, e.rrule, JSON.stringify(e.exdates),
|
|
1609
1633
|
e.cancelled ? 1 : 0, JSON.stringify(e.location), JSON.stringify(e.cta), JSON.stringify(e.calendarIds), JSON.stringify(e.fields),
|
|
1610
1634
|
e.sourceUid, e.startAt, e.endAt, e.draft ? JSON.stringify(e.draft) : null, e.publishedAt, e.scheduledAt, e.createdAt, e.updatedAt,
|
|
1611
|
-
JSON.stringify(e.rsvp ?? {}),
|
|
1635
|
+
JSON.stringify(e.rsvp ?? {}), JSON.stringify(e.roomHold ?? {}),
|
|
1612
1636
|
],
|
|
1613
1637
|
})),
|
|
1614
1638
|
...(content.resources ?? []).map((r) => this.resourceInsert(r)),
|
|
@@ -1633,6 +1657,10 @@ export class SqlRepository implements ContentRepository {
|
|
|
1633
1657
|
args: [p.id, aid, i] as unknown[],
|
|
1634
1658
|
}))
|
|
1635
1659
|
),
|
|
1660
|
+
// translations (v36): written after the rows so older backups need no change
|
|
1661
|
+
...[['pages', content.pages], ['posts', content.posts], ['events', content.events ?? []]].flatMap(([table, rows]) =>
|
|
1662
|
+
(rows as { id: string; translations?: Record<string, unknown> }[]).filter((r) => r.translations && Object.keys(r.translations).length)
|
|
1663
|
+
.map((r) => ({ sql: `UPDATE ${table} SET translations = ? WHERE id = ?`, args: [JSON.stringify(r.translations), r.id] as unknown[] }))),
|
|
1636
1664
|
...content.media.map((m) => ({
|
|
1637
1665
|
sql: 'INSERT INTO media (id, key, filename, mime, width, height, size, alt, variants, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
1638
1666
|
args: [m.id, m.key, m.filename, m.mime, m.width, m.height, m.size, m.alt, JSON.stringify(m.variants), m.createdAt],
|
|
@@ -1818,6 +1846,8 @@ export class SqlRepository implements ContentRepository {
|
|
|
1818
1846
|
timezone: r?.timezone ?? '', locale: r?.locale ?? '',
|
|
1819
1847
|
ai: JSON.parse(String(r?.ai ?? '{}')),
|
|
1820
1848
|
live: JSON.parse(String(r?.live ?? '{}')),
|
|
1849
|
+
push: JSON.parse(String(r?.push ?? '{}')),
|
|
1850
|
+
calendar: JSON.parse(String(r?.calendar ?? '{}')),
|
|
1821
1851
|
agent: JSON.parse(String(r?.agent ?? '{}')),
|
|
1822
1852
|
draftsApiKey: r?.drafts_api_key ?? '',
|
|
1823
1853
|
draftsAllowedOrigins: JSON.parse(String(r?.drafts_allowed_origins ?? '[]')),
|
|
@@ -1834,9 +1864,9 @@ export class SqlRepository implements ContentRepository {
|
|
|
1834
1864
|
siteTitle:'site_title', tagline:'tagline', footerText:'footer_text', linksName:'links_name', linksBio:'links_bio', linksAvatar:'links_avatar',
|
|
1835
1865
|
activeTheme:'active_theme', designOverrides:'design_overrides', sectionPages:'section_pages', siteLogo:'site_logo', siteUrl:'site_url', siteFavicon:'site_favicon', siteCover:'site_cover',
|
|
1836
1866
|
announcement:'announcement', analytics:'analytics', headCode:'head_code', footerCode:'footer_code', timezone:'timezone', locale:'locale',
|
|
1837
|
-
draftsApiKey:'drafts_api_key', draftsAllowedOrigins:'drafts_allowed_origins', draftDesign:'draft_design', ai:'ai', agent:'agent', live:'live',
|
|
1867
|
+
draftsApiKey:'drafts_api_key', draftsAllowedOrigins:'drafts_allowed_origins', draftDesign:'draft_design', ai:'ai', agent:'agent', live:'live', push:'push', calendar:'calendar',
|
|
1838
1868
|
};
|
|
1839
|
-
const jsonKeys = new Set(['designOverrides','sectionPages','announcement','analytics','draftsAllowedOrigins','draftDesign','ai','agent','live']);
|
|
1869
|
+
const jsonKeys = new Set(['designOverrides','sectionPages','announcement','analytics','draftsAllowedOrigins','draftDesign','ai','agent','live','push','calendar']);
|
|
1840
1870
|
const entries = Object.entries(data).filter(([k,v]) => columns[k] && v !== undefined);
|
|
1841
1871
|
if (!entries.length) return;
|
|
1842
1872
|
const design = entries.some(([k]) => ['activeTheme','designOverrides','sectionPages','siteLogo','siteFavicon','siteCover','draftDesign'].includes(k));
|
|
@@ -1901,6 +1931,21 @@ export class SqlRepository implements ContentRepository {
|
|
|
1901
1931
|
} }));
|
|
1902
1932
|
await this.designWrite(opts.expectedRevision ?? current.designRevision, opts.author ?? 'admin', 'publish', statements);
|
|
1903
1933
|
}
|
|
1934
|
+
async publishSectionPages(live: Settings['sectionPages'], staged: Settings['sectionPages'] | null, opts: { author?: string; summary?: string; expectedRevision: number }) {
|
|
1935
|
+
const current = await this.getSettings();
|
|
1936
|
+
const liveNav = await this.listNavItems();
|
|
1937
|
+
const draft: Record<string, unknown> = { ...(current.draftDesign ?? {}) };
|
|
1938
|
+
if (staged) draft.sectionPages = staged; else delete draft.sectionPages;
|
|
1939
|
+
// a draft with nothing left to publish goes away entirely
|
|
1940
|
+
const remaining = Object.keys(pickDesignDraft(draft)).length > 0 || Array.isArray(draft.navItems);
|
|
1941
|
+
const statements: SqlStatement[] = [
|
|
1942
|
+
{ sql: 'INSERT INTO design_versions (id,created_at,author,summary,snapshot) VALUES (?,?,?,?,?)', args: [crypto.randomUUID(), Date.now(), opts.author ?? '', opts.summary ?? '', JSON.stringify(designSnapshot(current, liveNav))] },
|
|
1943
|
+
{ sql: 'DELETE FROM design_versions WHERE id NOT IN (SELECT id FROM design_versions ORDER BY created_at DESC LIMIT 20)' },
|
|
1944
|
+
{ sql: 'UPDATE settings SET section_pages=?,draft_design=? WHERE id=1', args: [JSON.stringify(live), remaining ? JSON.stringify({ ...draft, savedAt: Date.now() }) : null] },
|
|
1945
|
+
webhookStatement('design.published', { design: { revision: current.designRevision + 1, activeTheme: current.activeTheme, navigationChanged: false, sectionPageIds: Object.keys(live) } }),
|
|
1946
|
+
];
|
|
1947
|
+
await this.designWrite(opts.expectedRevision, opts.author ?? 'admin', 'publish', statements);
|
|
1948
|
+
}
|
|
1904
1949
|
async discardDesignDraft() { await this.setDesignDraft(null); }
|
|
1905
1950
|
async listDesignVersions(limit = 20) {
|
|
1906
1951
|
await this.ready;
|
package/src/lib/repo/types.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { LiveStore } from '../live/store';
|
|
2
2
|
import type { BookingRequest } from '../backup/content';
|
|
3
3
|
import type { EmailStore } from '../email/store';
|
|
4
|
+
import type { PushStore } from '../push/store';
|
|
5
|
+
import type { EditorStore } from '../editor/store';
|
|
4
6
|
export interface ImportRun { id: string; source: string; label: string; status: 'running' | 'done' | 'undone'; report: Record<string, unknown>; createdAt: number; updatedAt: number }
|
|
5
7
|
export type ImportRecordKind = 'post' | 'page' | 'user' | 'tag' | 'media' | 'redirect' | 'nav';
|
|
6
8
|
export interface ImportRecord { runId: string; kind: ImportRecordKind; sourceId: string; acmsId: string; action: 'created' | 'updated'; stamp: number }
|
|
@@ -37,7 +39,7 @@ export type EventUpdate = Partial<
|
|
|
37
39
|
| 'title' | 'slug' | 'bodyJson' | 'bodyHtml' | 'excerpt' | 'cover' | 'coverAlt'
|
|
38
40
|
| 'seoTitle' | 'seoDesc' | 'status' | 'featured' | 'tags' | 'template'
|
|
39
41
|
| 'start' | 'end' | 'allDay' | 'timezone' | 'rrule' | 'exdates' | 'cancelled'
|
|
40
|
-
| 'location' | 'cta' | 'calendarIds' | 'fields' | 'sourceUid' | 'rsvp'
|
|
42
|
+
| 'location' | 'cta' | 'calendarIds' | 'fields' | 'sourceUid' | 'rsvp' | 'translations' | 'roomHold'
|
|
41
43
|
| 'publishedAt' | 'scheduledAt'
|
|
42
44
|
>
|
|
43
45
|
>;
|
|
@@ -47,6 +49,8 @@ export type EventUpdate = Partial<
|
|
|
47
49
|
export interface ContentRepository {
|
|
48
50
|
readonly email: EmailStore;
|
|
49
51
|
readonly live: LiveStore;
|
|
52
|
+
readonly push: PushStore;
|
|
53
|
+
readonly editor: EditorStore;
|
|
50
54
|
// Bounded public catalog; fetch limit + 1 IDs without loading every body.
|
|
51
55
|
listPublicContent(options: { page: number; limit: number; kind?: 'post' | 'page'; collection?: string; locale?: string; collections: string[]; defaultLocale: string }): Promise<{ id: string; kind: 'post' | 'page' }[]>;
|
|
52
56
|
exportBookingState(): Promise<{ bookings: Booking[]; bookingRequests: BookingRequest[] }>;
|
|
@@ -60,7 +64,7 @@ export interface ContentRepository {
|
|
|
60
64
|
createPage(data: { title: string; slug: string }): Promise<Page>;
|
|
61
65
|
updatePage(
|
|
62
66
|
id: string,
|
|
63
|
-
data: Partial<Pick<Page, 'title' | 'slug' | 'status' | 'isHome' | 'locale' | 'seoTitle' | 'seoDesc' | 'bodyJson' | 'bodyHtml' | 'template'>> & {
|
|
67
|
+
data: Partial<Pick<Page, 'title' | 'slug' | 'status' | 'isHome' | 'locale' | 'seoTitle' | 'seoDesc' | 'bodyJson' | 'bodyHtml' | 'template' | 'translations'>> & {
|
|
64
68
|
blocks?: WidgetInstance[];
|
|
65
69
|
}
|
|
66
70
|
): Promise<void>;
|
|
@@ -90,7 +94,7 @@ export interface ContentRepository {
|
|
|
90
94
|
| 'title' | 'slug' | 'collection' | 'body' | 'bodyJson' | 'bodyHtml' | 'excerpt'
|
|
91
95
|
| 'tags' | 'cover' | 'coverAlt' | 'seoTitle' | 'seoDesc'
|
|
92
96
|
| 'status' | 'featured' | 'authorId' | 'authorIds' | 'template'
|
|
93
|
-
| 'publishedAt' | 'scheduledAt'
|
|
97
|
+
| 'publishedAt' | 'scheduledAt' | 'translations'
|
|
94
98
|
>
|
|
95
99
|
>
|
|
96
100
|
): Promise<void>;
|
|
@@ -148,7 +152,7 @@ export interface ContentRepository {
|
|
|
148
152
|
// write that can fail on a race: it inserts atomically only if the held
|
|
149
153
|
// range is still free (resource bookings) or seats remain (RSVPs), and
|
|
150
154
|
// returns null when it lost.
|
|
151
|
-
listBookings(filter?: { resourceId?: string; eventId?: string; from?: number; to?: number; statuses?: BookingStatus[]; kinds?: Booking['kind'][] }): Promise<Booking[]>;
|
|
155
|
+
listBookings(filter?: { resourceId?: string; eventId?: string; email?: string; from?: number; to?: number; statuses?: BookingStatus[]; kinds?: Booking['kind'][] }): Promise<Booking[]>;
|
|
152
156
|
getBooking(id: string): Promise<Booking | null>;
|
|
153
157
|
getBookingByToken(token: string): Promise<Booking | null>;
|
|
154
158
|
createBooking(
|
|
@@ -298,6 +302,8 @@ export interface ContentRepository {
|
|
|
298
302
|
// publish: snapshot the outgoing live design into design_versions (last 20
|
|
299
303
|
// kept), apply the draft's settings + staged navigation, clear the draft
|
|
300
304
|
applyDesignDraft(opts?: { author?: string; summary?: string; expectedRevision?: number }): Promise<void>;
|
|
305
|
+
/** publish section layouts only (one section's edits); the rest of the draft stays staged */
|
|
306
|
+
publishSectionPages(live: Settings['sectionPages'], staged: Settings['sectionPages'] | null, opts: { author?: string; summary?: string; expectedRevision: number }): Promise<void>;
|
|
301
307
|
discardDesignDraft(): Promise<void>;
|
|
302
308
|
listDesignVersions(limit?: number): Promise<DesignVersion[]>;
|
|
303
309
|
getDesignVersion(id: string): Promise<DesignVersion | null>;
|
package/src/lib/roles.ts
ADDED
package/src/lib/scheduler.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { syncDueLive } from './live/service';
|
|
|
2
2
|
import type { ContentRepository } from './repo/types';
|
|
3
3
|
import { processDueDeliveries } from './webhooks';
|
|
4
4
|
import { dispatchEmail } from './email/service';
|
|
5
|
-
import { sendDueReminders } from './booking/service';
|
|
5
|
+
import { decideOverdue, sendDueReminders } from './booking/service';
|
|
6
|
+
import { runPushJobs } from './push/service';
|
|
6
7
|
|
|
7
8
|
// All work is awaited on both hosts. Traffic-triggered fallbacks do not mark
|
|
8
9
|
// this heartbeat: monitoring must prove the independent scheduler ran.
|
|
@@ -13,8 +14,12 @@ export async function runScheduledTasks(repo: ContentRepository, locals?: unknow
|
|
|
13
14
|
publish: () => repo.publishDuePosts(startedAt),
|
|
14
15
|
webhooks: () => processDueDeliveries(repo),
|
|
15
16
|
reminders: () => sendDueReminders(repo, { origin: settings.siteUrl, locals, now: startedAt }),
|
|
17
|
+
// pending bookings past their approval deadline (Calendar settings → Bookings)
|
|
18
|
+
approvals: () => decideOverdue(repo, { origin: settings.siteUrl, locals, now: startedAt }),
|
|
16
19
|
email: () => dispatchEmail(repo, locals, 10),
|
|
17
20
|
live: () => syncDueLive(repo, locals),
|
|
21
|
+
// queued first-publish notices, starting-soon reminders, receipts, retention
|
|
22
|
+
push: () => runPushJobs(repo, { locals, now: startedAt }),
|
|
18
23
|
};
|
|
19
24
|
const results: Record<string, { ok: boolean; count?: number }> = {};
|
|
20
25
|
for (const [name, run] of Object.entries(jobs)) {
|
package/src/lib/schema.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LiveConfig } from './live/types';
|
|
2
|
+
import { PushSettings } from './push/types';
|
|
2
3
|
import { SectionPages } from '../theme-sdk/contract.mjs';
|
|
3
4
|
import { AiConfig, AgentConfig } from './ai-schema';
|
|
4
5
|
import { z } from 'zod';
|
|
@@ -64,6 +65,26 @@ export function parseConfig<T extends WidgetType>(type: T, raw: unknown): z.infe
|
|
|
64
65
|
export const Status = z.enum(['draft', 'scheduled', 'published', 'archived']);
|
|
65
66
|
export type Status = z.infer<typeof Status>;
|
|
66
67
|
|
|
68
|
+
// Per-language words for a post, page or event (the facts stay shared):
|
|
69
|
+
// translations: { es: { title, excerpt, bodyHtml, seoTitle, seoDesc, slug,
|
|
70
|
+
// locationName, ctaLabel, meta: { title: { state, src } } } }
|
|
71
|
+
// state 'ai' = machine draft (shown, flagged for review), 'reviewed' = a person
|
|
72
|
+
// wrote/approved it; src = hash of the main-language text it was made from.
|
|
73
|
+
export const DocTranslation = z.object({
|
|
74
|
+
title: z.string().max(1000).default(''),
|
|
75
|
+
excerpt: z.string().max(5000).default(''),
|
|
76
|
+
bodyHtml: z.string().max(2_000_000).default(''),
|
|
77
|
+
seoTitle: z.string().max(1000).default(''),
|
|
78
|
+
seoDesc: z.string().max(5000).default(''),
|
|
79
|
+
slug: z.string().max(300).default(''),
|
|
80
|
+
locationName: z.string().max(500).default(''),
|
|
81
|
+
ctaLabel: z.string().max(300).default(''),
|
|
82
|
+
meta: z.record(z.string().max(40), z.object({ state: z.enum(['ai', 'reviewed']), src: z.string().max(20) })).default({}),
|
|
83
|
+
});
|
|
84
|
+
export type DocTranslation = z.infer<typeof DocTranslation>;
|
|
85
|
+
export const DocTranslations = z.record(z.string().regex(/^[a-z]{2,3}(-[a-z0-9]{2,8})?$/), DocTranslation).default({});
|
|
86
|
+
export type DocTranslations = z.infer<typeof DocTranslations>;
|
|
87
|
+
|
|
67
88
|
export const Page = z.object({
|
|
68
89
|
id: z.string(),
|
|
69
90
|
slug: z.string(),
|
|
@@ -83,6 +104,7 @@ export const Page = z.object({
|
|
|
83
104
|
// theme template name (templates/page-<name>.astro); empty = default layout
|
|
84
105
|
template: z.string().default(''),
|
|
85
106
|
// pending edits on a live page (see lib/drafts.ts); null = none staged
|
|
107
|
+
translations: DocTranslations,
|
|
86
108
|
draft: z.record(z.any()).nullable().default(null),
|
|
87
109
|
createdAt: z.number(),
|
|
88
110
|
updatedAt: z.number(),
|
|
@@ -117,6 +139,7 @@ export const Post = z.object({
|
|
|
117
139
|
// theme template name (templates/post-<name>.astro); empty = default layout
|
|
118
140
|
template: z.string().default(''),
|
|
119
141
|
// pending edits on a live post (see lib/drafts.ts); null = none staged
|
|
142
|
+
translations: DocTranslations,
|
|
120
143
|
draft: z.record(z.any()).nullable().default(null),
|
|
121
144
|
publishedAt: z.number().nullable(),
|
|
122
145
|
scheduledAt: z.number().nullable().default(null),
|
|
@@ -171,7 +194,7 @@ export const Snippet = z.object({
|
|
|
171
194
|
});
|
|
172
195
|
export type Snippet = z.infer<typeof Snippet>;
|
|
173
196
|
|
|
174
|
-
export const Role = z.enum(['
|
|
197
|
+
export const Role = z.enum(['contributor', 'author', 'editor', 'administrator', 'owner']);
|
|
175
198
|
export type Role = z.infer<typeof Role>;
|
|
176
199
|
|
|
177
200
|
// Password hashes never leave the repo layer — User is the public shape.
|
|
@@ -291,6 +314,17 @@ export const EventCta = z.object({
|
|
|
291
314
|
});
|
|
292
315
|
export type EventCta = z.infer<typeof EventCta>;
|
|
293
316
|
|
|
317
|
+
export const RoomHold = z.object({
|
|
318
|
+
resourceIds: z.array(z.string()).default([]),
|
|
319
|
+
span: z.enum(['event', 'day']).default('event'),
|
|
320
|
+
before: z.number().int().min(0).max(1440).default(0), // setup minutes
|
|
321
|
+
after: z.number().int().min(0).max(1440).default(0), // cleanup minutes
|
|
322
|
+
whileDraft: z.boolean().default(false),
|
|
323
|
+
// bookings you chose to keep (share the room): no longer flagged as conflicts
|
|
324
|
+
keep: z.array(z.string()).default([]),
|
|
325
|
+
});
|
|
326
|
+
export type RoomHold = z.infer<typeof RoomHold>;
|
|
327
|
+
|
|
294
328
|
export const CalEvent = z.object({
|
|
295
329
|
id: z.string(),
|
|
296
330
|
slug: z.string(),
|
|
@@ -332,8 +366,13 @@ export const CalEvent = z.object({
|
|
|
332
366
|
autoConfirm: z.boolean().default(true),
|
|
333
367
|
maxSeats: z.number().int().default(4),
|
|
334
368
|
}).default({}),
|
|
369
|
+
// rooms/people this event uses: they can't be booked while it happens (or
|
|
370
|
+
// all day). Computed into availability, never stored as bookings. Drafts
|
|
371
|
+
// only hold when whileDraft is on; cancelled events never hold.
|
|
372
|
+
roomHold: RoomHold.default({}),
|
|
335
373
|
startAt: z.number(),
|
|
336
374
|
endAt: z.number(),
|
|
375
|
+
translations: DocTranslations,
|
|
337
376
|
draft: z.record(z.any()).nullable().default(null),
|
|
338
377
|
publishedAt: z.number().nullable().default(null),
|
|
339
378
|
scheduledAt: z.number().nullable().default(null),
|
|
@@ -420,6 +459,36 @@ export type BookingQuestion = z.infer<typeof BookingQuestion>;
|
|
|
420
459
|
export const BookingMode = z.enum(['slots', 'range', 'day']);
|
|
421
460
|
export type BookingMode = z.infer<typeof BookingMode>;
|
|
422
461
|
|
|
462
|
+
// Booking approval: "auto" confirms at once, "manual" holds every booking for
|
|
463
|
+
// a decision, "rules" confirms unless one of the rules flags it. A link can
|
|
464
|
+
// follow the site default ("default") or pick its own. The deadline decides
|
|
465
|
+
// pending bookings nobody answered (0 hours = keep waiting).
|
|
466
|
+
export const ApprovalMode = z.enum(['auto', 'rules', 'manual']);
|
|
467
|
+
export type ApprovalMode = z.infer<typeof ApprovalMode>;
|
|
468
|
+
export const ApprovalRules = z.object({
|
|
469
|
+
// 0 = rule off
|
|
470
|
+
longerThan: z.number().int().min(0).default(0), // minutes
|
|
471
|
+
firstTime: z.boolean().default(false),
|
|
472
|
+
moreSeats: z.number().int().min(0).default(0),
|
|
473
|
+
deadlineHours: z.number().int().min(0).max(24 * 60).default(0),
|
|
474
|
+
deadlineAction: z.enum(['approve', 'decline']).default('approve'),
|
|
475
|
+
});
|
|
476
|
+
export type ApprovalRules = z.infer<typeof ApprovalRules>;
|
|
477
|
+
|
|
478
|
+
// Site-wide calendar preferences (Calendar settings → Defaults / Bookings)
|
|
479
|
+
export const CalendarSettings = z.object({
|
|
480
|
+
// first column of every month grid: 0 = Sunday, 1 = Monday
|
|
481
|
+
weekStart: z.union([z.literal(0), z.literal(1)]).default(0),
|
|
482
|
+
// calendar new events land in (quick add, New event); '' = none
|
|
483
|
+
defaultCalendarId: z.string().default(''),
|
|
484
|
+
// '' = not chosen in the admin yet → acms({ calendar.bookings.approval }) → auto
|
|
485
|
+
approval: z.union([ApprovalMode, z.literal('')]).default(''),
|
|
486
|
+
approvalRules: ApprovalRules.default({}),
|
|
487
|
+
// show bookings on the Schedule grid
|
|
488
|
+
showBookings: z.boolean().default(true),
|
|
489
|
+
});
|
|
490
|
+
export type CalendarSettings = z.infer<typeof CalendarSettings>;
|
|
491
|
+
|
|
423
492
|
export const BookingLink = z.object({
|
|
424
493
|
id: z.string(),
|
|
425
494
|
resourceId: z.string(),
|
|
@@ -442,6 +511,9 @@ export const BookingLink = z.object({
|
|
|
442
511
|
hours: WeeklyHours.nullable().default(null),
|
|
443
512
|
// confirmed at once (and on the calendar) vs. held as pending for approval
|
|
444
513
|
autoConfirm: z.boolean().default(true),
|
|
514
|
+
// supersedes autoConfirm: 'default' follows Calendar settings → Bookings
|
|
515
|
+
approval: z.union([ApprovalMode, z.literal('default')]).default('default'),
|
|
516
|
+
approvalRules: ApprovalRules.default({}),
|
|
445
517
|
visibility: z.enum(['public', 'unlisted']).default('public'),
|
|
446
518
|
askPhone: z.boolean().default(false),
|
|
447
519
|
questions: z.array(BookingQuestion).default([]),
|
|
@@ -560,6 +632,9 @@ export function normalizeRedirectPath(input: string): string {
|
|
|
560
632
|
|
|
561
633
|
export const Settings = z.object({
|
|
562
634
|
live: LiveConfig.default({}),
|
|
635
|
+
calendar: CalendarSettings.default({}),
|
|
636
|
+
// push notification automation (lib/push); the feature itself is acms({ mobile: { push } })
|
|
637
|
+
push: PushSettings.default({}),
|
|
563
638
|
siteTitle: z.string().default('My Site'),
|
|
564
639
|
tagline: z.string().default(''),
|
|
565
640
|
// site-wide footer, markdown allowed; empty falls back to "title — tagline"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import sanitizeHtml from 'sanitize-html';
|
|
3
|
+
import type { AiConfig } from './ai-schema';
|
|
4
|
+
import { completeText } from './ai';
|
|
5
|
+
import { ApiError } from './api';
|
|
6
|
+
|
|
7
|
+
// "Help me write" for section fields: rewrite one field's text with the
|
|
8
|
+
// site's configured AI provider. The owner reviews the suggestion before it
|
|
9
|
+
// replaces anything; nothing is saved here.
|
|
10
|
+
|
|
11
|
+
export const AssistInput = z.object({
|
|
12
|
+
action: z.enum(['shorter', 'friendlier', 'clearer', 'translate', 'draft']),
|
|
13
|
+
text: z.string().max(20_000).default(''),
|
|
14
|
+
label: z.string().max(100),
|
|
15
|
+
kind: z.enum(['text', 'textarea', 'richtext']).default('text'),
|
|
16
|
+
hint: z.string().max(300).optional(),
|
|
17
|
+
maxLength: z.number().int().min(1).max(20_000).optional(),
|
|
18
|
+
language: z.string().max(40).optional(),
|
|
19
|
+
// other filled-in fields of the same section, for context ("Heading: Join us")
|
|
20
|
+
context: z.string().max(4000).optional(),
|
|
21
|
+
}).strict();
|
|
22
|
+
|
|
23
|
+
const ACTIONS: Record<z.infer<typeof AssistInput>['action'], string> = {
|
|
24
|
+
shorter: 'Make it noticeably shorter while keeping every fact (names, days, times, places, prices).',
|
|
25
|
+
friendlier: 'Make it warmer and more welcoming, plain words, no hype or exclamation marks.',
|
|
26
|
+
clearer: 'Fix spelling and grammar and make it clearer. Change as little as possible.',
|
|
27
|
+
translate: 'Translate it into the requested language. Keep names of people and places as they are.',
|
|
28
|
+
draft: 'Write it from scratch for this field, using the other fields as context. Be concrete and plain.',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export async function assistField(config: AiConfig, raw: unknown): Promise<{ text: string }> {
|
|
32
|
+
const a = AssistInput.parse(raw);
|
|
33
|
+
if (a.action !== 'draft' && !a.text.trim()) throw new ApiError(400, 'Write something first, or use “Write a first draft”.');
|
|
34
|
+
if (a.action === 'translate' && !a.language) throw new ApiError(400, 'Choose a language.');
|
|
35
|
+
const rules = [
|
|
36
|
+
'You edit short text for a section of a website (for example a church, club or small business homepage).',
|
|
37
|
+
`The field is “${a.label}”.`,
|
|
38
|
+
ACTIONS[a.action],
|
|
39
|
+
a.action === 'translate' ? `Target language: ${a.language}.` : 'Keep the original language.',
|
|
40
|
+
a.hint ? `The site owner's guidance for this field: ${a.hint}` : '',
|
|
41
|
+
a.maxLength ? `Stay under ${a.maxLength} characters.` : '',
|
|
42
|
+
a.kind === 'richtext' ? 'Return simple HTML using only <p>, <strong>, <em>, <a href>, <ul>, <ol>, <li>. Keep existing links.' : 'Return plain text only.',
|
|
43
|
+
a.kind === 'text' ? 'One line, no line breaks.' : '',
|
|
44
|
+
'Return only the new text: no quotes, labels, notes or alternatives. Never invent facts, dates, prices or contact details.',
|
|
45
|
+
].filter(Boolean).join('\n');
|
|
46
|
+
const input = [a.context ? `Other fields in this section:\n${a.context}` : '', a.text ? `Current text:\n${a.text}` : 'The field is empty.'].filter(Boolean).join('\n\n');
|
|
47
|
+
let out = (await completeText(config, rules, input)).replace(/^```\w*\n?|\n?```$/g, '').trim();
|
|
48
|
+
if (/^["“].*["”]$/s.test(out)) out = out.slice(1, -1).trim();
|
|
49
|
+
out = a.kind === 'richtext'
|
|
50
|
+
? sanitizeHtml(out, { allowedTags: ['p', 'br', 'strong', 'b', 'em', 'i', 'a', 'ul', 'ol', 'li'], allowedAttributes: { a: ['href'] }, allowedSchemes: ['http', 'https', 'mailto', 'tel'] })
|
|
51
|
+
: sanitizeHtml(out, { allowedTags: [], allowedAttributes: {} }).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'");
|
|
52
|
+
if (a.kind === 'text') out = out.replace(/\s*\n+\s*/g, ' ');
|
|
53
|
+
if (!out) throw new ApiError(502, 'The AI returned nothing useful. Try again.');
|
|
54
|
+
return { text: out };
|
|
55
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ContentRepository } from './repo/types';
|
|
2
|
+
import { CONTENT, collectionUrl } from './content';
|
|
3
|
+
import { I18N, extraLocales, localeName } from './i18n';
|
|
4
|
+
import { translationEngine } from './translate';
|
|
5
|
+
import { CALENDAR, bookingLinkUrl, bookingsEnabled, eventUrl } from './calendar/config';
|
|
6
|
+
|
|
7
|
+
const NAMES: Record<string, string> = { en: 'English', es: 'Spanish', fr: 'French', de: 'German', pt: 'Portuguese', it: 'Italian', nl: 'Dutch', ko: 'Korean', zh: 'Chinese', ja: 'Japanese', tl: 'Tagalog', vi: 'Vietnamese', ar: 'Arabic', ru: 'Russian', uk: 'Ukrainian', pl: 'Polish' };
|
|
8
|
+
// the site's own languages first, then common ones
|
|
9
|
+
const languageNames = () => [...new Set([...(I18N.enabled ? I18N.locales : []).map((l) => NAMES[l.split('-')[0]] ?? l), 'English', 'Spanish', 'French', 'Portuguese', 'Korean', 'Chinese'])];
|
|
10
|
+
|
|
11
|
+
// Choices for the Design screen's section fields: what a link field can point
|
|
12
|
+
// at (stored as a ref so it follows slug changes) and what a source field can
|
|
13
|
+
// show. Published/public things only — this is what visitors can reach.
|
|
14
|
+
|
|
15
|
+
export interface LinkTarget { ref: string; label: string; url: string; group: 'Pages' | 'Posts' | 'Events' | 'Booking' }
|
|
16
|
+
export interface SectionTargets {
|
|
17
|
+
links: LinkTarget[];
|
|
18
|
+
calendars: { slug: string; name: string }[];
|
|
19
|
+
collections: { key: string; label: string }[];
|
|
20
|
+
tags: string[];
|
|
21
|
+
resources: { slug: string; name: string }[];
|
|
22
|
+
features: { events: boolean; bookings: boolean };
|
|
23
|
+
/** AI help for text fields: provider configured + languages to translate into */
|
|
24
|
+
ai: { enabled: boolean; languages: string[] };
|
|
25
|
+
/** extra site languages to translate into, and whether machine drafts are available */
|
|
26
|
+
i18n: { defaultLocale: string; locales: { code: string; name: string }[]; drafts: boolean };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function sectionTargets(repo: ContentRepository, locals?: unknown): Promise<SectionTargets> {
|
|
30
|
+
const bookings = bookingsEnabled();
|
|
31
|
+
const settings = await repo.getSettings();
|
|
32
|
+
const [pages, posts, events, calendars, tags, resources, links] = await Promise.all([
|
|
33
|
+
repo.listPages(),
|
|
34
|
+
repo.listPublishedPosts(),
|
|
35
|
+
CALENDAR.enabled ? repo.listPublishedEvents() : Promise.resolve([]),
|
|
36
|
+
CALENDAR.enabled ? repo.listCalendars() : Promise.resolve([]),
|
|
37
|
+
repo.listTags(),
|
|
38
|
+
bookings ? repo.listResources() : Promise.resolve([]),
|
|
39
|
+
bookings ? repo.listBookingLinks() : Promise.resolve([]),
|
|
40
|
+
]);
|
|
41
|
+
const publicResources = resources.filter((r) => r.visibility === 'public');
|
|
42
|
+
const bySlug = new Map(resources.map((r) => [r.id, r]));
|
|
43
|
+
const out: LinkTarget[] = [
|
|
44
|
+
...pages.filter((p) => p.status === 'published').map((p): LinkTarget => ({ ref: `page:${p.id}`, label: p.title || p.slug, url: p.isHome ? '/' : `/${p.slug}`, group: 'Pages' })),
|
|
45
|
+
...posts.slice(0, 200).map((p): LinkTarget => ({ ref: `post:${p.id}`, label: p.title, url: collectionUrl(p.collection, p.slug), group: 'Posts' })),
|
|
46
|
+
...events.slice(0, 200).map((e): LinkTarget => ({ ref: `event:${e.id}`, label: e.title, url: eventUrl(e.slug), group: 'Events' })),
|
|
47
|
+
...links.filter((l) => l.active && bySlug.has(l.resourceId)).map((l): LinkTarget => {
|
|
48
|
+
const r = bySlug.get(l.resourceId)!;
|
|
49
|
+
return { ref: `booking:${l.id}`, label: `${r.name} · ${l.name}`, url: bookingLinkUrl(r.slug, l.slug), group: 'Booking' };
|
|
50
|
+
}),
|
|
51
|
+
];
|
|
52
|
+
return {
|
|
53
|
+
links: out,
|
|
54
|
+
calendars: calendars.filter((c) => c.visibility !== 'private').map((c) => ({ slug: c.slug, name: c.name })),
|
|
55
|
+
collections: Object.entries(CONTENT.collections).map(([key, c]) => ({ key, label: c.plural })),
|
|
56
|
+
tags: [...new Set(tags.map((t) => t.name))].sort((a, b) => a.localeCompare(b)),
|
|
57
|
+
resources: publicResources.map((r) => ({ slug: r.slug, name: r.name })),
|
|
58
|
+
features: { events: CALENDAR.enabled, bookings },
|
|
59
|
+
ai: { enabled: Boolean(settings.ai.enabled && settings.ai.model), languages: languageNames() },
|
|
60
|
+
i18n: { defaultLocale: I18N.defaultLocale, locales: extraLocales().map((code) => ({ code, name: localeName(code, true) })), drafts: translationEngine(settings.ai, locals) !== null },
|
|
61
|
+
};
|
|
62
|
+
}
|