@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.
- package/LICENSE +21 -0
- package/package.json +16 -14
- package/src/admin/api/media-file.ts +39 -0
- package/src/admin/api/media.ts +199 -0
- package/src/admin/layouts/CMSDashboardLayout.astro +178 -0
- package/src/admin/layouts/DefaultParentLayout.astro +50 -0
- package/src/admin/pages/assets.astro +424 -0
- package/src/admin/pages/byline-schema.astro +62 -0
- package/src/admin/pages/bylines.astro +16 -0
- package/src/admin/pages/categories.astro +21 -0
- package/src/admin/pages/content-types.astro +55 -0
- package/src/admin/pages/dashboard.astro +61 -0
- package/src/admin/pages/pages-edit.astro +595 -0
- package/src/admin/pages/pages-index.astro +146 -0
- package/src/admin/pages/pages-new.astro +130 -0
- package/src/admin/pages/pages-preview.astro +111 -0
- package/src/admin/pages/pages-review.astro +236 -0
- package/src/admin/pages/settings.astro +203 -0
- package/src/admin/pages/tags.astro +21 -0
- package/src/admin/pages/templates-edit.astro +234 -0
- package/src/admin/pages/templates-index.astro +95 -0
- package/src/admin/pages/templates-new.astro +193 -0
- package/src/admin/pages/users.astro +74 -0
- package/src/admin/pages/versions.astro +74 -0
- package/src/astro/BlockRenderer.astro +44 -102
- package/src/astro/PageRenderer.astro +9 -2
- package/src/astro/blocks/CalloutBlock.astro +7 -7
- package/src/astro/blocks/CardBlock.astro +5 -5
- package/src/astro/blocks/CodeBlock.astro +2 -2
- package/src/astro/blocks/FileTreeBlock.astro +7 -7
- package/src/astro/blocks/ImageBlock.astro +2 -2
- package/src/astro/blocks/OrderedListBlock.astro +2 -2
- package/src/astro/blocks/ParagraphBlock.astro +3 -3
- package/src/astro/blocks/SectionBlock.astro +6 -6
- package/src/astro/blocks/StepItemBlock.astro +3 -3
- package/src/astro/blocks/StepsBlock.astro +1 -1
- package/src/astro/blocks/TableBlock.astro +6 -6
- package/src/astro/blocks/TabsBlock.astro +9 -9
- package/src/astro/blocks/UnorderedListBlock.astro +2 -2
- package/src/astro/blocks/index.ts +35 -0
- package/src/client/components/RimelightBlock.tsx +19 -19
- package/src/client/components/RimelightBlockPicker.tsx +9 -13
- package/src/client/components/RimelightBylinesManager.tsx +438 -0
- package/src/client/components/RimelightEditor.tsx +3 -3
- package/src/client/components/RimelightPreview.tsx +37 -45
- package/src/client/components/RimelightPropertiesPanel.tsx +368 -26
- package/src/client/components/RimelightTaxonomyManager.tsx +369 -0
- package/src/client/components/RimelightTaxonomyMasterDetail.tsx +412 -0
- package/src/client/components/RimelightTemplateEditor.tsx +6 -8
- package/src/client/components/editors/CalloutEditor.tsx +2 -4
- package/src/client/components/editors/CardsEditor.tsx +2 -4
- package/src/client/components/editors/CodeEditor.tsx +1 -3
- package/src/client/components/editors/FileTreeEditor.tsx +2 -4
- package/src/client/components/editors/ImageEditor.tsx +3 -3
- package/src/client/components/editors/ParagraphEditor.tsx +2 -2
- package/src/client/components/editors/SceneEditor.tsx +6 -12
- package/src/client/components/editors/ScriptEditor.tsx +8 -16
- package/src/client/components/editors/SectionEditor.tsx +4 -4
- package/src/client/components/editors/StepItemEditor.tsx +2 -2
- package/src/client/components/editors/StepsEditor.tsx +2 -4
- package/src/client/components/editors/TabItemEditor.tsx +3 -3
- package/src/client/components/editors/TableEditor.tsx +5 -5
- package/src/client/components/editors/TabsEditor.tsx +2 -4
- package/src/client/components/index.ts +3 -0
- package/src/client/loader.ts +3 -0
- package/src/core/diff.ts +145 -0
- package/src/core/excerpt.ts +159 -0
- package/src/core/preview.ts +87 -0
- package/src/core/registry.ts +0 -86
- package/src/core/types/blocks.ts +6 -1
- package/src/core/types/bylines.ts +14 -0
- package/src/core/types/media.ts +13 -0
- package/src/core/types/taxonomies.ts +20 -0
- package/src/cron/scheduled.ts +57 -0
- package/src/docs/agent.ts +116 -0
- package/src/docs/config.ts +25 -0
- package/src/docs/index.ts +5 -0
- package/src/docs/routing.ts +104 -0
- package/src/docs/sidebar.ts +251 -0
- package/src/docs/toc.ts +47 -0
- package/src/env.d.ts +13 -0
- package/src/index.ts +40 -8
- package/src/integration.ts +196 -0
- package/src/loader/live.ts +254 -0
- package/src/markdown/index.ts +0 -1
- package/src/mcp/index.ts +228 -0
- package/src/schema/bylines.ts +23 -0
- package/src/schema/index.ts +3 -4
- package/src/schema/page_versions.ts +2 -0
- package/src/schema/pages.ts +2 -0
- package/src/schema/site_settings.ts +41 -0
- package/src/schema/taxonomies.ts +49 -0
- package/src/services/bylines.ts +74 -0
- package/src/services/search-indexer.ts +61 -0
- package/src/services/site-settings.ts +108 -0
- package/src/services/taxonomies.ts +276 -0
- package/src/storage/index.ts +253 -0
- package/src/astro/blocks/ApiDocumentationBlock.astro +0 -350
- package/src/astro/blocks/DialogueBlock.astro +0 -23
- package/src/astro/blocks/LivePreviewBlock.astro +0 -27
- package/src/astro/blocks/SceneBlock.astro +0 -65
- package/src/astro/blocks/ScriptBlock.astro +0 -59
- package/src/core/template-sync.ts +0 -75
- package/src/markdown/parser.ts +0 -347
- package/src/migration-tools.ts +0 -334
- package/src/migration.ts +0 -155
- package/src/schema/component_dependencies.ts +0 -21
- package/src/schema/component_metadata.ts +0 -31
- package/src/schema/component_metadata_cache.ts +0 -15
- package/src/schema/component_versions.ts +0 -19
- package/src/search/sync.ts +0 -32
- 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>
|