@rimelight/cms 0.0.2 → 0.0.3

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.
Files changed (112) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +16 -14
  3. package/src/admin/api/media-file.ts +39 -0
  4. package/src/admin/api/media.ts +199 -0
  5. package/src/admin/layouts/CMSDashboardLayout.astro +178 -0
  6. package/src/admin/layouts/DefaultParentLayout.astro +50 -0
  7. package/src/admin/pages/assets.astro +424 -0
  8. package/src/admin/pages/byline-schema.astro +62 -0
  9. package/src/admin/pages/bylines.astro +16 -0
  10. package/src/admin/pages/categories.astro +21 -0
  11. package/src/admin/pages/content-types.astro +55 -0
  12. package/src/admin/pages/dashboard.astro +61 -0
  13. package/src/admin/pages/pages-edit.astro +595 -0
  14. package/src/admin/pages/pages-index.astro +146 -0
  15. package/src/admin/pages/pages-new.astro +130 -0
  16. package/src/admin/pages/pages-preview.astro +111 -0
  17. package/src/admin/pages/pages-review.astro +236 -0
  18. package/src/admin/pages/settings.astro +203 -0
  19. package/src/admin/pages/tags.astro +21 -0
  20. package/src/admin/pages/templates-edit.astro +234 -0
  21. package/src/admin/pages/templates-index.astro +95 -0
  22. package/src/admin/pages/templates-new.astro +193 -0
  23. package/src/admin/pages/users.astro +74 -0
  24. package/src/admin/pages/versions.astro +74 -0
  25. package/src/astro/BlockRenderer.astro +44 -102
  26. package/src/astro/PageRenderer.astro +9 -2
  27. package/src/astro/blocks/CalloutBlock.astro +7 -7
  28. package/src/astro/blocks/CardBlock.astro +5 -5
  29. package/src/astro/blocks/CodeBlock.astro +2 -2
  30. package/src/astro/blocks/FileTreeBlock.astro +7 -7
  31. package/src/astro/blocks/ImageBlock.astro +2 -2
  32. package/src/astro/blocks/OrderedListBlock.astro +2 -2
  33. package/src/astro/blocks/ParagraphBlock.astro +3 -3
  34. package/src/astro/blocks/SectionBlock.astro +6 -6
  35. package/src/astro/blocks/StepItemBlock.astro +3 -3
  36. package/src/astro/blocks/StepsBlock.astro +1 -1
  37. package/src/astro/blocks/TableBlock.astro +6 -6
  38. package/src/astro/blocks/TabsBlock.astro +9 -9
  39. package/src/astro/blocks/UnorderedListBlock.astro +2 -2
  40. package/src/astro/blocks/index.ts +35 -0
  41. package/src/client/components/RimelightBlock.tsx +19 -19
  42. package/src/client/components/RimelightBlockPicker.tsx +9 -13
  43. package/src/client/components/RimelightBylinesManager.tsx +438 -0
  44. package/src/client/components/RimelightEditor.tsx +3 -3
  45. package/src/client/components/RimelightPreview.tsx +37 -45
  46. package/src/client/components/RimelightPropertiesPanel.tsx +368 -26
  47. package/src/client/components/RimelightTaxonomyManager.tsx +369 -0
  48. package/src/client/components/RimelightTaxonomyMasterDetail.tsx +412 -0
  49. package/src/client/components/RimelightTemplateEditor.tsx +6 -8
  50. package/src/client/components/editors/CalloutEditor.tsx +2 -4
  51. package/src/client/components/editors/CardsEditor.tsx +2 -4
  52. package/src/client/components/editors/CodeEditor.tsx +1 -3
  53. package/src/client/components/editors/FileTreeEditor.tsx +2 -4
  54. package/src/client/components/editors/ImageEditor.tsx +3 -3
  55. package/src/client/components/editors/ParagraphEditor.tsx +2 -2
  56. package/src/client/components/editors/SceneEditor.tsx +6 -12
  57. package/src/client/components/editors/ScriptEditor.tsx +8 -16
  58. package/src/client/components/editors/SectionEditor.tsx +4 -4
  59. package/src/client/components/editors/StepItemEditor.tsx +2 -2
  60. package/src/client/components/editors/StepsEditor.tsx +2 -4
  61. package/src/client/components/editors/TabItemEditor.tsx +3 -3
  62. package/src/client/components/editors/TableEditor.tsx +5 -5
  63. package/src/client/components/editors/TabsEditor.tsx +2 -4
  64. package/src/client/components/index.ts +3 -0
  65. package/src/client/loader.ts +3 -0
  66. package/src/core/diff.ts +145 -0
  67. package/src/core/excerpt.ts +159 -0
  68. package/src/core/preview.ts +87 -0
  69. package/src/core/registry.ts +0 -86
  70. package/src/core/types/blocks.ts +6 -1
  71. package/src/core/types/bylines.ts +14 -0
  72. package/src/core/types/media.ts +13 -0
  73. package/src/core/types/taxonomies.ts +20 -0
  74. package/src/cron/scheduled.ts +57 -0
  75. package/src/docs/agent.ts +116 -0
  76. package/src/docs/config.ts +25 -0
  77. package/src/docs/index.ts +5 -0
  78. package/src/docs/routing.ts +104 -0
  79. package/src/docs/sidebar.ts +251 -0
  80. package/src/docs/toc.ts +47 -0
  81. package/src/env.d.ts +13 -0
  82. package/src/index.ts +40 -8
  83. package/src/integration.ts +196 -0
  84. package/src/loader/live.ts +254 -0
  85. package/src/markdown/index.ts +0 -1
  86. package/src/mcp/index.ts +228 -0
  87. package/src/schema/bylines.ts +23 -0
  88. package/src/schema/index.ts +3 -4
  89. package/src/schema/page_versions.ts +2 -0
  90. package/src/schema/pages.ts +2 -0
  91. package/src/schema/site_settings.ts +41 -0
  92. package/src/schema/taxonomies.ts +49 -0
  93. package/src/services/bylines.ts +74 -0
  94. package/src/services/search-indexer.ts +61 -0
  95. package/src/services/site-settings.ts +108 -0
  96. package/src/services/taxonomies.ts +276 -0
  97. package/src/storage/index.ts +253 -0
  98. package/src/astro/blocks/ApiDocumentationBlock.astro +0 -350
  99. package/src/astro/blocks/DialogueBlock.astro +0 -23
  100. package/src/astro/blocks/LivePreviewBlock.astro +0 -27
  101. package/src/astro/blocks/SceneBlock.astro +0 -65
  102. package/src/astro/blocks/ScriptBlock.astro +0 -59
  103. package/src/core/template-sync.ts +0 -75
  104. package/src/markdown/parser.ts +0 -347
  105. package/src/migration-tools.ts +0 -334
  106. package/src/migration.ts +0 -155
  107. package/src/schema/component_dependencies.ts +0 -21
  108. package/src/schema/component_metadata.ts +0 -31
  109. package/src/schema/component_metadata_cache.ts +0 -15
  110. package/src/schema/component_versions.ts +0 -19
  111. package/src/search/sync.ts +0 -32
  112. package/src/services/component-metadata.ts +0 -134
@@ -0,0 +1,595 @@
1
+ ---
2
+ import ParentLayout from "virtual:rimelight-cms/parent-layout";
3
+ import RLAButton from "@rimelight/ui/components/button/RLAButton.astro";
4
+ import RLABadge from "@rimelight/ui/components/badge/RLABadge.astro";
5
+ import RLAIcon from "@rimelight/ui/components/icon/RLAIcon.astro";
6
+ import RLADropdownMenu from "@rimelight/ui/components/dropdown-menu/RLADropdownMenu.astro";
7
+ import RLAModal from "@rimelight/ui/components/modal/RLAModal.astro";
8
+ import { db } from "virtual:rimelight-cms/db";
9
+ import { pages, pageDrafts } from "../../schema";
10
+ import { eq } from "drizzle-orm";
11
+ import { getRelativeLocaleUrl } from "@rimelight/i18n";
12
+ import { RimelightEditorLayout, RimelightPropertiesPanel, RimelightPreview } from "../../client/components";
13
+ import "../../client/styles/cms-editor.css";
14
+
15
+ export const prerender = false;
16
+
17
+ const currentLocale = Astro.currentLocale ?? "en";
18
+ const id = Astro.params.id;
19
+
20
+ let page: any = null;
21
+ let draft: any = null;
22
+
23
+ if (db && id) {
24
+ try {
25
+ const pageResult = await db.select().from(pages).where(eq(pages.id, id as any)).limit(1);
26
+ page = pageResult[0] || null;
27
+ if (page) {
28
+ const draftResult = await db.select().from(pageDrafts).where(eq(pageDrafts.pageId, id as any)).limit(1);
29
+ draft = draftResult[0] || null;
30
+ }
31
+ } catch (err) {
32
+ console.warn("Failed to load page for edit:", err);
33
+ }
34
+ }
35
+
36
+ if (!page) {
37
+ return Astro.redirect(getRelativeLocaleUrl(currentLocale, "/cms/pages"));
38
+ }
39
+
40
+ let rawContent: any = draft?.content || page.content || {};
41
+ if (typeof rawContent === "string") {
42
+ try {
43
+ rawContent = JSON.parse(rawContent);
44
+ } catch {
45
+ rawContent = {};
46
+ }
47
+ }
48
+ if (rawContent && rawContent.content) {
49
+ rawContent = rawContent.content;
50
+ }
51
+
52
+ const extractedBlocks = Array.isArray(rawContent?.blocks)
53
+ ? rawContent.blocks
54
+ : Array.isArray(rawContent)
55
+ ? rawContent
56
+ : [];
57
+
58
+ const titleVal =
59
+ typeof page.title === "object" && page.title !== null
60
+ ? (page.title as Record<string, string>)[currentLocale] || (page.title as Record<string, string>).en || ""
61
+ : String(page.title || "");
62
+
63
+ const descVal =
64
+ typeof page.description === "object" && page.description !== null
65
+ ? (page.description as Record<string, string>)[currentLocale] || (page.description as Record<string, string>).en || ""
66
+ : String(page.description || "");
67
+
68
+ const pageType = (page as any).type || "blog";
69
+ const initialProperties = rawContent?.properties || {};
70
+ const heroImage = initialProperties?.heroImage || (page as any).banner?.src || "";
71
+ const viewUrl = page.type === "blog" ? getRelativeLocaleUrl(currentLocale, `/blog/${page.slug}`) : getRelativeLocaleUrl(currentLocale, `/${page.slug}`);
72
+ const isPublished = !!page.publishedVersionId;
73
+ ---
74
+
75
+ <ParentLayout title={`Edit: ${titleVal || 'Untitled Page'}`} description="CMS Page Editor">
76
+ <div class="flex flex-col h-screen overflow-hidden bg-background text-foreground select-none">
77
+ <!-- Top Header Bar -->
78
+ <header class="h-12 w-full bg-elevated/70 flex items-center justify-between px-4 border-b border-default shrink-0 z-30">
79
+ <div class="flex items-center gap-2.5">
80
+ <RLAButton
81
+ href={getRelativeLocaleUrl(currentLocale, "/cms/pages")}
82
+ variant="ghost"
83
+ color="neutral"
84
+ size="xs"
85
+ leadingIcon="i-lucide-arrow-left"
86
+ class="!px-2"
87
+ >
88
+ Pages
89
+ </RLAButton>
90
+
91
+ <span class="w-px h-4 bg-default" />
92
+
93
+ <div class="flex items-center gap-1.5 text-xs text-muted font-medium whitespace-nowrap overflow-hidden max-w-[200px] sm:max-w-xs">
94
+ <span>Editing:</span>
95
+ <span id="header-page-title" class="font-semibold text-highlighted truncate">{titleVal || "Untitled Page"}</span>
96
+ </div>
97
+
98
+ <span class="w-px h-4 bg-default" />
99
+
100
+ <div class="flex items-center gap-1">
101
+ <RLAButton id="undo-btn" size="xs" square variant="ghost" color="neutral" leadingIcon="i-lucide-rotate-ccw" aria-label="Undo" />
102
+ <RLAButton id="redo-btn" size="xs" square variant="ghost" color="neutral" leadingIcon="i-lucide-rotate-cw" aria-label="Redo" />
103
+ </div>
104
+
105
+ <RLABadge id="save-status-indicator" variant="soft" color="emerald" size="xs">
106
+ ● Saved
107
+ </RLABadge>
108
+ </div>
109
+
110
+ <div class="flex items-center gap-2">
111
+ <RLAButton
112
+ href={viewUrl}
113
+ target="_blank"
114
+ variant="ghost"
115
+ color="neutral"
116
+ size="xs"
117
+ leadingIcon="i-lucide-external-link"
118
+ >
119
+ <span class="hidden sm:inline">View Page</span>
120
+ </RLAButton>
121
+
122
+ <RLAButton
123
+ id="preview-btn"
124
+ variant="ghost"
125
+ color="neutral"
126
+ size="xs"
127
+ leadingIcon="i-lucide-eye"
128
+ >
129
+ <span class="hidden sm:inline">Preview</span>
130
+ </RLAButton>
131
+
132
+ <span class="w-px h-4 bg-default hidden sm:block" />
133
+
134
+ <RLAButton
135
+ id="publish-btn"
136
+ variant="outline"
137
+ color="neutral"
138
+ size="xs"
139
+ leadingIcon="i-lucide-globe"
140
+ >
141
+ {isPublished ? "Unpublish" : "Publish"}
142
+ </RLAButton>
143
+
144
+ <RLAButton
145
+ id="save-btn"
146
+ variant="solid"
147
+ color="primary"
148
+ size="xs"
149
+ leadingIcon="i-lucide-save"
150
+ >
151
+ Save
152
+ </RLAButton>
153
+
154
+ <RLADropdownMenu
155
+ size="xs"
156
+ content={{ align: "end" }}
157
+ items={[
158
+ {
159
+ id: "open-review-diffs-item",
160
+ label: "Review & Diffs...",
161
+ icon: "i-lucide-git-compare",
162
+ href: getRelativeLocaleUrl(currentLocale, `/cms/pages/${id}/review`)
163
+ },
164
+ {
165
+ id: "open-guest-preview-item",
166
+ label: "Live Guest Preview...",
167
+ icon: "i-lucide-share-2",
168
+ href: getRelativeLocaleUrl(currentLocale, `/cms/pages/${id}/preview`)
169
+ },
170
+ {
171
+ id: "open-delete-dialog-item",
172
+ label: "Delete Page...",
173
+ icon: "i-lucide-trash-2",
174
+ color: "error"
175
+ }
176
+ ]}
177
+ />
178
+ </div>
179
+ </header>
180
+
181
+ <!-- Main Workspace Split Container -->
182
+ <div id="split-container" class="flex w-full min-h-0 flex-1 overflow-hidden relative select-text">
183
+ <!-- Editor Pane (Left Column) -->
184
+ <div id="editor-pane" class="relative overflow-y-auto p-6 flex-1 w-full min-w-0">
185
+ <div class="max-w-7xl mx-auto flex flex-col py-2 w-full gap-6">
186
+ <!-- Banner Container -->
187
+ <div class="w-full relative group/banner aspect-[5/1] rounded-xl overflow-hidden bg-elevated/40 border border-default flex items-center justify-center">
188
+ <img
189
+ id="banner-img"
190
+ src={heroImage}
191
+ alt="Banner"
192
+ class={`absolute inset-0 size-full object-cover transition-opacity ${heroImage ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}
193
+ />
194
+ <div
195
+ id="banner-placeholder"
196
+ class={`flex flex-col items-center justify-center text-dimmed gap-1.5 opacity-50 hover:opacity-100 transition cursor-pointer ${heroImage ? 'hidden' : 'flex'}`}
197
+ >
198
+ <RLAIcon name="i-lucide-image" class="size-8" />
199
+ <span class="text-xs uppercase font-bold tracking-wider">Add Banner</span>
200
+ </div>
201
+
202
+ <div class="absolute top-3 right-3 flex items-center gap-2 opacity-0 group-hover/banner:opacity-100 transition">
203
+ <RLAButton id="change-banner-btn" size="xs" variant="solid" color="neutral" leadingIcon="i-lucide-edit-2">
204
+ Change
205
+ </RLAButton>
206
+ <RLAButton id="remove-banner-btn" size="xs" square variant="solid" color="error" leadingIcon="i-lucide-trash" aria-label="Remove Banner" />
207
+ </div>
208
+ </div>
209
+
210
+ <!-- Banner Image URL Dialog -->
211
+ <dialog
212
+ id="banner-url-dialog"
213
+ class="fixed inset-0 z-50 m-auto w-full max-w-sm rounded-xl border border-default bg-default p-6 shadow-2xl backdrop:bg-black/60 backdrop:backdrop-blur-sm"
214
+ >
215
+ <h3 class="text-base font-semibold text-highlighted mb-4">Banner Image URL</h3>
216
+ <div class="flex flex-col gap-3">
217
+ <input
218
+ id="banner-url-input"
219
+ type="url"
220
+ placeholder="https://..."
221
+ class="w-full rounded-md border border-default bg-elevated px-3 py-2 text-sm text-highlighted placeholder:text-dimmed focus:outline-none focus:ring-2 focus:ring-primary/50"
222
+ />
223
+ <div class="flex justify-end gap-2 pt-1">
224
+ <button
225
+ id="banner-dialog-remove"
226
+ type="button"
227
+ class="hidden px-3 py-1.5 text-xs font-medium rounded-md text-error-500 hover:bg-error-500/10 transition border border-error-500/30"
228
+ >Remove</button>
229
+ <div class="flex gap-2 ml-auto">
230
+ <button
231
+ id="banner-dialog-cancel"
232
+ type="button"
233
+ class="px-3 py-1.5 text-xs font-medium rounded-md border border-default text-highlighted hover:bg-elevated transition"
234
+ >Cancel</button>
235
+ <button
236
+ id="banner-dialog-apply"
237
+ type="button"
238
+ class="px-3 py-1.5 text-xs font-semibold rounded-md bg-primary text-white hover:bg-primary/90 transition"
239
+ >Apply</button>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ </dialog>
244
+
245
+ <!-- Title and Description Header -->
246
+ <div class="border-b border-default pb-4 flex flex-col gap-2">
247
+ <input
248
+ id="page-title-input"
249
+ type="text"
250
+ value={titleVal}
251
+ placeholder="Page Title"
252
+ class="w-full text-4xl font-bold bg-transparent border-0 focus:ring-0 focus:outline-none p-0 text-highlighted"
253
+ />
254
+ <textarea
255
+ id="page-desc-input"
256
+ rows="2"
257
+ placeholder="Add a description..."
258
+ class="w-full text-lg text-dimmed bg-transparent border-0 focus:ring-0 focus:outline-none p-0 resize-none mt-1"
259
+ >{descVal}</textarea>
260
+ </div>
261
+
262
+ <!-- 24-Column Grid: Properties (8) + Content (16) -->
263
+ <div class="grid grid-cols-1 lg:grid-cols-24 gap-8 items-start w-full">
264
+ <div class="order-1 lg:order-2 lg:col-span-8 xl:col-span-7 flex flex-col gap-4">
265
+ <div class="sticky top-4">
266
+ <RimelightPropertiesPanel
267
+ client:only="solid"
268
+ pageType={pageType}
269
+ initialProperties={initialProperties}
270
+ class="w-full"
271
+ />
272
+ </div>
273
+ </div>
274
+
275
+ <div class="order-2 lg:order-1 lg:col-span-16 xl:col-span-17 flex flex-col gap-6 w-full min-w-0">
276
+ <div id="cms-editor-layout" class="w-full">
277
+ <RimelightEditorLayout
278
+ client:only="solid"
279
+ pageId={id!}
280
+ initialBlocks={extractedBlocks}
281
+ showInternalPreview={false}
282
+ />
283
+ </div>
284
+
285
+ <div class="flex flex-col gap-1 text-xs text-dimmed pt-4 mt-6">
286
+ <span class="font-bold text-highlighted">Metadata</span>
287
+ <span>ID: {page.id}</span>
288
+ {page.createdAt && <span>Created: {new Date(page.createdAt).toLocaleString(currentLocale)}</span>}
289
+ {page.updatedAt && <span>Updated: {new Date(page.updatedAt).toLocaleString(currentLocale)}</span>}
290
+ </div>
291
+ </div>
292
+ </div>
293
+ </div>
294
+ </div>
295
+
296
+ <!-- Resizer Handle -->
297
+ <div id="split-drag-handle" class="hidden cms-resize-handle" title="Drag to resize">
298
+ <div class="cms-resize-pill" />
299
+ </div>
300
+
301
+ <!-- Live Preview Column -->
302
+ <div id="preview-pane" class="hidden flex-col h-full overflow-y-auto border-l border-default shrink-0">
303
+ <div id="preview-content-container" class="w-full h-full">
304
+ <RimelightPreview
305
+ client:only="solid"
306
+ blocks={extractedBlocks}
307
+ title={titleVal}
308
+ description={descVal}
309
+ banner={heroImage}
310
+ category={initialProperties?.category}
311
+ />
312
+ </div>
313
+ </div>
314
+ </div>
315
+ </div>
316
+
317
+ <!-- Delete Confirmation Modal -->
318
+ <RLAModal
319
+ id="delete-page-modal"
320
+ size="sm"
321
+ title="Delete Page"
322
+ description={`Are you sure you want to delete "${titleVal || 'Untitled Page'}"? This action cannot be undone.`}
323
+ >
324
+ <div slot="footer" class="flex justify-end gap-2">
325
+ <RLAButton
326
+ id="cancel-delete-page-btn"
327
+ variant="outline"
328
+ color="neutral"
329
+ size="sm"
330
+ data-dialog-close="delete-page-modal"
331
+ >
332
+ Cancel
333
+ </RLAButton>
334
+ <RLAButton id="confirm-delete-page-btn" variant="solid" color="error" size="sm">
335
+ Delete Page
336
+ </RLAButton>
337
+ </div>
338
+ </RLAModal>
339
+
340
+ <script is:inline id="initial-properties-data" type="application/json" set:html={JSON.stringify(initialProperties)} />
341
+ <script is:inline id="initial-page-data" type="application/json" set:html={JSON.stringify({ id: page.id, slug: page.slug, title: titleVal, description: descVal, banner: heroImage, isPublished, pageType })} />
342
+
343
+ <script>
344
+ import { initSplitPane } from "../../client/utils/splitpane";
345
+
346
+ const initialData = JSON.parse(document.getElementById('initial-page-data')?.textContent || '{}');
347
+ const initialProps = JSON.parse(document.getElementById('initial-properties-data')?.textContent || '{}');
348
+ (window as any).cmsPageProperties = initialProps;
349
+
350
+ const titleInput = document.getElementById('page-title-input') as HTMLInputElement | null;
351
+ const descInput = document.getElementById('page-desc-input') as HTMLTextAreaElement | null;
352
+ const headerTitle = document.getElementById('header-page-title');
353
+ const bannerImg = document.getElementById('banner-img') as HTMLImageElement | null;
354
+ const bannerPlaceholder = document.getElementById('banner-placeholder');
355
+
356
+ const splitContainer = document.getElementById('split-container');
357
+ const editorPane = document.getElementById('editor-pane');
358
+ const previewPane = document.getElementById('preview-pane');
359
+ const dragHandle = document.getElementById('split-drag-handle');
360
+ const previewBtn = document.getElementById('preview-btn');
361
+
362
+ let isPreviewOpen = false;
363
+ let cleanupSplitPane: (() => void) | null = null;
364
+
365
+ const broadcastMeta = () => {
366
+ document.dispatchEvent(new CustomEvent('page-meta-change', {
367
+ detail: {
368
+ title: titleInput?.value || '',
369
+ description: descInput?.value || '',
370
+ banner: (window as any).cmsPageProperties?.heroImage || '',
371
+ category: (window as any).cmsPageProperties?.category || ''
372
+ }
373
+ }));
374
+ };
375
+
376
+ const togglePreview = (forcedState?: boolean) => {
377
+ isPreviewOpen = typeof forcedState === 'boolean' ? forcedState : !isPreviewOpen;
378
+
379
+ if (isPreviewOpen) {
380
+ editorPane?.style.setProperty('flex', '0 0 50%');
381
+ editorPane?.style.setProperty('width', '50%');
382
+ previewPane?.style.setProperty('flex', '0 0 50%');
383
+ previewPane?.style.setProperty('width', '50%');
384
+ previewPane?.classList.remove('hidden');
385
+ previewPane?.classList.add('flex');
386
+ dragHandle?.classList.remove('hidden');
387
+ dragHandle?.classList.add('flex');
388
+
389
+ if (splitContainer && editorPane && previewPane && dragHandle) {
390
+ cleanupSplitPane = initSplitPane(splitContainer, editorPane, previewPane, dragHandle);
391
+ }
392
+ broadcastMeta();
393
+ } else {
394
+ if (cleanupSplitPane) {
395
+ cleanupSplitPane();
396
+ cleanupSplitPane = null;
397
+ }
398
+ editorPane?.style.setProperty('flex', '1 1 100%');
399
+ editorPane?.style.setProperty('width', '100%');
400
+ previewPane?.classList.add('hidden');
401
+ previewPane?.classList.remove('flex');
402
+ dragHandle?.classList.add('hidden');
403
+ dragHandle?.classList.remove('flex');
404
+ }
405
+ };
406
+
407
+ previewBtn?.addEventListener('click', () => togglePreview());
408
+
409
+ titleInput?.addEventListener('input', () => {
410
+ if (headerTitle && titleInput) {
411
+ headerTitle.textContent = titleInput.value || 'Untitled Page';
412
+ }
413
+ broadcastMeta();
414
+ });
415
+
416
+ descInput?.addEventListener('input', () => {
417
+ broadcastMeta();
418
+ });
419
+
420
+ const setBanner = (url: string) => {
421
+ (window as any).cmsPageProperties = { ...(window as any).cmsPageProperties, heroImage: url };
422
+ if (bannerImg) {
423
+ bannerImg.src = url;
424
+ bannerImg.classList.toggle('opacity-100', !!url);
425
+ bannerImg.classList.toggle('opacity-0', !url);
426
+ bannerImg.classList.toggle('pointer-events-none', !url);
427
+ }
428
+ if (bannerPlaceholder) {
429
+ bannerPlaceholder.classList.toggle('hidden', !!url);
430
+ bannerPlaceholder.classList.toggle('flex', !url);
431
+ }
432
+ broadcastMeta();
433
+ };
434
+
435
+ const bannerDialog = document.getElementById('banner-url-dialog') as HTMLDialogElement | null;
436
+ const bannerUrlInput = document.getElementById('banner-url-input') as HTMLInputElement | null;
437
+ const bannerDialogRemove = document.getElementById('banner-dialog-remove');
438
+ const bannerDialogCancel = document.getElementById('banner-dialog-cancel');
439
+ const bannerDialogApply = document.getElementById('banner-dialog-apply');
440
+
441
+ const openBannerDialog = (current = '') => {
442
+ if (!bannerDialog || !bannerUrlInput) return;
443
+ bannerUrlInput.value = current;
444
+ if (bannerDialogRemove) bannerDialogRemove.classList.toggle('hidden', !current);
445
+ bannerDialog.showModal();
446
+ setTimeout(() => bannerUrlInput.focus(), 50);
447
+ };
448
+
449
+ bannerDialogCancel?.addEventListener('click', () => bannerDialog?.close());
450
+ bannerDialogApply?.addEventListener('click', () => {
451
+ const url = bannerUrlInput?.value?.trim() || '';
452
+ setBanner(url);
453
+ bannerDialog?.close();
454
+ });
455
+ bannerDialogRemove?.addEventListener('click', () => {
456
+ setBanner('');
457
+ bannerDialog?.close();
458
+ });
459
+ bannerUrlInput?.addEventListener('keydown', (e: KeyboardEvent) => {
460
+ if (e.key === 'Enter') { e.preventDefault(); bannerDialogApply?.click(); }
461
+ if (e.key === 'Escape') { bannerDialog?.close(); }
462
+ });
463
+ bannerDialog?.addEventListener('click', (e: MouseEvent) => {
464
+ if (e.target === bannerDialog) bannerDialog.close();
465
+ });
466
+
467
+ document.getElementById('banner-placeholder')?.addEventListener('click', () => openBannerDialog(''));
468
+ document.getElementById('change-banner-btn')?.addEventListener('click', () => {
469
+ openBannerDialog((window as any).cmsPageProperties?.heroImage || '');
470
+ });
471
+ document.getElementById('remove-banner-btn')?.addEventListener('click', () => setBanner(''));
472
+
473
+ const getLayout = () => (window as any).cmsEditorLayout || (document.getElementById('cms-editor-layout') as any);
474
+
475
+ document.getElementById('undo-btn')?.addEventListener('click', () => getLayout()?.undo?.());
476
+ document.getElementById('redo-btn')?.addEventListener('click', () => getLayout()?.redo?.());
477
+
478
+ document.addEventListener('properties-change', (e: Event) => {
479
+ if (e instanceof CustomEvent && e.detail?.properties) {
480
+ (window as any).cmsPageProperties = e.detail.properties;
481
+ }
482
+ });
483
+
484
+ const savePage = async () => {
485
+ const saveBtn = document.getElementById('save-btn') as HTMLButtonElement | null;
486
+ const layout = getLayout();
487
+ if (!layout) return;
488
+
489
+ const pageId = initialData.id;
490
+ const blocks = layout.getBlocks ? await layout.getBlocks() : [];
491
+ const properties = (window as any).cmsPageProperties || {};
492
+ const title = titleInput?.value || initialData.title || '';
493
+ const description = descInput?.value || initialData.description || '';
494
+ const content = { blocks, properties };
495
+
496
+ try {
497
+ if (saveBtn) saveBtn.disabled = true;
498
+ const res = await fetch(`/api/cms/pages/${pageId}`, {
499
+ method: 'PUT',
500
+ headers: { 'Content-Type': 'application/json' },
501
+ body: JSON.stringify({
502
+ title: { en: title },
503
+ description: { en: description },
504
+ content
505
+ })
506
+ });
507
+ if (res.ok) {
508
+ const indicator = document.getElementById('save-status-indicator');
509
+ if (indicator) indicator.textContent = '● Saved';
510
+ } else {
511
+ alert('Failed to save page');
512
+ }
513
+ } catch {
514
+ alert('Network error saving page');
515
+ } finally {
516
+ if (saveBtn) saveBtn.disabled = false;
517
+ }
518
+ };
519
+
520
+ document.getElementById('save-btn')?.addEventListener('click', savePage);
521
+
522
+ document.getElementById('publish-btn')?.addEventListener('click', async () => {
523
+ const publishBtn = document.getElementById('publish-btn') as HTMLButtonElement | null;
524
+ const layout = getLayout();
525
+ if (!layout) { alert('Editor not loaded'); return; }
526
+
527
+ const pageId = initialData.id;
528
+ const blocks = layout.getBlocks ? await layout.getBlocks() : [];
529
+ const properties = (window as any).cmsPageProperties || {};
530
+ const title = titleInput?.value || initialData.title || '';
531
+ const description = descInput?.value || initialData.description || '';
532
+ const content = { blocks, properties };
533
+
534
+ try {
535
+ if (publishBtn) publishBtn.disabled = true;
536
+ await fetch(`/api/cms/pages/${pageId}`, {
537
+ method: 'PUT',
538
+ headers: { 'Content-Type': 'application/json' },
539
+ body: JSON.stringify({
540
+ title: { en: title },
541
+ description: { en: description },
542
+ content
543
+ })
544
+ });
545
+
546
+ const isUnpublish = publishBtn?.textContent?.trim() === 'Unpublish';
547
+ const endpoint = isUnpublish ? `/api/cms/pages/${pageId}/unpublish` : `/api/cms/pages/${pageId}/publish`;
548
+ const res = await fetch(endpoint, {
549
+ method: 'POST',
550
+ headers: { 'Content-Type': 'application/json' },
551
+ body: JSON.stringify({ content, changeSummary: isUnpublish ? 'Unpublished from editor' : 'Published from editor' })
552
+ });
553
+
554
+ if (res.ok) {
555
+ alert(isUnpublish ? 'Page unpublished!' : 'Page published successfully!');
556
+ window.location.reload();
557
+ } else {
558
+ const errorData = (await res.json().catch(() => ({}))) as { error?: string };
559
+ alert(`Failed: ${errorData.error || 'Unknown error'}`);
560
+ }
561
+ } catch {
562
+ alert('Network error');
563
+ } finally {
564
+ if (publishBtn) publishBtn.disabled = false;
565
+ }
566
+ });
567
+
568
+ const deleteModalDialog = document.querySelector('#delete-page-modal dialog') as HTMLDialogElement | null;
569
+ const confirmDeletePageBtn = document.getElementById('confirm-delete-page-btn');
570
+
571
+ document.addEventListener('click', (e) => {
572
+ const target = e.target as HTMLElement | null;
573
+ const deleteItem = target?.closest('[data-item-id="open-delete-dialog-item"]');
574
+ if (deleteItem && deleteModalDialog) {
575
+ deleteModalDialog.showModal();
576
+ }
577
+ });
578
+
579
+ confirmDeletePageBtn?.addEventListener('click', async () => {
580
+ try {
581
+ if (confirmDeletePageBtn) (confirmDeletePageBtn as HTMLButtonElement).disabled = true;
582
+ const res = await fetch(`/api/cms/pages/${initialData.id}`, { method: 'DELETE' });
583
+ if (res.ok) {
584
+ window.location.href = '/cms/pages';
585
+ } else {
586
+ alert('Failed to delete page');
587
+ if (confirmDeletePageBtn) (confirmDeletePageBtn as HTMLButtonElement).disabled = false;
588
+ }
589
+ } catch {
590
+ alert('Network error deleting page');
591
+ if (confirmDeletePageBtn) (confirmDeletePageBtn as HTMLButtonElement).disabled = false;
592
+ }
593
+ });
594
+ </script>
595
+ </ParentLayout>