@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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useMemo, useState } from 'react';
|
|
2
2
|
import type { EventCta, EventLocation } from '../../lib/schema';
|
|
3
3
|
import type { EventFieldConfig } from '../../lib/calendar/config';
|
|
4
|
+
import WhereItLands from '../calendar/WhereItLands';
|
|
5
|
+
import RoomHolds, { type RoomHoldValue } from '../calendar/RoomHolds';
|
|
4
6
|
import { buildRepeat, describeRepeat, parseRepeat, WEEKDAY_CODES, weekdayOf, type RepeatParts } from '../../lib/calendar/rrule';
|
|
5
7
|
|
|
6
8
|
// The event half of the editor sidebar: When · Repeats · Where · Button ·
|
|
@@ -21,6 +23,7 @@ export interface EventMeta {
|
|
|
21
23
|
calendarIds: string[];
|
|
22
24
|
fields: Record<string, string>;
|
|
23
25
|
rsvp: { enabled: boolean; capacity: number; autoConfirm: boolean; maxSeats: number };
|
|
26
|
+
roomHold: RoomHoldValue;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export interface CalendarOption {
|
|
@@ -35,6 +38,11 @@ interface Props {
|
|
|
35
38
|
calendars: CalendarOption[];
|
|
36
39
|
fields: EventFieldConfig[];
|
|
37
40
|
siteTimezone: string;
|
|
41
|
+
// shows the "where it lands" mini month (Schedule data, minus this event)
|
|
42
|
+
eventId?: string;
|
|
43
|
+
// for the Rooms section (conflict wording, "Hold now" on drafts)
|
|
44
|
+
title?: string;
|
|
45
|
+
status?: string;
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
const DAY_LETTERS = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
|
@@ -64,7 +72,7 @@ function zoneList(): string[] {
|
|
|
64
72
|
}
|
|
65
73
|
}
|
|
66
74
|
|
|
67
|
-
export function EventPanel({ meta, onChange, calendars, fields, siteTimezone }: Props) {
|
|
75
|
+
export function EventPanel({ meta, onChange, calendars, fields, siteTimezone, eventId, title = '', status = 'draft' }: Props) {
|
|
68
76
|
const repeat = useMemo(() => parseRepeat(meta.rrule, meta.start), [meta.rrule, meta.start]);
|
|
69
77
|
const [skipDraft, setSkipDraft] = useState('');
|
|
70
78
|
const zones = useMemo(zoneList, []);
|
|
@@ -136,6 +144,10 @@ export function EventPanel({ meta, onChange, calendars, fields, siteTimezone }:
|
|
|
136
144
|
{!meta.allDay && !showEndDate && (
|
|
137
145
|
<button type="button" className="pe-ev-link" onClick={() => setShowEndDate(true)}>Ends on another day</button>
|
|
138
146
|
)}
|
|
147
|
+
{eventId && datePart(meta.start) && (
|
|
148
|
+
<WhereItLands date={datePart(meta.start)} eventId={eventId}
|
|
149
|
+
onPick={(d) => setStart(meta.allDay ? d : `${d}T${timePart(meta.start) || '10:00'}`)} />
|
|
150
|
+
)}
|
|
139
151
|
<label className="pe-switch-row">
|
|
140
152
|
<span><b>All day</b><small>No start or end time</small></span>
|
|
141
153
|
<input type="checkbox" role="switch" className="pe-switch" checked={meta.allDay} onChange={(e) => toggleAllDay(e.target.checked)} />
|
|
@@ -277,6 +289,15 @@ export function EventPanel({ meta, onChange, calendars, fields, siteTimezone }:
|
|
|
277
289
|
</label>
|
|
278
290
|
</section>
|
|
279
291
|
|
|
292
|
+
<RoomHolds
|
|
293
|
+
times={{ eventId: eventId ?? '', title, start: meta.start, end: meta.end, allDay: meta.allDay, timezone: meta.timezone, rrule: meta.rrule, exdates: meta.exdates }}
|
|
294
|
+
hold={meta.roomHold}
|
|
295
|
+
status={status}
|
|
296
|
+
locationName={meta.location.name}
|
|
297
|
+
onChange={(roomHold) => onChange({ roomHold })}
|
|
298
|
+
onLocation={(name) => onChange({ location: { ...meta.location, name } })}
|
|
299
|
+
/>
|
|
300
|
+
|
|
280
301
|
<section className="pe-sb-section pe-ev">
|
|
281
302
|
<h3 className="pe-sb-title">Button</h3>
|
|
282
303
|
<div className="pe-ev-row">
|
|
@@ -327,7 +348,7 @@ export function EventPanel({ meta, onChange, calendars, fields, siteTimezone }:
|
|
|
327
348
|
<section className="pe-sb-section pe-ev">
|
|
328
349
|
<h3 className="pe-sb-title">Calendars</h3>
|
|
329
350
|
{calendars.length === 0 ? (
|
|
330
|
-
<p className="pe-ev-note">No calendars yet — the event shows on the main calendar. <a href="/admin/calendar/
|
|
351
|
+
<p className="pe-ev-note">No calendars yet — the event shows on the main calendar. <a href="/admin/calendar/settings?new=1" target="_blank" rel="noopener">Create one</a> to group events.</p>
|
|
331
352
|
) : (
|
|
332
353
|
<div className="pe-ev-cals">
|
|
333
354
|
{calendars.map((c) => {
|
|
@@ -7,7 +7,8 @@ import Highlight from '@tiptap/extension-highlight';
|
|
|
7
7
|
import { TableKit } from '@tiptap/extension-table';
|
|
8
8
|
import { Placeholder, CharacterCount } from '@tiptap/extensions';
|
|
9
9
|
import { actions } from 'astro:actions';
|
|
10
|
-
import type { CalEvent, Page, Post, Status } from '../../lib/schema';
|
|
10
|
+
import type { CalEvent, DocTranslations, Page, Post, Status } from '../../lib/schema';
|
|
11
|
+
import { textHash } from '../../theme-sdk/fields.mjs';
|
|
11
12
|
import { ImageCard, EmbedCard, ButtonCard, Callout, HtmlCard, detectEmbed } from './extensions/cards';
|
|
12
13
|
import { FileCard, GalleryCard, BookmarkCard } from './extensions/mediaCards';
|
|
13
14
|
import { CodeBlock } from './extensions/codeBlock';
|
|
@@ -30,6 +31,7 @@ import { I18N, localeHomeUrl } from '../../lib/i18n';
|
|
|
30
31
|
import { mergeEventDraft, mergePageDraft, mergePostDraft } from '../../lib/drafts';
|
|
31
32
|
import { CALENDAR, eventUrl, type EventFieldConfig } from '../../lib/calendar/config';
|
|
32
33
|
import { EventPanel, eventWhenLabel, type CalendarOption, type EventMeta } from './EventPanel';
|
|
34
|
+
import { fetchRoomStatus, NO_HOLD, RoomConflictDialog } from '../calendar/RoomHolds';
|
|
33
35
|
|
|
34
36
|
// The Ghost-style writing surface, shared by posts AND pages: distraction-free
|
|
35
37
|
// column, cards via "/", autosave, a collapsible settings sidebar, and an
|
|
@@ -62,6 +64,10 @@ interface Props {
|
|
|
62
64
|
templates?: string[];
|
|
63
65
|
// public identity for the Search / Social preview tabs
|
|
64
66
|
site?: { title: string; url: string };
|
|
67
|
+
// extra site languages (acms({ i18n })) and whether AI drafts are available
|
|
68
|
+
languages?: { code: string; name: string }[];
|
|
69
|
+
mainLanguage?: { code: string; name: string };
|
|
70
|
+
translationDrafts?: boolean;
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
interface Meta extends EventMeta {
|
|
@@ -136,13 +142,18 @@ function joinBrokenTableRows(text: string): string {
|
|
|
136
142
|
return out.join('\n');
|
|
137
143
|
}
|
|
138
144
|
|
|
145
|
+
type TrField = 'title' | 'excerpt' | 'bodyHtml' | 'seoTitle' | 'seoDesc' | 'locationName' | 'ctaLabel';
|
|
146
|
+
const emptyTr = () => ({ title: '', excerpt: '', bodyHtml: '', seoTitle: '', seoDesc: '', slug: '', locationName: '', ctaLabel: '', meta: {} as Record<string, { state: 'ai' | 'reviewed'; src: string }> });
|
|
147
|
+
const TR_STATE: Record<string, string> = { missing: 'not translated', ai: 'AI draft', reviewed: 'reviewed', outdated: 'English changed' };
|
|
148
|
+
|
|
139
149
|
const NO_EVENT: EventMeta = {
|
|
140
150
|
start: '', end: '', allDay: false, timezone: '', rrule: '', exdates: [], cancelled: false,
|
|
141
151
|
location: { name: '', address: '', url: '' }, cta: { label: '', href: '', style: 'primary' }, calendarIds: [], fields: {},
|
|
142
152
|
rsvp: { enabled: false, capacity: 0, autoConfirm: true, maxSeats: 4 },
|
|
153
|
+
roomHold: NO_HOLD,
|
|
143
154
|
};
|
|
144
155
|
|
|
145
|
-
export default function PostEditor({ kind = 'post', post, page, event, calendars = [], eventFields = [], siteTimezone = '', initialHTML, unsplashEnabled, previewPath, sharePath, livePath: livePathProp, templates = [], site = { title: '', url: '' } }: Props) {
|
|
156
|
+
export default function PostEditor({ kind = 'post', post, page, event, calendars = [], eventFields = [], siteTimezone = '', initialHTML, unsplashEnabled, previewPath, sharePath, livePath: livePathProp, templates = [], site = { title: '', url: '' }, languages = [], mainLanguage, translationDrafts = false }: Props) {
|
|
146
157
|
const doc = (kind === 'page' ? page : kind === 'event' ? event : post)!;
|
|
147
158
|
// posts and events share the feature image, excerpt, tags and "featured"
|
|
148
159
|
const postLike = kind !== 'page';
|
|
@@ -167,7 +178,7 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
167
178
|
template: event!.template, publishedAt: event!.publishedAt, scheduledAt: event!.scheduledAt,
|
|
168
179
|
start: event!.start, end: event!.end, allDay: event!.allDay, timezone: event!.timezone, rrule: event!.rrule,
|
|
169
180
|
exdates: event!.exdates, cancelled: event!.cancelled, location: event!.location, cta: event!.cta,
|
|
170
|
-
calendarIds: event!.calendarIds, fields: event!.fields, rsvp: event!.rsvp,
|
|
181
|
+
calendarIds: event!.calendarIds, fields: event!.fields, rsvp: event!.rsvp, roomHold: event!.roomHold,
|
|
171
182
|
}
|
|
172
183
|
: {
|
|
173
184
|
...NO_EVENT,
|
|
@@ -183,7 +194,7 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
183
194
|
const col = kind === 'event'
|
|
184
195
|
? { singular: 'Event', plural: 'Events', base: CALENDAR.eventsBase, redirectFrom: [] }
|
|
185
196
|
: kind === 'page' ? CONTENT.posts : (CONTENT.collections[meta.collection] ?? CONTENT.posts);
|
|
186
|
-
const listHref = kind === 'page' ? '/admin/pages' : kind === 'event' ? '/admin/calendar
|
|
197
|
+
const listHref = kind === 'page' ? '/admin/pages' : kind === 'event' ? '/admin/calendar' : `/admin/c/${meta.collection}`;
|
|
187
198
|
const noun = kind === 'page' ? 'page' : col.singular.toLowerCase();
|
|
188
199
|
|
|
189
200
|
const [authors, setAuthors] = useState<AuthorOption[]>([]);
|
|
@@ -230,6 +241,21 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
230
241
|
};
|
|
231
242
|
// excerpt line under the title: shown when it has text or was asked for
|
|
232
243
|
const [excerptOpen, setExcerptOpen] = useState(false);
|
|
244
|
+
|
|
245
|
+
// ---- translations: the same document's words in another language ----
|
|
246
|
+
// Spanish (etc.) lives in `translations[lang]`; switching language swaps
|
|
247
|
+
// the editor body, the rest (dates, cover, tags…) stays shared.
|
|
248
|
+
const [trs, setTrs] = useState<DocTranslations>(() => ((doc as { translations?: DocTranslations }).translations ?? {}) as DocTranslations);
|
|
249
|
+
const trsRef = useRef(trs);
|
|
250
|
+
trsRef.current = trs;
|
|
251
|
+
const [lang, setLangState] = useState('');
|
|
252
|
+
const langRef = useRef('');
|
|
253
|
+
const mainBody = useRef<{ html: string; json: unknown } | null>(null);
|
|
254
|
+
const trBodyDirty = useRef(false);
|
|
255
|
+
const [trBusy, setTrBusy] = useState(false);
|
|
256
|
+
const [trMsg, setTrMsg] = useState('');
|
|
257
|
+
const mainLangName = mainLanguage?.name || 'English';
|
|
258
|
+
const mainLangShort = (mainLanguage?.code || 'en').toUpperCase();
|
|
233
259
|
// sidebar: reveal the date input for drafts that have no date yet
|
|
234
260
|
const [dateOpen, setDateOpen] = useState(false);
|
|
235
261
|
// which Preview tab opens (the sidebar's search card jumps to Search)
|
|
@@ -322,7 +348,7 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
322
348
|
return false;
|
|
323
349
|
},
|
|
324
350
|
},
|
|
325
|
-
onUpdate: () => queueSave(),
|
|
351
|
+
onUpdate: () => { if (langRef.current) trBodyDirty.current = true; queueSave(); },
|
|
326
352
|
onSelectionUpdate: ({ editor: ed }) => {
|
|
327
353
|
// the top-level block holding the caret; the live pane scrolls to it.
|
|
328
354
|
// Debounced and split-only so typing doesn't re-render the editor.
|
|
@@ -344,8 +370,18 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
344
370
|
if (saveTimer.current) { clearTimeout(saveTimer.current); saveTimer.current = null; }
|
|
345
371
|
const m = { ...metaRef.current, ...overrides };
|
|
346
372
|
setSave('saving');
|
|
347
|
-
|
|
348
|
-
|
|
373
|
+
// in another language the editor holds that language's body: store it as
|
|
374
|
+
// the translation and keep sending the main-language body unchanged
|
|
375
|
+
if (langRef.current && trBodyDirty.current) {
|
|
376
|
+
trBodyDirty.current = false;
|
|
377
|
+
const l = langRef.current;
|
|
378
|
+
const cur = trsRef.current[l] ?? emptyTr();
|
|
379
|
+
trsRef.current = { ...trsRef.current, [l]: { ...cur, bodyHtml: ed.isEmpty ? '' : ed.getHTML(), meta: { ...cur.meta, bodyHtml: { state: 'reviewed', src: textHash(mainBody.current?.html ?? '') } } } };
|
|
380
|
+
setTrs(trsRef.current);
|
|
381
|
+
}
|
|
382
|
+
const bodyJson = langRef.current && mainBody.current ? JSON.stringify(mainBody.current.json) : JSON.stringify(ed.getJSON());
|
|
383
|
+
const bodyHtml = langRef.current && mainBody.current ? mainBody.current.html : ed.getHTML();
|
|
384
|
+
const translations = languages.length ? trsRef.current : undefined;
|
|
349
385
|
if (kind === 'event') {
|
|
350
386
|
const r = await actions.events.save({
|
|
351
387
|
id: docId,
|
|
@@ -374,7 +410,9 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
374
410
|
calendarIds: m.calendarIds,
|
|
375
411
|
fields: m.fields,
|
|
376
412
|
rsvp: m.rsvp,
|
|
413
|
+
roomHold: m.roomHold,
|
|
377
414
|
scheduledAt: m.scheduledAt,
|
|
415
|
+
translations,
|
|
378
416
|
});
|
|
379
417
|
if (r.error) {
|
|
380
418
|
setSave('error');
|
|
@@ -403,6 +441,7 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
403
441
|
status: m.status,
|
|
404
442
|
isHome: m.isHome,
|
|
405
443
|
locale: m.locale,
|
|
444
|
+
translations,
|
|
406
445
|
});
|
|
407
446
|
if (r.error) {
|
|
408
447
|
setSave('error');
|
|
@@ -437,6 +476,7 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
437
476
|
template: m.template,
|
|
438
477
|
publishedAt: m.publishedAt,
|
|
439
478
|
scheduledAt: m.scheduledAt,
|
|
479
|
+
translations,
|
|
440
480
|
});
|
|
441
481
|
if (r.error) {
|
|
442
482
|
setSave('error');
|
|
@@ -463,6 +503,92 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
463
503
|
saveTimer.current = setTimeout(() => save(), 1200);
|
|
464
504
|
}, [save]);
|
|
465
505
|
|
|
506
|
+
// the main-language text a translated field is made from
|
|
507
|
+
const mainValue = (f: TrField): string => {
|
|
508
|
+
const m = metaRef.current;
|
|
509
|
+
switch (f) {
|
|
510
|
+
case 'title': return m.title;
|
|
511
|
+
case 'excerpt': return postLike ? m.excerpt : '';
|
|
512
|
+
case 'seoTitle': return m.seoTitle;
|
|
513
|
+
case 'seoDesc': return m.seoDesc;
|
|
514
|
+
case 'locationName': return kind === 'event' ? m.location.name : '';
|
|
515
|
+
case 'ctaLabel': return kind === 'event' ? m.cta.label : '';
|
|
516
|
+
default: return langRef.current && mainBody.current ? mainBody.current.html : (editorRef.current?.isEmpty ? '' : editorRef.current?.getHTML() ?? '');
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
const trFields = (): TrField[] => ['title', ...(postLike ? ['excerpt' as const] : []), 'bodyHtml', 'seoTitle', 'seoDesc', ...(kind === 'event' ? ['locationName' as const, 'ctaLabel' as const] : [])];
|
|
520
|
+
const trStatus = (l: string, f: TrField) => {
|
|
521
|
+
const src = mainValue(f);
|
|
522
|
+
if (!src.trim()) return 'n/a';
|
|
523
|
+
const t = trsRef.current[l];
|
|
524
|
+
const v = t?.[f] ?? '';
|
|
525
|
+
if (!String(v).trim()) return 'missing';
|
|
526
|
+
const meta = t?.meta?.[f];
|
|
527
|
+
if (meta && meta.src !== textHash(src)) return 'outdated';
|
|
528
|
+
return meta?.state === 'reviewed' ? 'reviewed' : 'ai';
|
|
529
|
+
};
|
|
530
|
+
const setTr = (f: TrField, value: string, state: 'ai' | 'reviewed' = 'reviewed') => {
|
|
531
|
+
const l = langRef.current;
|
|
532
|
+
if (!l) return;
|
|
533
|
+
const cur = trsRef.current[l] ?? emptyTr();
|
|
534
|
+
trsRef.current = { ...trsRef.current, [l]: { ...cur, [f]: value, meta: { ...cur.meta, [f]: { state, src: textHash(mainValue(f)) } } } };
|
|
535
|
+
setTrs(trsRef.current);
|
|
536
|
+
queueSave();
|
|
537
|
+
};
|
|
538
|
+
// switch the editor between the main language and a translation
|
|
539
|
+
const switchLang = async (next: string) => {
|
|
540
|
+
const ed = editorRef.current;
|
|
541
|
+
if (!ed || next === langRef.current) return;
|
|
542
|
+
await save();
|
|
543
|
+
if (!langRef.current) mainBody.current = { html: ed.getHTML(), json: ed.getJSON() };
|
|
544
|
+
const html = next ? (trsRef.current[next]?.bodyHtml ?? '') : (mainBody.current?.html ?? '');
|
|
545
|
+
langRef.current = next;
|
|
546
|
+
setLangState(next);
|
|
547
|
+
trBodyDirty.current = false;
|
|
548
|
+
ed.commands.setContent(html, { emitUpdate: false });
|
|
549
|
+
if (!next) mainBody.current = null;
|
|
550
|
+
setTrMsg('');
|
|
551
|
+
};
|
|
552
|
+
// machine drafts for every field still missing or with changed English
|
|
553
|
+
const translateAll = async (includeReviewed = false) => {
|
|
554
|
+
const l = langRef.current;
|
|
555
|
+
if (!l) return;
|
|
556
|
+
const fields = trFields().filter((f) => { const st = trStatus(l, f); return st === 'missing' || (st === 'outdated' && (includeReviewed || trsRef.current[l]?.meta?.[f]?.state !== 'reviewed')) || (includeReviewed && st === 'ai'); });
|
|
557
|
+
if (!fields.length) { setTrMsg('Everything is already translated.'); return; }
|
|
558
|
+
setTrBusy(true); setTrMsg('');
|
|
559
|
+
try {
|
|
560
|
+
const res = await fetch('/api/design/translate', {
|
|
561
|
+
method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
562
|
+
body: JSON.stringify({ locale: l, units: fields.map((f) => ({ id: f, text: mainValue(f), kind: f === 'bodyHtml' ? 'richtext' : f === 'excerpt' || f === 'seoDesc' ? 'textarea' : 'text', label: f })) }),
|
|
563
|
+
});
|
|
564
|
+
const body = await res.json().catch(() => ({})) as { results?: Record<string, string>; error?: string };
|
|
565
|
+
if (!res.ok) { setTrMsg(body.error || `Translation failed (${res.status}).`); return; }
|
|
566
|
+
const results = body.results ?? {};
|
|
567
|
+
const cur = trsRef.current[l] ?? emptyTr();
|
|
568
|
+
const next = { ...cur, meta: { ...cur.meta } } as typeof cur;
|
|
569
|
+
for (const f of fields) {
|
|
570
|
+
if (results[f] === undefined) continue;
|
|
571
|
+
(next as Record<string, unknown>)[f] = results[f];
|
|
572
|
+
next.meta[f] = { state: 'ai', src: textHash(mainValue(f)) };
|
|
573
|
+
}
|
|
574
|
+
trsRef.current = { ...trsRef.current, [l]: next };
|
|
575
|
+
setTrs(trsRef.current);
|
|
576
|
+
if (results.bodyHtml !== undefined && langRef.current === l) { editorRef.current?.commands.setContent(results.bodyHtml, { emitUpdate: false }); trBodyDirty.current = false; }
|
|
577
|
+
setTrMsg(`Translated ${Object.keys(results).length} ${Object.keys(results).length === 1 ? 'part' : 'parts'} as AI drafts. Check them, then mark them right.`);
|
|
578
|
+
queueSave();
|
|
579
|
+
} catch { setTrMsg('Couldn’t reach the server.'); } finally { setTrBusy(false); }
|
|
580
|
+
};
|
|
581
|
+
const confirmAll = () => {
|
|
582
|
+
const l = langRef.current;
|
|
583
|
+
if (!l) return;
|
|
584
|
+
const cur = trsRef.current[l] ?? emptyTr();
|
|
585
|
+
const meta = { ...cur.meta };
|
|
586
|
+
for (const f of trFields()) if (String(cur[f] ?? '').trim() && mainValue(f).trim()) meta[f] = { state: 'reviewed', src: textHash(mainValue(f)) };
|
|
587
|
+
trsRef.current = { ...trsRef.current, [l]: { ...cur, meta } };
|
|
588
|
+
setTrs(trsRef.current);
|
|
589
|
+
queueSave();
|
|
590
|
+
};
|
|
591
|
+
|
|
466
592
|
const setMeta = useCallback((patch: Partial<Meta>) => {
|
|
467
593
|
setMetaState((prev) => {
|
|
468
594
|
const next = { ...prev, ...patch };
|
|
@@ -588,8 +714,17 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
588
714
|
toastTimer.current = setTimeout(() => setToast(null), 4500);
|
|
589
715
|
}
|
|
590
716
|
|
|
591
|
-
|
|
717
|
+
// publishing or scheduling an event that holds rooms: check for bookings
|
|
718
|
+
// that overlap first, and let the author keep or cancel each one
|
|
719
|
+
const [roomGate, setRoomGate] = useState<{ patch: Partial<Meta>; rooms: Parameters<typeof RoomConflictDialog>[0]['rooms'] } | null>(null);
|
|
720
|
+
async function transition(patch: Partial<Meta>, roomsChecked = false) {
|
|
592
721
|
setPublishOpen(false);
|
|
722
|
+
const m0 = metaRef.current;
|
|
723
|
+
if (kind === 'event' && !roomsChecked && (patch.status === 'published' || patch.status === 'scheduled') && m0.roomHold.resourceIds.length) {
|
|
724
|
+
const st = await fetchRoomStatus({ eventId: docId, title: m0.title, start: m0.start, end: m0.end, allDay: m0.allDay, timezone: m0.timezone, rrule: m0.rrule, exdates: m0.exdates }, m0.roomHold).catch(() => null);
|
|
725
|
+
const rooms = (st?.rooms ?? []).filter((r) => m0.roomHold.resourceIds.includes(r.id) && r.conflicts.length);
|
|
726
|
+
if (rooms.length) { setRoomGate({ patch, rooms }); return; }
|
|
727
|
+
}
|
|
593
728
|
const next = { ...metaRef.current, ...patch };
|
|
594
729
|
metaRef.current = next;
|
|
595
730
|
setMetaState(next);
|
|
@@ -779,6 +914,19 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
779
914
|
|
|
780
915
|
return (
|
|
781
916
|
<div className={`pe-shell ${sidebarOpen ? 'has-sidebar' : ''} ${split ? 'has-split' : ''}`}>
|
|
917
|
+
{roomGate && (
|
|
918
|
+
<RoomConflictDialog
|
|
919
|
+
rooms={roomGate.rooms}
|
|
920
|
+
onCancel={() => setRoomGate(null)}
|
|
921
|
+
onConfirm={async ({ keep, cancel, message }) => {
|
|
922
|
+
if (cancel.length) await actions.events.cancelBookingsForEvent({ ids: cancel, message });
|
|
923
|
+
const hold = metaRef.current.roomHold;
|
|
924
|
+
const patch = { ...roomGate.patch, roomHold: { ...hold, keep: [...new Set([...hold.keep, ...keep])] } };
|
|
925
|
+
setRoomGate(null);
|
|
926
|
+
await transition(patch, true);
|
|
927
|
+
}}
|
|
928
|
+
/>
|
|
929
|
+
)}
|
|
782
930
|
{toast && (
|
|
783
931
|
<div className="pe-toast" role="status">
|
|
784
932
|
<span className="pe-toast-check">✓</span>
|
|
@@ -788,7 +936,7 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
788
936
|
)}
|
|
789
937
|
<header className="pe-topbar">
|
|
790
938
|
<div className="pe-topbar-left">
|
|
791
|
-
<a className="pe-btn ghost sm" href={listHref} onClick={(e) => { if (e.metaKey || e.ctrlKey || e.shiftKey) return; e.preventDefault(); requestLeave('list'); }} aria-label={kind === 'page' ? 'Back to pages' : `Back to ${col.plural.toLowerCase()}`}>‹<span className="pe-hide-sm">{kind === 'page' ? 'Pages' : col.plural}</span></a>
|
|
939
|
+
<a className="pe-btn ghost sm" href={listHref} onClick={(e) => { if (e.metaKey || e.ctrlKey || e.shiftKey) return; e.preventDefault(); requestLeave('list'); }} aria-label={kind === 'page' ? 'Back to pages' : kind === 'event' ? 'Back to the schedule' : `Back to ${col.plural.toLowerCase()}`}>‹<span className="pe-hide-sm">{kind === 'page' ? 'Pages' : kind === 'event' ? 'Schedule' : col.plural}</span></a>
|
|
792
940
|
<span className={`pe-savestate ${saveState}`}>
|
|
793
941
|
<i className="pe-save-dot" aria-hidden="true" />
|
|
794
942
|
{STATUS_LABEL[meta.status]}{pending && <em className="pe-edited-dot"> · edited</em>}<span className="pe-hide-sm"> · {saveLabel}</span>
|
|
@@ -808,6 +956,17 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
808
956
|
Preview
|
|
809
957
|
</button>
|
|
810
958
|
</div>
|
|
959
|
+
{languages.length > 0 && (
|
|
960
|
+
<div className="pe-seg pe-langs" role="radiogroup" aria-label="Language">
|
|
961
|
+
<button type="button" role="radio" aria-checked={!lang} onClick={() => void switchLang('')}>{mainLangShort}</button>
|
|
962
|
+
{languages.map((l) => (
|
|
963
|
+
<button key={l.code} type="button" role="radio" aria-checked={lang === l.code} title={`${l.name} version`} onClick={() => void switchLang(l.code)}>
|
|
964
|
+
{l.code.toUpperCase()}
|
|
965
|
+
{trFields().some((f) => ['missing', 'outdated'].includes(trStatus(l.code, f))) && <i className="pe-lang-dot" aria-label="needs translation" />}
|
|
966
|
+
</button>
|
|
967
|
+
))}
|
|
968
|
+
</div>
|
|
969
|
+
)}
|
|
811
970
|
<div className="pe-topbar-right">
|
|
812
971
|
<div className="pe-publish-wrap">
|
|
813
972
|
<button type="button" className="pe-btn primary sm" onClick={() => setPublishOpen((v) => !v)}>
|
|
@@ -973,7 +1132,7 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
973
1132
|
Feature image
|
|
974
1133
|
</button>
|
|
975
1134
|
)}
|
|
976
|
-
{!meta.excerpt && !excerptOpen && (
|
|
1135
|
+
{!meta.excerpt && !excerptOpen && !lang && (
|
|
977
1136
|
<button type="button" title="A short teaser under the title, on post cards and in link previews" onClick={() => setExcerptOpen(true)}>
|
|
978
1137
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><path d="M4 7h16M4 12h10M4 17h7" /></svg>
|
|
979
1138
|
Excerpt
|
|
@@ -982,6 +1141,36 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
982
1141
|
</div>
|
|
983
1142
|
)}
|
|
984
1143
|
|
|
1144
|
+
{lang && (() => {
|
|
1145
|
+
const name = languages.find((l) => l.code === lang)?.name ?? lang;
|
|
1146
|
+
const statuses = trFields().map((f) => [f, trStatus(lang, f)] as const).filter(([, st]) => st !== 'n/a');
|
|
1147
|
+
const label: Record<TrField, string> = { title: 'Title', excerpt: 'Excerpt', bodyHtml: 'Body', seoTitle: 'Search title', seoDesc: 'Search description', locationName: 'Place name', ctaLabel: 'Button text' };
|
|
1148
|
+
const t = trs[lang] ?? emptyTr();
|
|
1149
|
+
return (
|
|
1150
|
+
<section className="pe-trpanel" aria-label={`${name} version`}>
|
|
1151
|
+
<div className="pe-tr-head">
|
|
1152
|
+
<b>{name} version</b>
|
|
1153
|
+
<span className="pe-tr-states">{statuses.map(([f, st]) => <span key={f} className={`is-${st}`}>{label[f]}: {TR_STATE[st]}</span>)}</span>
|
|
1154
|
+
</div>
|
|
1155
|
+
<p className="pe-tr-note">You're editing the {name} words. Dates, photos, tags and links are shared with {mainLangName}. Anything left empty shows in {mainLangName}.</p>
|
|
1156
|
+
<div className="pe-tr-fields">
|
|
1157
|
+
{postLike && <label>Excerpt<textarea rows={2} value={t.excerpt} placeholder={meta.excerpt} onChange={(e) => setTr('excerpt', e.target.value)} /></label>}
|
|
1158
|
+
<label>Search title<input value={t.seoTitle} placeholder={meta.seoTitle || meta.title} onChange={(e) => setTr('seoTitle', e.target.value)} /></label>
|
|
1159
|
+
<label>Search description<textarea rows={2} value={t.seoDesc} placeholder={meta.seoDesc} onChange={(e) => setTr('seoDesc', e.target.value)} /></label>
|
|
1160
|
+
{kind === 'event' && <label>Place name<input value={t.locationName} placeholder={meta.location.name} onChange={(e) => setTr('locationName', e.target.value)} /></label>}
|
|
1161
|
+
{kind === 'event' && <label>Button text<input value={t.ctaLabel} placeholder={meta.cta.label} onChange={(e) => setTr('ctaLabel', e.target.value)} /></label>}
|
|
1162
|
+
</div>
|
|
1163
|
+
<div className="pe-tr-actions">
|
|
1164
|
+
{translationDrafts && <button type="button" className="pe-btn sm" disabled={trBusy} onClick={() => void translateAll()}>{trBusy ? 'Translating…' : `Translate the rest with AI`}</button>}
|
|
1165
|
+
{statuses.some(([, st]) => st === 'ai' || st === 'outdated') && <button type="button" className="pe-btn ghost sm" onClick={confirmAll}>✓ Looks right</button>}
|
|
1166
|
+
{!translationDrafts && <span className="pe-tr-msg">Turn on AI in Settings → AI & agents and the {name} can draft itself.</span>}
|
|
1167
|
+
<button type="button" className="pe-btn ghost sm" onClick={() => void switchLang('')}>Back to {mainLangName}</button>
|
|
1168
|
+
{trMsg && <span className="pe-tr-msg" role="status">{trMsg}</span>}
|
|
1169
|
+
</div>
|
|
1170
|
+
</section>
|
|
1171
|
+
);
|
|
1172
|
+
})()}
|
|
1173
|
+
|
|
985
1174
|
<textarea
|
|
986
1175
|
className="pe-title"
|
|
987
1176
|
rows={1}
|
|
@@ -989,19 +1178,19 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
989
1178
|
// grow to fit on mount and on narrow screens, not just while typing
|
|
990
1179
|
if (el) { el.style.height = 'auto'; el.style.height = `${el.scrollHeight}px`; }
|
|
991
1180
|
}}
|
|
992
|
-
placeholder={kind === 'page' ? 'Page title' : `${col.singular} title`}
|
|
993
|
-
value={meta.title}
|
|
1181
|
+
placeholder={lang ? meta.title || 'Title' : kind === 'page' ? 'Page title' : `${col.singular} title`}
|
|
1182
|
+
value={lang ? (trs[lang]?.title ?? '') : meta.title}
|
|
994
1183
|
onChange={(e) => {
|
|
995
1184
|
e.target.style.height = 'auto';
|
|
996
1185
|
e.target.style.height = `${e.target.scrollHeight}px`;
|
|
997
|
-
setMeta({ title: e.target.value });
|
|
1186
|
+
if (langRef.current) setTr('title', e.target.value); else setMeta({ title: e.target.value });
|
|
998
1187
|
}}
|
|
999
1188
|
onKeyDown={(e) => {
|
|
1000
1189
|
if (e.key === 'Enter') { e.preventDefault(); editor?.commands.focus('start'); }
|
|
1001
1190
|
}}
|
|
1002
1191
|
/>
|
|
1003
1192
|
|
|
1004
|
-
{postLike && (meta.excerpt || excerptOpen) && (
|
|
1193
|
+
{postLike && !lang && (meta.excerpt || excerptOpen) && (
|
|
1005
1194
|
<textarea
|
|
1006
1195
|
className="pe-excerpt"
|
|
1007
1196
|
rows={1}
|
|
@@ -1374,11 +1563,14 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
1374
1563
|
|
|
1375
1564
|
{kind === 'event' && (
|
|
1376
1565
|
<EventPanel
|
|
1566
|
+
eventId={docId}
|
|
1377
1567
|
meta={meta}
|
|
1378
1568
|
onChange={(patch) => setMeta(patch)}
|
|
1379
1569
|
calendars={calendars}
|
|
1380
1570
|
fields={eventFields}
|
|
1381
1571
|
siteTimezone={siteTimezone}
|
|
1572
|
+
title={meta.title}
|
|
1573
|
+
status={meta.status}
|
|
1382
1574
|
/>
|
|
1383
1575
|
)}
|
|
1384
1576
|
|
|
@@ -13,7 +13,7 @@ interface Plan {
|
|
|
13
13
|
}
|
|
14
14
|
interface Run { id: string; source: string; label: string; status: 'running' | 'done' | 'undone'; report: Report; createdAt: number }
|
|
15
15
|
interface Report {
|
|
16
|
-
counts?: { posts: { created: number; updated: number }; pages: { created: number; updated: number }; users: number; tags: number; media: number; redirects: number; nav: number };
|
|
16
|
+
counts?: { posts: { created: number; updated: number }; pages: { created: number; updated: number }; users: number; tags: number; media: number; redirects: number; nav: number; layouts?: number };
|
|
17
17
|
settings?: string[];
|
|
18
18
|
warnings?: { item: string; code: string; message: string }[];
|
|
19
19
|
failedAssets?: { url: string; error: string }[];
|
|
@@ -336,6 +336,9 @@ function Done({ report, reset, openHistory }: { report: Report; reset: () => voi
|
|
|
336
336
|
{report.settings?.length ? ' · site identity updated' : ''}
|
|
337
337
|
</p>
|
|
338
338
|
)}
|
|
339
|
+
{!!c?.layouts && (
|
|
340
|
+
<p className="imp-mark ok">{plural(c.layouts, 'page layout')} staged in the design draft · <a href="/admin/design#review">Review and publish</a></p>
|
|
341
|
+
)}
|
|
339
342
|
{!!report.failedAssets?.length && (
|
|
340
343
|
<>
|
|
341
344
|
<p className="imp-mark warn">{plural(report.failedAssets.length, 'file')} couldn’t be copied and still point at the old site</p>
|
|
@@ -268,6 +268,7 @@ export async function runImport(
|
|
|
268
268
|
nav: bundle.nav.map((n) => ({ ...n, url: fix(n.url) })),
|
|
269
269
|
legacy: bundle.items.filter((i) => i.legacyPath).map((i) => ({ sourceId: i.sourceId, type: i.type, path: i.legacyPath })),
|
|
270
270
|
redirects: bundle.redirects,
|
|
271
|
+
layouts: bundle.layouts ?? [],
|
|
271
272
|
warnings: bundle.warnings,
|
|
272
273
|
failedAssets,
|
|
273
274
|
});
|