@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,203 @@
1
+ ---
2
+ import CMSDashboardLayout from "../layouts/CMSDashboardLayout.astro";
3
+ import RLABadge from "@rimelight/ui/components/badge/RLABadge.astro";
4
+ import { db } from "virtual:rimelight-cms/db";
5
+ import { getSiteSettings } from "../../services/site-settings.ts";
6
+
7
+ export const prerender = false;
8
+
9
+ let settings: any = {
10
+ name: "Rimelight",
11
+ description: "Next-generation game studio & platform",
12
+ url: "https://rimelight.com",
13
+ author: "Rimelight Entertainment",
14
+ email: "contact@rimelight.com",
15
+ branding: {},
16
+ seo: {}
17
+ };
18
+
19
+ if (db) {
20
+ try {
21
+ settings = await getSiteSettings(db);
22
+ } catch (err) {
23
+ console.warn("Failed to load settings:", err);
24
+ }
25
+ }
26
+ ---
27
+
28
+ <CMSDashboardLayout title="Settings" description="CMS Settings" activeSection="settings">
29
+ <Fragment slot="toolbar-right">
30
+ <button
31
+ id="save-settings-btn"
32
+ type="button"
33
+ class="inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-white bg-primary rounded-md hover:bg-primary/90 transition-colors shadow-sm cursor-pointer disabled:opacity-50"
34
+ >
35
+ <span id="btn-text">Save Settings</span>
36
+ </button>
37
+ </Fragment>
38
+
39
+ <form id="settings-form" class="max-w-3xl mx-auto space-y-8 pb-12">
40
+ <!-- General Site Identity -->
41
+ <div class="bg-elevated/40 border border-default rounded-xl shadow p-6">
42
+ <div class="flex items-center justify-between mb-6">
43
+ <div>
44
+ <h2 class="text-xl font-semibold text-highlighted">Site Identity & Metadata</h2>
45
+ <p class="text-sm text-muted mt-1">Dynamic database-backed site configuration</p>
46
+ </div>
47
+ <RLABadge variant="soft" color="primary" size="sm">Dynamic DB</RLABadge>
48
+ </div>
49
+
50
+ <div class="space-y-4">
51
+ <div>
52
+ <label for="setting-name" class="block text-sm font-medium text-highlighted mb-1">Site Name</label>
53
+ <input
54
+ id="setting-name"
55
+ name="name"
56
+ type="text"
57
+ value={settings.name}
58
+ class="w-full px-3 py-2 bg-elevated border border-default rounded-md shadow-sm text-highlighted focus:ring-2 focus:ring-primary/50 text-sm"
59
+ />
60
+ </div>
61
+
62
+ <div>
63
+ <label for="setting-description" class="block text-sm font-medium text-highlighted mb-1">Site Description</label>
64
+ <textarea
65
+ id="setting-description"
66
+ name="description"
67
+ rows="3"
68
+ class="w-full px-3 py-2 bg-elevated border border-default rounded-md shadow-sm text-highlighted focus:ring-2 focus:ring-primary/50 text-sm"
69
+ >{settings.description}</textarea>
70
+ </div>
71
+
72
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
73
+ <div>
74
+ <label for="setting-url" class="block text-sm font-medium text-highlighted mb-1">Site URL</label>
75
+ <input
76
+ id="setting-url"
77
+ name="url"
78
+ type="url"
79
+ value={settings.url}
80
+ class="w-full px-3 py-2 bg-elevated border border-default rounded-md shadow-sm text-highlighted focus:ring-2 focus:ring-primary/50 text-sm"
81
+ />
82
+ </div>
83
+ <div>
84
+ <label for="setting-author" class="block text-sm font-medium text-highlighted mb-1">Author / Organization</label>
85
+ <input
86
+ id="setting-author"
87
+ name="author"
88
+ type="text"
89
+ value={settings.author}
90
+ class="w-full px-3 py-2 bg-elevated border border-default rounded-md shadow-sm text-highlighted focus:ring-2 focus:ring-primary/50 text-sm"
91
+ />
92
+ </div>
93
+ </div>
94
+
95
+ <div>
96
+ <label for="setting-email" class="block text-sm font-medium text-highlighted mb-1">Contact Email</label>
97
+ <input
98
+ id="setting-email"
99
+ name="email"
100
+ type="email"
101
+ value={settings.email}
102
+ class="w-full px-3 py-2 bg-elevated border border-default rounded-md shadow-sm text-highlighted focus:ring-2 focus:ring-primary/50 text-sm"
103
+ />
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <!-- SEO Defaults -->
109
+ <div class="bg-elevated/40 border border-default rounded-xl shadow p-6">
110
+ <h2 class="text-xl font-semibold mb-6 text-highlighted">SEO & Social Sharing</h2>
111
+ <div class="space-y-4">
112
+ <div>
113
+ <label for="setting-title-template" class="block text-sm font-medium text-highlighted mb-1">Title Template</label>
114
+ <input
115
+ id="setting-title-template"
116
+ name="seo_title_template"
117
+ type="text"
118
+ value={settings.seo?.titleTemplate || "%s | " + settings.name}
119
+ class="w-full px-3 py-2 bg-elevated border border-default rounded-md shadow-sm text-highlighted focus:ring-2 focus:ring-primary/50 text-sm"
120
+ />
121
+ </div>
122
+
123
+ <div>
124
+ <label for="setting-og-image" class="block text-sm font-medium text-highlighted mb-1">OpenGraph Image Fallback URL</label>
125
+ <input
126
+ id="setting-og-image"
127
+ name="og_image"
128
+ type="text"
129
+ value={settings.ogImage || settings.seo?.ogImageFallback || ""}
130
+ class="w-full px-3 py-2 bg-elevated border border-default rounded-md shadow-sm text-highlighted focus:ring-2 focus:ring-primary/50 text-sm"
131
+ />
132
+ </div>
133
+
134
+ <div>
135
+ <label for="setting-max-desc" class="block text-sm font-medium text-highlighted mb-1">Max Description Length (chars)</label>
136
+ <input
137
+ id="setting-max-desc"
138
+ name="seo_max_description_length"
139
+ type="number"
140
+ value={settings.seo?.maxDescriptionLength || 160}
141
+ class="w-full px-3 py-2 bg-elevated border border-default rounded-md shadow-sm text-highlighted focus:ring-2 focus:ring-primary/50 text-sm"
142
+ />
143
+ </div>
144
+ </div>
145
+ </div>
146
+ </form>
147
+
148
+ <script>
149
+ const saveBtn = document.getElementById("save-settings-btn");
150
+ const btnText = document.getElementById("btn-text");
151
+
152
+ saveBtn?.addEventListener("click", async () => {
153
+ const name = (document.getElementById("setting-name") as HTMLInputElement)?.value || "";
154
+ const description = (document.getElementById("setting-description") as HTMLTextAreaElement)?.value || "";
155
+ const url = (document.getElementById("setting-url") as HTMLInputElement)?.value || "";
156
+ const author = (document.getElementById("setting-author") as HTMLInputElement)?.value || "";
157
+ const email = (document.getElementById("setting-email") as HTMLInputElement)?.value || "";
158
+ const ogImage = (document.getElementById("setting-og-image") as HTMLInputElement)?.value || "";
159
+
160
+ const titleTemplate = (document.getElementById("setting-title-template") as HTMLInputElement)?.value || "";
161
+ const maxDescLength = parseInt((document.getElementById("setting-max-desc") as HTMLInputElement)?.value || "160", 10);
162
+
163
+ const payload = {
164
+ name,
165
+ description,
166
+ url,
167
+ author,
168
+ email,
169
+ ogImage,
170
+ seo: {
171
+ titleTemplate,
172
+ ogImageFallback: ogImage,
173
+ maxDescriptionLength: maxDescLength
174
+ }
175
+ };
176
+
177
+ if (saveBtn) (saveBtn as HTMLButtonElement).disabled = true;
178
+ if (btnText) btnText.textContent = "Saving...";
179
+
180
+ try {
181
+ const res = await fetch("/api/cms/settings", {
182
+ method: "PUT",
183
+ headers: { "Content-Type": "application/json" },
184
+ body: JSON.stringify(payload)
185
+ });
186
+
187
+ if (!res.ok) {
188
+ const err = await res.json();
189
+ alert(err.error || "Failed to save settings");
190
+ } else {
191
+ if (btnText) btnText.textContent = "Saved!";
192
+ setTimeout(() => {
193
+ if (btnText) btnText.textContent = "Save Settings";
194
+ }, 2000);
195
+ }
196
+ } catch {
197
+ alert("Failed to save settings");
198
+ } finally {
199
+ if (saveBtn) (saveBtn as HTMLButtonElement).disabled = false;
200
+ }
201
+ });
202
+ </script>
203
+ </CMSDashboardLayout>
@@ -0,0 +1,21 @@
1
+ ---
2
+ import CMSDashboardLayout from "../layouts/CMSDashboardLayout.astro";
3
+ import { RimelightTaxonomyMasterDetail } from "../../client/components";
4
+
5
+ export const prerender = false;
6
+ ---
7
+
8
+ <CMSDashboardLayout
9
+ title="Tags"
10
+ description="Manage content tags and keywords"
11
+ activeSection="tags"
12
+ >
13
+ <div class="w-full">
14
+ <RimelightTaxonomyMasterDetail
15
+ client:only="solid"
16
+ taxonomy="tag"
17
+ title="Tags"
18
+ description="Label articles and posts with keywords for easy filtering."
19
+ />
20
+ </div>
21
+ </CMSDashboardLayout>
@@ -0,0 +1,234 @@
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 RLATextarea from "@rimelight/ui/components/textarea/RLATextarea.astro";
6
+ import RLASelect from "@rimelight/ui/components/select/RLASelect.astro";
7
+ import { db } from "virtual:rimelight-cms/db";
8
+ import { pageTemplates } from "../../schema/page_templates.ts";
9
+ import { eq } from "drizzle-orm";
10
+ import { getRelativeLocaleUrl } from "@rimelight/i18n";
11
+
12
+ export const prerender = false;
13
+
14
+ const currentLocale = Astro.currentLocale ?? "en";
15
+ const id = Astro.params.id;
16
+
17
+ let template: any = null;
18
+ if (db && id) {
19
+ try {
20
+ const templateResult = await db
21
+ .select()
22
+ .from(pageTemplates)
23
+ .where(eq(pageTemplates.id, id as any))
24
+ .limit(1);
25
+ template = templateResult[0] || null;
26
+ } catch (err) {
27
+ console.warn("Failed to fetch template:", err);
28
+ }
29
+ }
30
+
31
+ if (!template) {
32
+ return Astro.redirect(getRelativeLocaleUrl(currentLocale, "/cms/templates"));
33
+ }
34
+
35
+ const defaultRolesList = [
36
+ { name: "admin", displayName: "Administrator", description: "Full system & CMS access" },
37
+ { name: "editor", displayName: "Editor", description: "Can create and publish pages" },
38
+ { name: "author", displayName: "Author", description: "Can create and edit drafts" },
39
+ { name: "reviewer", displayName: "Reviewer", description: "Can review & approve versions" },
40
+ { name: "viewer", displayName: "Viewer", description: "Read-only access" }
41
+ ];
42
+
43
+ const roleOptions = defaultRolesList.map((r) => ({
44
+ label: r.displayName || r.name,
45
+ value: r.name,
46
+ description: r.description || ""
47
+ }));
48
+
49
+ const rolePerms = template.rolePermissions || {
50
+ whoCanCreate: template.allowedRoles || [],
51
+ whoCanEdit: [],
52
+ whoCanReview: template.approvalRules?.requiredRolesForApproval || [],
53
+ whoCanView: []
54
+ };
55
+
56
+ const titleVal = typeof template.title === "object" && template.title !== null ? (template.title as any).en || "" : template.title || "";
57
+ const descVal = typeof template.description === "object" && template.description !== null ? (template.description as any).en || "" : template.description || "";
58
+ ---
59
+
60
+ <CMSDashboardLayout title={`Edit Template: ${titleVal}`} description="Edit CMS template" activeSection="templates">
61
+ <div class="max-w-3xl mx-auto space-y-6">
62
+ <form id="template-edit-form" class="bg-elevated/40 border border-default p-6 rounded-2xl shadow space-y-6">
63
+ <div>
64
+ <label class="block text-sm font-medium text-highlighted mb-2">Slug</label>
65
+ <RLAInput name="slug" value={template.slug} required />
66
+ </div>
67
+
68
+ <div>
69
+ <label class="block text-sm font-medium text-highlighted mb-2">Title</label>
70
+ <RLAInput name="title" value={titleVal} required />
71
+ </div>
72
+
73
+ <div>
74
+ <label class="block text-sm font-medium text-highlighted mb-2">Description</label>
75
+ <RLATextarea name="description" rows="3" value={descVal} />
76
+ </div>
77
+
78
+ <div>
79
+ <label class="block text-sm font-medium text-highlighted mb-2">Page Type</label>
80
+ <RLASelect
81
+ name="pageType"
82
+ placeholder="Select page type..."
83
+ value={template.pageType}
84
+ items={[
85
+ { label: "Blog Post", value: "blog" },
86
+ { label: "Wiki Page", value: "wiki" },
87
+ { label: "Documentation", value: "doc" },
88
+ { label: "Announcement", value: "announcement" }
89
+ ]}
90
+ required
91
+ />
92
+ </div>
93
+
94
+ <div class="border-t border-default pt-6 space-y-4">
95
+ <h3 class="text-md font-semibold text-highlighted">Role Permissions</h3>
96
+ <p class="text-xs text-muted mb-4">Define granular user role access for pages instantiated from this template.</p>
97
+
98
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
99
+ <div>
100
+ <label class="block text-sm font-medium text-highlighted mb-1">Who Can Create</label>
101
+ <RLASelect
102
+ name="whoCanCreate"
103
+ placeholder="Select creator roles..."
104
+ multiple={true}
105
+ value={rolePerms.whoCanCreate || []}
106
+ items={roleOptions}
107
+ />
108
+ </div>
109
+
110
+ <div>
111
+ <label class="block text-sm font-medium text-highlighted mb-1">Who Can Edit</label>
112
+ <RLASelect
113
+ name="whoCanEdit"
114
+ placeholder="Select editor roles..."
115
+ multiple={true}
116
+ value={rolePerms.whoCanEdit || []}
117
+ items={roleOptions}
118
+ />
119
+ </div>
120
+
121
+ <div>
122
+ <label class="block text-sm font-medium text-highlighted mb-1">Who Can Review</label>
123
+ <RLASelect
124
+ name="whoCanReview"
125
+ placeholder="Select reviewer roles..."
126
+ multiple={true}
127
+ value={rolePerms.whoCanReview || []}
128
+ items={roleOptions}
129
+ />
130
+ </div>
131
+
132
+ <div>
133
+ <label class="block text-sm font-medium text-highlighted mb-1">Who Can View</label>
134
+ <RLASelect
135
+ name="whoCanView"
136
+ placeholder="Select viewer roles..."
137
+ multiple={true}
138
+ value={rolePerms.whoCanView || []}
139
+ items={roleOptions}
140
+ />
141
+ </div>
142
+ </div>
143
+ </div>
144
+
145
+ <div class="border-t border-default pt-6">
146
+ <rimelight-template-editor id="template-block-editor"></rimelight-template-editor>
147
+ </div>
148
+
149
+ <div class="border-t border-default pt-6 flex items-center justify-end gap-3">
150
+ <RLAButton href={getRelativeLocaleUrl(currentLocale, "/cms/templates")} variant="outline" color="neutral" size="sm">
151
+ Cancel
152
+ </RLAButton>
153
+ <RLAButton variant="solid" color="primary" size="sm" type="submit">
154
+ Save Changes
155
+ </RLAButton>
156
+ </div>
157
+ </form>
158
+ </div>
159
+
160
+ <script is:inline define:vars={{ currentLocale, templateId: id, initialBlocks: template.initialBlocks || [] }}>
161
+ import('@rimelight/cms/client/loader').then(({ defineCustomElements }) => {
162
+ defineCustomElements();
163
+ const templateEditor = document.getElementById('template-block-editor');
164
+ if (templateEditor && typeof templateEditor.setBlocks === 'function') {
165
+ templateEditor.setBlocks(initialBlocks);
166
+ }
167
+ return null;
168
+ });
169
+
170
+ function parseRoles(rawVal) {
171
+ if (!rawVal) return [];
172
+ try {
173
+ const parsed = JSON.parse(rawVal);
174
+ return Array.isArray(parsed) ? parsed : [rawVal];
175
+ } catch {
176
+ return rawVal.split(',').map(r => r.trim()).filter(Boolean);
177
+ }
178
+ }
179
+
180
+ document.getElementById('template-edit-form')?.addEventListener('submit', async (e) => {
181
+ e.preventDefault();
182
+
183
+ const formData = new FormData(e.target);
184
+ const whoCanCreate = parseRoles(formData.get('whoCanCreate'));
185
+ const whoCanEdit = parseRoles(formData.get('whoCanEdit'));
186
+ const whoCanReview = parseRoles(formData.get('whoCanReview'));
187
+ const whoCanView = parseRoles(formData.get('whoCanView'));
188
+
189
+ const templateEditor = document.getElementById('template-block-editor');
190
+ const updatedBlocks = templateEditor && typeof templateEditor.getBlocks === 'function'
191
+ ? templateEditor.getBlocks()
192
+ : [];
193
+
194
+ const rolePermissions = {
195
+ whoCanCreate,
196
+ whoCanEdit,
197
+ whoCanReview,
198
+ whoCanView
199
+ };
200
+
201
+ const data = {
202
+ slug: formData.get('slug'),
203
+ title: formData.get('title'),
204
+ description: formData.get('description'),
205
+ pageType: formData.get('pageType'),
206
+ allowedRoles: whoCanCreate,
207
+ rolePermissions,
208
+ initialBlocks: updatedBlocks,
209
+ approvalRules: {
210
+ allowSelfApproval: true,
211
+ minApprovals: 1,
212
+ requiredRolesForApproval: whoCanReview
213
+ }
214
+ };
215
+
216
+ try {
217
+ const response = await fetch(`/api/cms/templates/${templateId}`, {
218
+ method: 'PUT',
219
+ headers: { 'Content-Type': 'application/json' },
220
+ body: JSON.stringify(data)
221
+ });
222
+
223
+ if (response.ok) {
224
+ window.location.href = `/${currentLocale}/cms/templates`;
225
+ } else {
226
+ const errorData = (await response.json().catch(() => ({})));
227
+ alert(`Failed to save template: ${errorData.error || 'Unknown error'}`);
228
+ }
229
+ } catch {
230
+ alert('Error saving template: Network error');
231
+ }
232
+ });
233
+ </script>
234
+ </CMSDashboardLayout>
@@ -0,0 +1,95 @@
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 { pageTemplates } from "../../schema/page_templates.ts";
7
+ import { desc } from "drizzle-orm";
8
+ import { getRelativeLocaleUrl } from "@rimelight/i18n";
9
+
10
+ export const prerender = false;
11
+
12
+ const currentLocale = Astro.currentLocale ?? "en";
13
+
14
+ let templates: any[] = [];
15
+ if (db) {
16
+ try {
17
+ templates = await db
18
+ .select()
19
+ .from(pageTemplates)
20
+ .orderBy(desc(pageTemplates.createdAt));
21
+ } catch (err) {
22
+ console.warn("Failed to fetch templates:", err);
23
+ }
24
+ }
25
+ ---
26
+
27
+ <CMSDashboardLayout title="Templates" description="Manage CMS templates" activeSection="templates">
28
+ <Fragment slot="toolbar-right">
29
+ <RLAButton href={getRelativeLocaleUrl(currentLocale, "/cms/templates/new")} variant="solid" color="primary" leadingIcon="i-lucide-plus">
30
+ Create Template
31
+ </RLAButton>
32
+ </Fragment>
33
+
34
+ <div class="bg-elevated/40 border border-default rounded-xl overflow-hidden">
35
+ <table class="w-full text-left text-sm">
36
+ <thead class="bg-elevated/70 border-b border-default text-xs font-semibold text-muted uppercase tracking-wider">
37
+ <tr>
38
+ <th class="px-6 py-3">Name</th>
39
+ <th class="px-6 py-3">Type</th>
40
+ <th class="px-6 py-3">Version</th>
41
+ <th class="px-6 py-3 text-right">Actions</th>
42
+ </tr>
43
+ </thead>
44
+ <tbody class="divide-y divide-default/50">
45
+ {templates.map((template: any) => (
46
+ <tr class="hover:bg-elevated/50 transition">
47
+ <td class="px-6 py-4 whitespace-nowrap">
48
+ <div class="text-sm font-medium text-highlighted">{template.title}</div>
49
+ <div class="text-xs font-mono text-muted">{template.slug}</div>
50
+ </td>
51
+ <td class="px-6 py-4 whitespace-nowrap">
52
+ <RLABadge variant="soft" color="primary" size="xs">{template.pageType}</RLABadge>
53
+ </td>
54
+ <td class="px-6 py-4 whitespace-nowrap text-xs font-mono text-muted">
55
+ v{template.version}
56
+ </td>
57
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-right flex items-center justify-end gap-2">
58
+ <RLAButton href={getRelativeLocaleUrl(currentLocale, `/cms/pages/new?templateId=${template.id}`)} variant="ghost" color="primary" size="xs">
59
+ Create Page
60
+ </RLAButton>
61
+ <RLAButton href={getRelativeLocaleUrl(currentLocale, `/cms/templates/${template.id}/edit`)} variant="ghost" color="neutral" size="xs">
62
+ Edit
63
+ </RLAButton>
64
+ <RLAButton variant="ghost" color="error" size="xs" class="delete-template-btn" data-id={template.id}>
65
+ Delete
66
+ </RLAButton>
67
+ </td>
68
+ </tr>
69
+ ))}
70
+ </tbody>
71
+ </table>
72
+ </div>
73
+
74
+ <script>
75
+ document.querySelectorAll('.delete-template-btn').forEach((btn) => {
76
+ btn.addEventListener('click', async () => {
77
+ const id = (btn as HTMLElement).dataset.id;
78
+ if (!id) return;
79
+
80
+ if (!confirm('Are you sure you want to delete this template?')) return;
81
+
82
+ try {
83
+ const res = await fetch(`/api/cms/templates/${id}`, { method: 'DELETE' });
84
+ if (res.ok) {
85
+ window.location.reload();
86
+ } else {
87
+ alert('Failed to delete template');
88
+ }
89
+ } catch {
90
+ alert('Network error deleting template');
91
+ }
92
+ });
93
+ });
94
+ </script>
95
+ </CMSDashboardLayout>