@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,146 @@
|
|
|
1
|
+
---
|
|
2
|
+
import CMSDashboardLayout from "../layouts/CMSDashboardLayout.astro";
|
|
3
|
+
import RLAButton from "@rimelight/ui/components/button/RLAButton.astro";
|
|
4
|
+
import RLABadge from "@rimelight/ui/components/badge/RLABadge.astro";
|
|
5
|
+
import RLATable from "@rimelight/ui/components/table/RLATable.astro";
|
|
6
|
+
import type { TableColumn } from "@rimelight/ui/components/table/table.ts";
|
|
7
|
+
import { db } from "virtual:rimelight-cms/db";
|
|
8
|
+
import { pages } from "../../schema/pages.ts";
|
|
9
|
+
import { desc, isNull } from "drizzle-orm";
|
|
10
|
+
import { getRelativeLocaleUrl } from "@rimelight/i18n";
|
|
11
|
+
|
|
12
|
+
export const prerender = false;
|
|
13
|
+
|
|
14
|
+
const currentLocale = Astro.currentLocale ?? "en";
|
|
15
|
+
|
|
16
|
+
let pagesList: any[] = [];
|
|
17
|
+
if (db) {
|
|
18
|
+
try {
|
|
19
|
+
pagesList = await db
|
|
20
|
+
.select()
|
|
21
|
+
.from(pages)
|
|
22
|
+
.where(isNull(pages.deletedAt))
|
|
23
|
+
.orderBy(desc(pages.createdAt));
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.warn("Failed to fetch pages:", err);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const formattedPages = pagesList.map((page) => {
|
|
30
|
+
const titleVal =
|
|
31
|
+
typeof page.title === "object" && page.title !== null
|
|
32
|
+
? (page.title as Record<string, string>).en || ""
|
|
33
|
+
: String(page.title || "");
|
|
34
|
+
return {
|
|
35
|
+
id: page.id,
|
|
36
|
+
title: titleVal,
|
|
37
|
+
slug: page.slug,
|
|
38
|
+
type: page.type || "page",
|
|
39
|
+
status: page.publishedVersionId ? "published" : "draft",
|
|
40
|
+
publishedVersionId: page.publishedVersionId,
|
|
41
|
+
createdAt: page.createdAt
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const columns: TableColumn[] = [
|
|
46
|
+
{ id: "title", accessorKey: "title", header: "Title", enableSorting: true },
|
|
47
|
+
{ id: "slug", accessorKey: "slug", header: "Slug", enableSorting: true },
|
|
48
|
+
{ id: "type", accessorKey: "type", header: "Type", enableSorting: true },
|
|
49
|
+
{ id: "status", accessorKey: "status", header: "Status", enableSorting: true },
|
|
50
|
+
{ id: "actions", header: "Actions", enableSorting: false, meta: { class: { th: "text-right", td: "text-right" } } }
|
|
51
|
+
];
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
<CMSDashboardLayout title="Pages" description="Manage CMS pages" activeSection="pages">
|
|
55
|
+
<Fragment slot="toolbar-right">
|
|
56
|
+
<RLAButton href={getRelativeLocaleUrl(currentLocale, "/cms/pages/new")} variant="solid" color="success" leadingIcon="i-lucide-plus">
|
|
57
|
+
Create Page
|
|
58
|
+
</RLAButton>
|
|
59
|
+
</Fragment>
|
|
60
|
+
|
|
61
|
+
<RLATable
|
|
62
|
+
columns={columns}
|
|
63
|
+
data={formattedPages}
|
|
64
|
+
searchable
|
|
65
|
+
searchPlaceholder="Search pages..."
|
|
66
|
+
showColumnsToggle
|
|
67
|
+
hoverable
|
|
68
|
+
striped
|
|
69
|
+
class="w-full"
|
|
70
|
+
>
|
|
71
|
+
{formattedPages.map((page) => (
|
|
72
|
+
<tr class="hover:bg-muted/20 transition-colors" data-slot="tr" data-row-id={page.id}>
|
|
73
|
+
<td class="p-3.5 text-sm font-medium text-highlighted" data-col-id="title" data-slot="td">
|
|
74
|
+
{page.title}
|
|
75
|
+
</td>
|
|
76
|
+
<td class="p-3.5 text-sm text-muted font-mono text-xs" data-col-id="slug" data-slot="td">
|
|
77
|
+
{page.slug}
|
|
78
|
+
</td>
|
|
79
|
+
<td class="p-3.5" data-col-id="type" data-slot="td">
|
|
80
|
+
<RLABadge variant="soft" color="blue" size="xs">
|
|
81
|
+
{page.type}
|
|
82
|
+
</RLABadge>
|
|
83
|
+
</td>
|
|
84
|
+
<td class="p-3.5" data-col-id="status" data-slot="td">
|
|
85
|
+
{page.status === "published" ? (
|
|
86
|
+
<RLABadge variant="soft" color="green" size="xs">Published</RLABadge>
|
|
87
|
+
) : (
|
|
88
|
+
<RLABadge variant="soft" color="yellow" size="xs">Draft</RLABadge>
|
|
89
|
+
)}
|
|
90
|
+
</td>
|
|
91
|
+
<td class="p-3.5 text-right whitespace-nowrap" data-col-id="actions" data-slot="td">
|
|
92
|
+
<div class="flex items-center justify-end gap-2">
|
|
93
|
+
<RLAButton
|
|
94
|
+
href={getRelativeLocaleUrl(currentLocale, `/cms/pages/${page.id}/edit`)}
|
|
95
|
+
variant="ghost"
|
|
96
|
+
color="primary"
|
|
97
|
+
size="xs"
|
|
98
|
+
>
|
|
99
|
+
Edit
|
|
100
|
+
</RLAButton>
|
|
101
|
+
<RLAButton
|
|
102
|
+
href={getRelativeLocaleUrl(currentLocale, `/cms/pages/${page.id}/preview`)}
|
|
103
|
+
variant="ghost"
|
|
104
|
+
color="neutral"
|
|
105
|
+
size="xs"
|
|
106
|
+
>
|
|
107
|
+
Preview
|
|
108
|
+
</RLAButton>
|
|
109
|
+
<RLAButton
|
|
110
|
+
variant="ghost"
|
|
111
|
+
color="error"
|
|
112
|
+
size="xs"
|
|
113
|
+
class="delete-page-btn"
|
|
114
|
+
data-page-id={page.id}
|
|
115
|
+
>
|
|
116
|
+
Delete
|
|
117
|
+
</RLAButton>
|
|
118
|
+
</div>
|
|
119
|
+
</td>
|
|
120
|
+
</tr>
|
|
121
|
+
))}
|
|
122
|
+
</RLATable>
|
|
123
|
+
|
|
124
|
+
<script>
|
|
125
|
+
document.querySelectorAll(".delete-page-btn").forEach((btn) => {
|
|
126
|
+
btn.addEventListener("click", async () => {
|
|
127
|
+
const pageId = (btn as HTMLElement).dataset.pageId;
|
|
128
|
+
if (!pageId) return;
|
|
129
|
+
if (!confirm("Are you sure you want to delete this page?")) return;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const res = await fetch(`/api/cms/pages/${pageId}`, {
|
|
133
|
+
method: "DELETE"
|
|
134
|
+
});
|
|
135
|
+
if (res.ok) {
|
|
136
|
+
window.location.reload();
|
|
137
|
+
} else {
|
|
138
|
+
alert("Failed to delete page");
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
alert("Error deleting page");
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
</script>
|
|
146
|
+
</CMSDashboardLayout>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
import CMSDashboardLayout from "../layouts/CMSDashboardLayout.astro";
|
|
3
|
+
import RLAButton from "@rimelight/ui/components/button/RLAButton.astro";
|
|
4
|
+
import RLAInput from "@rimelight/ui/components/input/RLAInput.astro";
|
|
5
|
+
import RLASelect from "@rimelight/ui/components/select/RLASelect.astro";
|
|
6
|
+
import { db } from "virtual:rimelight-cms/db";
|
|
7
|
+
import { pageTemplates } from "../../schema/page_templates.ts";
|
|
8
|
+
import { desc } from "drizzle-orm";
|
|
9
|
+
import { getRelativeLocaleUrl } from "@rimelight/i18n";
|
|
10
|
+
|
|
11
|
+
export const prerender = false;
|
|
12
|
+
|
|
13
|
+
const currentLocale = Astro.currentLocale ?? "en";
|
|
14
|
+
|
|
15
|
+
let templates: any[] = [];
|
|
16
|
+
if (db) {
|
|
17
|
+
try {
|
|
18
|
+
templates = await db
|
|
19
|
+
.select()
|
|
20
|
+
.from(pageTemplates)
|
|
21
|
+
.orderBy(desc(pageTemplates.createdAt));
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.warn("Failed to fetch templates:", err);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const url = new URL(Astro.request.url);
|
|
28
|
+
const selectedTemplateId = url.searchParams.get("templateId") || "";
|
|
29
|
+
const selectedTemplate = templates.find((t: any) => t.id === selectedTemplateId);
|
|
30
|
+
|
|
31
|
+
const templateOptions = [
|
|
32
|
+
{ label: "Blank Page", value: "" },
|
|
33
|
+
...templates.map((t: any) => ({
|
|
34
|
+
label: `${typeof t.title === "object" ? t.title.en || t.title : t.title} (${t.pageType})`,
|
|
35
|
+
value: t.id
|
|
36
|
+
}))
|
|
37
|
+
];
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
<CMSDashboardLayout title="Create Page" description="Create a new CMS page" activeSection="pages">
|
|
41
|
+
<div class="max-w-3xl mx-auto space-y-6">
|
|
42
|
+
<form id="page-form" class="bg-elevated/40 border border-default p-6 rounded-2xl shadow space-y-6">
|
|
43
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
44
|
+
<div>
|
|
45
|
+
<label class="block text-sm font-medium text-highlighted mb-2">Slug</label>
|
|
46
|
+
<RLAInput name="slug" required />
|
|
47
|
+
</div>
|
|
48
|
+
<div>
|
|
49
|
+
<label class="block text-sm font-medium text-highlighted mb-2">Type</label>
|
|
50
|
+
<RLASelect
|
|
51
|
+
name="type"
|
|
52
|
+
placeholder="Select page type..."
|
|
53
|
+
value={selectedTemplate?.pageType || "blog"}
|
|
54
|
+
items={[
|
|
55
|
+
{ label: "Blog Post", value: "blog" },
|
|
56
|
+
{ label: "Wiki Page", value: "wiki" },
|
|
57
|
+
{ label: "Documentation", value: "doc" },
|
|
58
|
+
{ label: "Announcement", value: "announcement" }
|
|
59
|
+
]}
|
|
60
|
+
required
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div>
|
|
66
|
+
<label class="block text-sm font-medium text-highlighted mb-2">Title</label>
|
|
67
|
+
<RLAInput name="title" required />
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div>
|
|
71
|
+
<label class="block text-sm font-medium text-highlighted mb-2">Template</label>
|
|
72
|
+
<RLASelect
|
|
73
|
+
name="templateId"
|
|
74
|
+
placeholder="Select template..."
|
|
75
|
+
value={selectedTemplateId}
|
|
76
|
+
items={templateOptions}
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div class="border-t border-default pt-6 flex items-center justify-end gap-3">
|
|
81
|
+
<RLAButton href={getRelativeLocaleUrl(currentLocale, "/cms/pages")} variant="outline" color="neutral" size="sm">
|
|
82
|
+
Cancel
|
|
83
|
+
</RLAButton>
|
|
84
|
+
<RLAButton variant="solid" color="primary" size="sm" leadingIcon="i-lucide-plus" type="submit">
|
|
85
|
+
Create & Edit
|
|
86
|
+
</RLAButton>
|
|
87
|
+
</div>
|
|
88
|
+
</form>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<script is:inline define:vars={{ currentLocale }}>
|
|
92
|
+
const form = document.getElementById('page-form');
|
|
93
|
+
|
|
94
|
+
form?.addEventListener('submit', async (e) => {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
|
|
97
|
+
const formData = new FormData(form);
|
|
98
|
+
const rawTmplId = formData.get('templateId');
|
|
99
|
+
const templateId =
|
|
100
|
+
rawTmplId && rawTmplId !== 'null' && String(rawTmplId).trim() !== ''
|
|
101
|
+
? String(rawTmplId).trim()
|
|
102
|
+
: null;
|
|
103
|
+
|
|
104
|
+
const data = {
|
|
105
|
+
slug: formData.get('slug'),
|
|
106
|
+
type: formData.get('type'),
|
|
107
|
+
title: formData.get('title'),
|
|
108
|
+
templateId
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const response = await fetch('/api/cms/pages', {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: { 'Content-Type': 'application/json' },
|
|
115
|
+
body: JSON.stringify(data)
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (response.ok) {
|
|
119
|
+
const result = (await response.json());
|
|
120
|
+
window.location.href = `/${currentLocale}/cms/pages/${result.page.id}/edit`;
|
|
121
|
+
} else {
|
|
122
|
+
const errorData = (await response.json().catch(() => ({})));
|
|
123
|
+
alert(`Failed to create page: ${errorData.error || 'Unknown error'}`);
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
alert('Error creating page: Network error');
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
</script>
|
|
130
|
+
</CMSDashboardLayout>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
import PageRenderer from "../../astro/PageRenderer.astro";
|
|
3
|
+
import CMSDashboardLayout from "../layouts/CMSDashboardLayout.astro";
|
|
4
|
+
import RLAButton from "@rimelight/ui/components/button/RLAButton.astro";
|
|
5
|
+
import { db } from "virtual:rimelight-cms/db";
|
|
6
|
+
import { pages, pageDrafts } from "../../schema";
|
|
7
|
+
import { eq } from "drizzle-orm";
|
|
8
|
+
import { getRelativeLocaleUrl } from "@rimelight/i18n";
|
|
9
|
+
import { verifyPreviewToken } from "../../core/preview.ts";
|
|
10
|
+
|
|
11
|
+
export const prerender = false;
|
|
12
|
+
|
|
13
|
+
const currentLocale = Astro.currentLocale ?? "en";
|
|
14
|
+
const id = Astro.params.id;
|
|
15
|
+
|
|
16
|
+
if (!id) {
|
|
17
|
+
return Astro.redirect(getRelativeLocaleUrl(currentLocale, "/cms/pages"));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const token = Astro.url.searchParams.get("token");
|
|
21
|
+
const secret = process.env.BETTER_AUTH_SECRET || "rimelight-preview-secret-key";
|
|
22
|
+
if (token) {
|
|
23
|
+
await verifyPreviewToken(id, token, secret);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let page: any = null;
|
|
27
|
+
let draft: any = null;
|
|
28
|
+
|
|
29
|
+
if (db) {
|
|
30
|
+
try {
|
|
31
|
+
const pageResult = await db.select().from(pages).where(eq(pages.id, id as any)).limit(1);
|
|
32
|
+
page = pageResult[0] || null;
|
|
33
|
+
if (page) {
|
|
34
|
+
const draftResult = await db.select().from(pageDrafts).where(eq(pageDrafts.pageId, id as any)).limit(1);
|
|
35
|
+
draft = draftResult[0] || null;
|
|
36
|
+
}
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.warn("Failed to load page for preview:", err);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!page) {
|
|
43
|
+
return Astro.redirect(getRelativeLocaleUrl(currentLocale, "/cms/pages"));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const parseContent = (c: any) => (typeof c === "string" ? JSON.parse(c) : c);
|
|
47
|
+
const content = draft?.content ? parseContent(draft.content) : parseContent(page.content);
|
|
48
|
+
|
|
49
|
+
const titleVal =
|
|
50
|
+
typeof page.title === "object" && page.title !== null
|
|
51
|
+
? (page.title as Record<string, string>)[currentLocale] || (page.title as Record<string, string>).en || ""
|
|
52
|
+
: String(page.title || "");
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
<CMSDashboardLayout title={`Preview: ${titleVal}`} description="Preview CMS page" activeSection="pages">
|
|
56
|
+
<Fragment slot="toolbar-right">
|
|
57
|
+
<div class="flex items-center gap-2">
|
|
58
|
+
<button
|
|
59
|
+
id="share-preview-btn"
|
|
60
|
+
type="button"
|
|
61
|
+
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-primary bg-primary/10 border border-primary/20 rounded-md hover:bg-primary/20 transition-colors cursor-pointer"
|
|
62
|
+
>
|
|
63
|
+
<span class="i-lucide-share-2 size-3.5"></span>
|
|
64
|
+
<span id="share-btn-text">Share Preview Link</span>
|
|
65
|
+
</button>
|
|
66
|
+
|
|
67
|
+
<RLAButton href={getRelativeLocaleUrl(currentLocale, `/cms/pages/${id}/edit`)} variant="outline" color="neutral" size="sm" leadingIcon="i-lucide-arrow-left">
|
|
68
|
+
Back to Editor
|
|
69
|
+
</RLAButton>
|
|
70
|
+
</div>
|
|
71
|
+
</Fragment>
|
|
72
|
+
|
|
73
|
+
<div class="max-w-4xl mx-auto space-y-6">
|
|
74
|
+
<div class="bg-primary/10 border border-primary/20 p-4 rounded-lg flex items-center justify-between">
|
|
75
|
+
<p class="text-sm text-primary font-medium">Live Preview Mode — displaying current draft</p>
|
|
76
|
+
<span class="text-xs px-2.5 py-0.5 bg-primary/20 text-primary rounded-full font-mono">Draft</span>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div class="bg-elevated/40 rounded-xl shadow-sm border border-default p-8">
|
|
80
|
+
<h1 class="text-3xl font-bold text-highlighted mb-6">{titleVal}</h1>
|
|
81
|
+
<PageRenderer
|
|
82
|
+
blocks={content?.blocks || []}
|
|
83
|
+
locale={currentLocale}
|
|
84
|
+
class="prose prose-lg max-w-none"
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<script is:inline define:vars={{ pageId: id }}>
|
|
90
|
+
const shareBtn = document.getElementById("share-preview-btn");
|
|
91
|
+
const shareText = document.getElementById("share-btn-text");
|
|
92
|
+
|
|
93
|
+
shareBtn?.addEventListener("click", async () => {
|
|
94
|
+
if (shareText) shareText.textContent = "Generating...";
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(`/api/cms/pages/${pageId}/preview-token`, { method: "POST" });
|
|
97
|
+
if (!res.ok) throw new Error("Failed to generate token");
|
|
98
|
+
const data = await res.json();
|
|
99
|
+
const fullUrl = `${window.location.origin}${data.previewUrl}`;
|
|
100
|
+
await navigator.clipboard.writeText(fullUrl);
|
|
101
|
+
if (shareText) shareText.textContent = "Copied to Clipboard!";
|
|
102
|
+
setTimeout(() => {
|
|
103
|
+
if (shareText) shareText.textContent = "Share Preview Link";
|
|
104
|
+
}, 3000);
|
|
105
|
+
} catch {
|
|
106
|
+
alert("Could not generate preview link");
|
|
107
|
+
if (shareText) shareText.textContent = "Share Preview Link";
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
</script>
|
|
111
|
+
</CMSDashboardLayout>
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
---
|
|
2
|
+
import CMSDashboardLayout from "../layouts/CMSDashboardLayout.astro";
|
|
3
|
+
import RLAButton from "@rimelight/ui/components/button/RLAButton.astro";
|
|
4
|
+
import RLABadge from "@rimelight/ui/components/badge/RLABadge.astro";
|
|
5
|
+
import { db } from "virtual:rimelight-cms/db";
|
|
6
|
+
import { pages, pageDrafts, pageVersions } from "../../schema";
|
|
7
|
+
import { eq, desc } from "drizzle-orm";
|
|
8
|
+
import { getRelativeLocaleUrl } from "@rimelight/i18n";
|
|
9
|
+
import { diffPageSnapshots } from "../../core/diff.ts";
|
|
10
|
+
|
|
11
|
+
export const prerender = false;
|
|
12
|
+
|
|
13
|
+
const currentLocale = Astro.currentLocale ?? "en";
|
|
14
|
+
const id = Astro.params.id;
|
|
15
|
+
|
|
16
|
+
if (!id) {
|
|
17
|
+
return Astro.redirect(getRelativeLocaleUrl(currentLocale, "/cms/pages"));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let page: any = null;
|
|
21
|
+
let draft: any = null;
|
|
22
|
+
let versions: any[] = [];
|
|
23
|
+
|
|
24
|
+
if (db) {
|
|
25
|
+
try {
|
|
26
|
+
const pageResult = await db.select().from(pages).where(eq(pages.id, id as any)).limit(1);
|
|
27
|
+
page = pageResult[0] || null;
|
|
28
|
+
if (page) {
|
|
29
|
+
const draftResult = await db.select().from(pageDrafts).where(eq(pageDrafts.pageId, id as any)).limit(1);
|
|
30
|
+
draft = draftResult[0] || null;
|
|
31
|
+
versions = await db
|
|
32
|
+
.select()
|
|
33
|
+
.from(pageVersions)
|
|
34
|
+
.where(eq(pageVersions.pageId, id as any))
|
|
35
|
+
.orderBy(desc(pageVersions.versionNumber))
|
|
36
|
+
.limit(10);
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.warn("Failed to load review data:", err);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!page) {
|
|
44
|
+
return Astro.redirect(getRelativeLocaleUrl(currentLocale, "/cms/pages"));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const parseJson = (val: any) => (typeof val === "string" ? JSON.parse(val) : val);
|
|
48
|
+
const publishedSnapshot = { ...page, content: parseJson(page.content) };
|
|
49
|
+
const draftSnapshot = draft ? { ...page, content: parseJson(draft.content) } : publishedSnapshot;
|
|
50
|
+
|
|
51
|
+
const diffResult = diffPageSnapshots(publishedSnapshot, draftSnapshot);
|
|
52
|
+
|
|
53
|
+
const titleVal =
|
|
54
|
+
typeof page.title === "object" && page.title !== null
|
|
55
|
+
? (page.title as Record<string, string>)[currentLocale] || (page.title as Record<string, string>).en || ""
|
|
56
|
+
: String(page.title || "");
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
<CMSDashboardLayout title={`Review & Diffs: ${titleVal}`} description="Review content changes and block diffs" activeSection="pages">
|
|
60
|
+
<Fragment slot="toolbar-right">
|
|
61
|
+
<div class="flex items-center gap-2">
|
|
62
|
+
<RLAButton href={getRelativeLocaleUrl(currentLocale, `/cms/pages/${id}/preview`)} variant="outline" color="neutral" size="sm" leadingIcon="i-lucide-eye">
|
|
63
|
+
Live Preview
|
|
64
|
+
</RLAButton>
|
|
65
|
+
<button
|
|
66
|
+
id="approve-publish-btn"
|
|
67
|
+
type="button"
|
|
68
|
+
class="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium text-white bg-emerald-600 rounded-md hover:bg-emerald-700 transition-colors shadow-sm cursor-pointer disabled:opacity-50"
|
|
69
|
+
>
|
|
70
|
+
<span class="i-lucide-check-circle size-4"></span>
|
|
71
|
+
<span id="approve-btn-text">Approve & Publish</span>
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
</Fragment>
|
|
75
|
+
|
|
76
|
+
<div class="max-w-4xl mx-auto space-y-8 pb-12">
|
|
77
|
+
<!-- Diff Summary Statistics -->
|
|
78
|
+
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
|
79
|
+
<div class="bg-elevated/40 p-4 rounded-xl border border-default shadow-sm text-center">
|
|
80
|
+
<p class="text-xs font-semibold text-emerald-500 uppercase">Blocks Added</p>
|
|
81
|
+
<p class="text-3xl font-bold text-emerald-500 mt-1">+{diffResult.summary.addedCount}</p>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="bg-elevated/40 p-4 rounded-xl border border-default shadow-sm text-center">
|
|
84
|
+
<p class="text-xs font-semibold text-error-500 uppercase">Blocks Removed</p>
|
|
85
|
+
<p class="text-3xl font-bold text-error-500 mt-1">-{diffResult.summary.removedCount}</p>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="bg-elevated/40 p-4 rounded-xl border border-default shadow-sm text-center">
|
|
88
|
+
<p class="text-xs font-semibold text-warning-500 uppercase">Blocks Modified</p>
|
|
89
|
+
<p class="text-3xl font-bold text-warning-500 mt-1">~{diffResult.summary.modifiedCount}</p>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="bg-elevated/40 p-4 rounded-xl border border-default shadow-sm text-center">
|
|
92
|
+
<p class="text-xs font-semibold text-muted uppercase">Unchanged</p>
|
|
93
|
+
<p class="text-3xl font-bold text-highlighted mt-1">{diffResult.summary.unchangedCount}</p>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<!-- Metadata Diffs -->
|
|
98
|
+
{diffResult.metaChanges.length > 0 && (
|
|
99
|
+
<div class="bg-elevated/40 rounded-xl shadow-sm border border-default p-6">
|
|
100
|
+
<h2 class="text-lg font-semibold text-highlighted mb-4">Metadata Changes</h2>
|
|
101
|
+
<div class="divide-y divide-default">
|
|
102
|
+
{diffResult.metaChanges.map((change) => (
|
|
103
|
+
<div class="py-3 flex items-start justify-between">
|
|
104
|
+
<span class="text-sm font-mono text-highlighted font-semibold">{change.field}</span>
|
|
105
|
+
<div class="text-sm text-right space-y-1">
|
|
106
|
+
<p class="text-error-500 line-through font-mono text-xs">{JSON.stringify(change.oldValue)}</p>
|
|
107
|
+
<p class="text-emerald-500 font-mono text-xs">{JSON.stringify(change.newValue)}</p>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
<!-- Block-by-Block Visual Diff -->
|
|
116
|
+
<div class="bg-elevated/40 rounded-xl shadow-sm border border-default p-6 space-y-4">
|
|
117
|
+
<div class="flex items-center justify-between border-b border-default pb-4">
|
|
118
|
+
<div>
|
|
119
|
+
<h2 class="text-lg font-semibold text-highlighted">Block-Level Comparison</h2>
|
|
120
|
+
<p class="text-sm text-muted">Visual breakdown comparing published content vs working draft</p>
|
|
121
|
+
</div>
|
|
122
|
+
<RLABadge variant="soft" color="primary" size="sm">Smart Diff</RLABadge>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div class="space-y-3">
|
|
126
|
+
{diffResult.blocks.map((blockDiff) => (
|
|
127
|
+
<div class={`p-4 rounded-lg border text-sm ${
|
|
128
|
+
blockDiff.status === 'added' ? 'bg-emerald-950/20 border-emerald-800/40' :
|
|
129
|
+
blockDiff.status === 'removed' ? 'bg-error-950/20 border-error-800/40' :
|
|
130
|
+
blockDiff.status === 'modified' ? 'bg-warning-950/20 border-warning-800/40' :
|
|
131
|
+
'bg-elevated/20 border-default opacity-75'
|
|
132
|
+
}`}>
|
|
133
|
+
<div class="flex items-center justify-between mb-2">
|
|
134
|
+
<div class="flex items-center gap-2">
|
|
135
|
+
<span class={`text-xs px-2 py-0.5 rounded font-mono font-semibold uppercase ${
|
|
136
|
+
blockDiff.status === 'added' ? 'bg-emerald-500/20 text-emerald-400' :
|
|
137
|
+
blockDiff.status === 'removed' ? 'bg-error-500/20 text-error-400' :
|
|
138
|
+
blockDiff.status === 'modified' ? 'bg-warning-500/20 text-warning-400' :
|
|
139
|
+
'bg-muted text-muted'
|
|
140
|
+
}`}>
|
|
141
|
+
{blockDiff.status}
|
|
142
|
+
</span>
|
|
143
|
+
<span class="font-semibold text-highlighted">{blockDiff.type}</span>
|
|
144
|
+
</div>
|
|
145
|
+
<span class="text-xs font-mono text-muted">{blockDiff.blockId.slice(0, 8)}...</span>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
{blockDiff.fieldChanges && blockDiff.fieldChanges.length > 0 ? (
|
|
149
|
+
<div class="space-y-2 mt-2 pt-2 border-t border-warning-500/30">
|
|
150
|
+
{blockDiff.fieldChanges.map((change) => (
|
|
151
|
+
<div class="text-xs font-mono">
|
|
152
|
+
<span class="font-bold text-highlighted">{change.field}:</span>
|
|
153
|
+
<div class="grid grid-cols-2 gap-2 mt-1">
|
|
154
|
+
<div class="p-2 bg-error-950/30 text-error-400 rounded break-words">
|
|
155
|
+
- {JSON.stringify(change.oldValue)}
|
|
156
|
+
</div>
|
|
157
|
+
<div class="p-2 bg-emerald-950/30 text-emerald-400 rounded break-words">
|
|
158
|
+
+ {JSON.stringify(change.newValue)}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
) : (
|
|
165
|
+
<p class="text-xs text-muted font-mono">
|
|
166
|
+
{JSON.stringify((blockDiff.newBlock || blockDiff.oldBlock)?.props || {})}
|
|
167
|
+
</p>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
))}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<!-- Version History List -->
|
|
175
|
+
<div class="bg-elevated/40 rounded-xl shadow-sm border border-default p-6">
|
|
176
|
+
<h2 class="text-lg font-semibold text-highlighted mb-4">Version History & Snapshots</h2>
|
|
177
|
+
<div class="space-y-2">
|
|
178
|
+
{versions.map((v) => (
|
|
179
|
+
<div class="flex items-center justify-between p-3 rounded-lg border border-default hover:bg-elevated/50 transition-colors">
|
|
180
|
+
<div class="flex items-center gap-3">
|
|
181
|
+
<span class="text-xs font-mono px-2 py-1 bg-elevated text-highlighted rounded font-semibold border border-default">
|
|
182
|
+
v{v.versionNumber}
|
|
183
|
+
</span>
|
|
184
|
+
<div>
|
|
185
|
+
<p class="text-sm font-medium text-highlighted">{v.changeSummary || "Snapshot"}</p>
|
|
186
|
+
<p class="text-xs text-muted">{new Date(v.createdAt).toLocaleString()}</p>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
<span class={`text-xs px-2 py-0.5 rounded font-mono ${
|
|
190
|
+
v.status === 'published' ? 'bg-emerald-500/20 text-emerald-400' : 'bg-muted text-muted'
|
|
191
|
+
}`}>
|
|
192
|
+
{v.status}
|
|
193
|
+
</span>
|
|
194
|
+
</div>
|
|
195
|
+
))}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<script is:inline define:vars={{ pageId: id, draftContent: draftSnapshot.content }}>
|
|
201
|
+
const approveBtn = document.getElementById("approve-publish-btn");
|
|
202
|
+
const btnText = document.getElementById("approve-btn-text");
|
|
203
|
+
|
|
204
|
+
approveBtn?.addEventListener("click", async () => {
|
|
205
|
+
if (!confirm("Are you sure you want to approve and publish these changes?")) return;
|
|
206
|
+
|
|
207
|
+
if (approveBtn) approveBtn.disabled = true;
|
|
208
|
+
if (btnText) btnText.textContent = "Publishing...";
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const res = await fetch(`/api/cms/pages/${pageId}/publish`, {
|
|
212
|
+
method: "POST",
|
|
213
|
+
headers: { "Content-Type": "application/json" },
|
|
214
|
+
body: JSON.stringify({
|
|
215
|
+
content: draftContent,
|
|
216
|
+
changeSummary: "Approved via CMS Review"
|
|
217
|
+
})
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
if (!res.ok) {
|
|
221
|
+
const err = await res.json();
|
|
222
|
+
alert(err.error || "Failed to publish version");
|
|
223
|
+
} else {
|
|
224
|
+
if (btnText) btnText.textContent = "Published!";
|
|
225
|
+
setTimeout(() => {
|
|
226
|
+
window.location.reload();
|
|
227
|
+
}, 1500);
|
|
228
|
+
}
|
|
229
|
+
} catch (err) {
|
|
230
|
+
alert("Failed to publish version");
|
|
231
|
+
} finally {
|
|
232
|
+
if (approveBtn) approveBtn.disabled = false;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
</script>
|
|
236
|
+
</CMSDashboardLayout>
|