domma-cms 0.10.0 → 0.12.0

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 (119) hide show
  1. package/CLAUDE.md +248 -159
  2. package/admin/css/admin.css +1 -1
  3. package/admin/js/api.js +1 -1
  4. package/admin/js/app.js +7 -3
  5. package/admin/js/config/sidebar-config.js +1 -1
  6. package/admin/js/http-interceptor.js +1 -0
  7. package/admin/js/lib/safe-html.js +1 -0
  8. package/admin/js/templates/layouts.html +5 -4
  9. package/admin/js/templates/notifications.html +14 -0
  10. package/admin/js/templates/plugin-marketplace.html +16 -0
  11. package/admin/js/templates/plugins.html +17 -5
  12. package/admin/js/views/index.js +1 -1
  13. package/admin/js/views/layouts.js +1 -16
  14. package/admin/js/views/notifications.js +1 -0
  15. package/admin/js/views/plugin-marketplace.js +1 -0
  16. package/admin/js/views/plugins.js +16 -16
  17. package/config/navigation.json +5 -72
  18. package/config/plugins.json +10 -14
  19. package/config/presets.json +50 -13
  20. package/config/site.json +11 -63
  21. package/package.json +2 -1
  22. package/plugins/_template/admin/templates/index.html +17 -0
  23. package/plugins/_template/admin/views/index.js +19 -0
  24. package/plugins/_template/config.js +8 -0
  25. package/plugins/_template/plugin.js +23 -0
  26. package/plugins/_template/plugin.json +34 -0
  27. package/plugins/analytics/plugin.json +41 -31
  28. package/plugins/blog/admin/templates/blog.html +22 -0
  29. package/plugins/blog/admin/templates/categories.html +7 -0
  30. package/plugins/blog/admin/templates/comments.html +11 -0
  31. package/plugins/blog/admin/templates/post-editor.html +97 -0
  32. package/plugins/blog/admin/templates/settings.html +11 -0
  33. package/plugins/blog/admin/views/blog.js +183 -0
  34. package/plugins/blog/admin/views/categories.js +235 -0
  35. package/plugins/blog/admin/views/comments.js +187 -0
  36. package/plugins/blog/admin/views/post-editor.js +291 -0
  37. package/plugins/blog/admin/views/settings.js +100 -0
  38. package/plugins/blog/collections/categories/schema.json +12 -0
  39. package/plugins/blog/collections/comments/schema.json +16 -0
  40. package/plugins/blog/collections/posts/schema.json +19 -0
  41. package/plugins/blog/config.js +8 -0
  42. package/plugins/blog/plugin.js +352 -0
  43. package/plugins/blog/plugin.json +96 -0
  44. package/plugins/blog/roles/blog-author.json +10 -0
  45. package/plugins/blog/roles/blog-editor.json +12 -0
  46. package/plugins/blog/templates/author.html +9 -0
  47. package/plugins/blog/templates/category.html +9 -0
  48. package/plugins/blog/templates/index.html +9 -0
  49. package/plugins/blog/templates/post.html +17 -0
  50. package/plugins/blog/templates/tag.html +9 -0
  51. package/plugins/contacts/collections/user-contact-groups/schema.json +1 -1
  52. package/plugins/contacts/collections/user-contacts/schema.json +1 -1
  53. package/plugins/contacts/plugin.js +4 -10
  54. package/plugins/contacts/plugin.json +13 -3
  55. package/plugins/notes/collections/user-notes/schema.json +1 -1
  56. package/plugins/notes/plugin.js +3 -9
  57. package/plugins/notes/plugin.json +13 -3
  58. package/plugins/site-search/plugin.json +5 -2
  59. package/plugins/theme-switcher/plugin.json +1 -1
  60. package/plugins/todo/collections/todos/schema.json +1 -1
  61. package/plugins/todo/plugin.js +3 -9
  62. package/plugins/todo/plugin.json +13 -3
  63. package/public/css/site.css +1 -1
  64. package/scripts/build.js +48 -0
  65. package/scripts/create-plugin.js +113 -0
  66. package/scripts/fresh.js +6 -7
  67. package/scripts/gen-instance-secret.js +46 -0
  68. package/scripts/reset.js +3 -3
  69. package/scripts/setup.js +31 -13
  70. package/server/middleware/auth.js +48 -0
  71. package/server/middleware/managerAuth.js +36 -0
  72. package/server/routes/api/actions.js +1 -1
  73. package/server/routes/api/auth.js +4 -3
  74. package/server/routes/api/layouts.js +173 -49
  75. package/server/routes/api/notifications.js +155 -0
  76. package/server/routes/api/plugin-marketplace.js +75 -0
  77. package/server/routes/api/users.js +1 -1
  78. package/server/routes/api/views.js +1 -1
  79. package/server/routes/public.js +4 -9
  80. package/server/server.js +32 -3
  81. package/server/services/actions.js +1 -1
  82. package/server/services/managerClient.js +182 -0
  83. package/server/services/permissionRegistry.js +245 -173
  84. package/server/services/pluginInstaller.js +301 -0
  85. package/server/services/plugins.js +117 -10
  86. package/server/services/presetCollections.js +66 -251
  87. package/server/services/renderer.js +99 -0
  88. package/server/services/roles.js +191 -39
  89. package/server/services/users.js +1 -1
  90. package/server/services/views.js +1 -1
  91. package/server/templates/page.html +2 -2
  92. package/plugins/docs/admin/templates/docs.html +0 -69
  93. package/plugins/docs/admin/views/docs.js +0 -276
  94. package/plugins/docs/config.js +0 -8
  95. package/plugins/docs/data/documents/57e003f0-68f2-47dc-9c36-ed4b10ed3deb.json +0 -11
  96. package/plugins/docs/data/folders.json +0 -9
  97. package/plugins/docs/data/templates.json +0 -1
  98. package/plugins/docs/data/versions/57e003f0-68f2-47dc-9c36-ed4b10ed3deb/1.json +0 -5
  99. package/plugins/docs/plugin.js +0 -375
  100. package/plugins/docs/plugin.json +0 -23
  101. package/plugins/form-builder/data/forms/contacts.json +0 -66
  102. package/plugins/form-builder/data/forms/enquiries.json +0 -103
  103. package/plugins/form-builder/data/forms/feedback.json +0 -131
  104. package/plugins/form-builder/data/forms/notes.json +0 -79
  105. package/plugins/form-builder/data/forms/to-do.json +0 -100
  106. package/plugins/form-builder/data/submissions/contacts.json +0 -1
  107. package/plugins/form-builder/data/submissions/enquiries.json +0 -1
  108. package/plugins/form-builder/data/submissions/feedback.json +0 -1
  109. package/plugins/form-builder/data/submissions/notes.json +0 -1
  110. package/plugins/form-builder/data/submissions/to-do.json +0 -1
  111. package/plugins/garage/admin/templates/garage.html +0 -111
  112. package/plugins/garage/admin/views/garage.js +0 -622
  113. package/plugins/garage/collections/garage-vehicles/schema.json +0 -101
  114. package/plugins/garage/config.js +0 -18
  115. package/plugins/garage/data/vehicles.json +0 -70
  116. package/plugins/garage/plugin.js +0 -398
  117. package/plugins/garage/plugin.json +0 -33
  118. package/scripts/seed.js +0 -1996
  119. package/server/services/userTypes.js +0 -227
package/config/site.json CHANGED
@@ -1,27 +1,14 @@
1
1
  {
2
- "title": "My Nice Little Super Blog!",
3
- "tagline": "A nice little bloggy type thingy.",
4
- "fontFamily": "Roboto",
5
- "fontSize": 16,
6
- "theme": "dreamy-light",
7
- "autoTheme": {
8
- "enabled": false,
9
- "dayTheme": "charcoal-light",
10
- "nightTheme": "charcoal-dark",
11
- "dayStart": "07:00",
12
- "nightStart": "19:00"
13
- },
14
- "adminTheme": "charcoal-dark",
15
- "layoutOptions": {
16
- "spacerSize": 40
17
- },
2
+ "title": "My Bloggy Thing",
3
+ "tagline": "This is the Domma CMS Testbed",
4
+ "theme": "charcoal-dark",
18
5
  "seo": {
19
- "defaultTitle": "My Nice Little Super Blog!",
6
+ "defaultTitle": "My Bloggy Thing",
20
7
  "titleSeparator": " | ",
21
8
  "defaultDescription": "A site built with Domma CMS"
22
9
  },
23
10
  "footer": {
24
- "copyright": "© 2026 DCBW Consulting Ltd",
11
+ "copyright": "© My Site. All rights reserved.",
25
12
  "links": [
26
13
  {
27
14
  "text": "Privacy Policy",
@@ -30,59 +17,20 @@
30
17
  {
31
18
  "text": "Contact",
32
19
  "url": "/contact"
33
- },
34
- {
35
- "text": "GDPR",
36
- "url": "/gdpr"
37
20
  }
38
21
  ]
39
22
  },
40
- "social": {
41
- "twitter": "",
42
- "facebook": "",
43
- "instagram": "",
44
- "linkedin": "",
45
- "github": "https://github.com/pinpointzero73/",
46
- "youtube": ""
47
- },
23
+ "social": {},
48
24
  "smtp": {
49
- "host": "192.168.1.27",
50
- "port": 1025,
25
+ "host": "",
26
+ "port": 587,
51
27
  "user": "",
52
28
  "pass": "",
53
29
  "secure": false,
54
- "fromAddress": "noreply@dwit.uk",
55
- "fromName": "My Big Boss Website"
56
- },
57
- "backToTop": {
58
- "enabled": true,
59
- "scrollThreshold": 180,
60
- "position": "bottom-right",
61
- "offset": 16,
62
- "bottomOffset": 16,
63
- "label": "",
64
- "smooth": true
65
- },
66
- "cookieConsent": {
67
- "enabled": true,
68
- "message": "We use cookies to enhance your browsing experience, serve personalised content, and analyse our traffic. By clicking \"Accept All\", you consent to our use of cookies.",
69
- "acceptAllText": "Accept All",
70
- "rejectAllText": "Reject All",
71
- "customizeText": "Customize",
72
- "savePreferencesText": "Save Preferences",
73
- "privacyPolicyText": "Privacy Policy",
74
- "privacyPolicyUrl": "/privacy-policy",
75
- "cookiePolicyText": "Cookie Policy",
76
- "cookiePolicyUrl": "",
77
- "position": "bottom",
78
- "layout": "bar",
79
- "theme": "dark",
80
- "showFunctional": true,
81
- "showAnalytics": true,
82
- "showMarketing": true,
83
- "consentVersion": "1.0"
30
+ "fromAddress": "",
31
+ "fromName": ""
84
32
  },
85
33
  "adminBrand": {
86
- "title": "Domma Admin"
34
+ "title": "Domma CMS"
87
35
  }
88
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -27,6 +27,7 @@
27
27
  ],
28
28
  "scripts": {
29
29
  "build": "node scripts/build.js",
30
+ "create-plugin": "node scripts/create-plugin.js",
30
31
  "delete-users": "node -e \"import('fs').then(({readdirSync,rmSync})=>{const d='content/users';readdirSync(d).filter(f=>f.endsWith('.json')).forEach(f=>{rmSync(d+'/'+f);console.log('deleted',f)})})\"",
31
32
  "start": "node server/server.js",
32
33
  "pm2:start": "pm2 start ecosystem.config.cjs --env production",
@@ -0,0 +1,17 @@
1
+ <div class="view-header d-flex justify-content-between align-items-center mb-4">
2
+ <div>
3
+ <h1 class="view-title">PLUGIN_DISPLAY_NAME</h1>
4
+ <p class="view-subtitle text-muted">Manage PLUGIN_DISPLAY_NAME.</p>
5
+ </div>
6
+ <div class="d-flex gap-2">
7
+ <button id="btn-new-item" class="btn btn-primary">
8
+ <span data-icon="plus"></span> New Item
9
+ </button>
10
+ </div>
11
+ </div>
12
+
13
+ <div class="card">
14
+ <div class="card-body">
15
+ <div id="items-table"></div>
16
+ </div>
17
+ </div>
@@ -0,0 +1,19 @@
1
+ /**
2
+ * PLUGIN_DISPLAY_NAME — admin view.
3
+ * Route: #/plugins/PLUGIN_SLUG
4
+ */
5
+ export const indexView = {
6
+ templateUrl: '/plugins/PLUGIN_SLUG/admin/templates/index.html',
7
+
8
+ async onMount($container) {
9
+ // Your view logic here.
10
+ // Available globals: $, _, M, D, S, H, F, E, I, T, R
11
+ //
12
+ // Fetch data: const data = await H.get('/api/plugins/PLUGIN_SLUG/items');
13
+ // Show toast: E.toast('Saved!', { type: 'success' });
14
+ // Confirm: if (!await E.confirm('Are you sure?')) return;
15
+ // Create table: T.create(el, { data, columns: [...] });
16
+ // Create form: F.create({ fields }, defaults, { showSubmitButton: false }).renderTo(el);
17
+ I.scan();
18
+ }
19
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * PLUGIN_DISPLAY_NAME plugin — default configuration.
3
+ * Merged with user overrides from config/plugins.json.
4
+ */
5
+ export default {
6
+ // Add your config keys and defaults here.
7
+ // Example: itemsPerPage: 20
8
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * PLUGIN_DISPLAY_NAME plugin — server-side routes and lifecycle hooks.
3
+ */
4
+
5
+ export default async function plugin(fastify, options) {
6
+ const { authenticate, requireAdmin } = options.auth;
7
+ const BASE = `/api/plugins/PLUGIN_SLUG`;
8
+
9
+ // Add your API routes here.
10
+ // Example:
11
+ // fastify.get(`${BASE}/items`, { preHandler: [authenticate] }, async (request, reply) => {
12
+ // return { items: [] };
13
+ // });
14
+ }
15
+
16
+ export async function onEnable({ fastify, services }) {
17
+ // Called when the plugin is enabled via admin.
18
+ // Create collections, pages, or forms here if needed.
19
+ }
20
+
21
+ export async function onDisable({ fastify, services }) {
22
+ // Called when the plugin is disabled via admin.
23
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "PLUGIN_SLUG",
3
+ "displayName": "PLUGIN_DISPLAY_NAME",
4
+ "version": "1.0.0",
5
+ "description": "PLUGIN_DESCRIPTION",
6
+ "author": "PLUGIN_AUTHOR",
7
+ "date": "PLUGIN_DATE",
8
+ "icon": "box",
9
+ "admin": {
10
+ "css": ["admin/css/index.css"],
11
+ "sidebar": [
12
+ {
13
+ "id": "PLUGIN_SLUG",
14
+ "text": "PLUGIN_DISPLAY_NAME",
15
+ "icon": "box",
16
+ "url": "#/plugins/PLUGIN_SLUG",
17
+ "section": "#/plugins/PLUGIN_SLUG"
18
+ }
19
+ ],
20
+ "routes": [
21
+ {
22
+ "path": "/plugins/PLUGIN_SLUG",
23
+ "view": "plugin-PLUGIN_SLUG",
24
+ "title": "PLUGIN_DISPLAY_NAME - Domma CMS"
25
+ }
26
+ ],
27
+ "views": {
28
+ "plugin-PLUGIN_SLUG": {
29
+ "entry": "PLUGIN_SLUG/admin/views/index.js",
30
+ "exportName": "indexView"
31
+ }
32
+ }
33
+ }
34
+ }
@@ -1,35 +1,45 @@
1
1
  {
2
2
  "name": "analytics",
3
- "displayName": "Analytics",
4
- "version": "1.0.0",
5
- "description": "Basic page view analytics. Tracks hits per page using a simple JSON store.",
6
- "author": "Darryl Waterhouse",
7
- "date": "2026-03-01",
8
- "icon": "chart-bar",
9
- "admin": {
10
- "sidebar": [
11
- { "id": "analytics", "text": "Analytics", "icon": "chart-bar", "url": "#/plugins/analytics", "section": "#/plugins/analytics" }
12
- ],
13
- "routes": [
14
- { "path": "/plugins/analytics", "view": "plugin-analytics", "title": "Analytics - Domma CMS" }
15
- ],
16
- "views": {
17
- "plugin-analytics": {
18
- "entry": "analytics/admin/views/analytics.js",
19
- "exportName": "analyticsView"
20
- }
21
- }
22
- },
23
- "inject": {
24
- "head": "public/inject-head.html",
25
- "bodyEnd": "public/inject-body.html"
26
- },
27
- "scaffold": {
28
- "reset": [
29
- {
30
- "path": "stats.json",
31
- "content": "{}"
32
- }
33
- ]
3
+ "displayName": "Analytics",
4
+ "version": "1.0.0",
5
+ "description": "Basic page view analytics. Tracks hits per page using a simple JSON store.",
6
+ "author": "Darryl Waterhouse",
7
+ "date": "2026-03-01",
8
+ "icon": "chart-bar",
9
+ "admin": {
10
+ "sidebar": [
11
+ {
12
+ "id": "analytics",
13
+ "text": "Analytics",
14
+ "icon": "chart-bar",
15
+ "url": "#/plugins/analytics",
16
+ "section": "#/plugins/analytics"
17
+ }
18
+ ],
19
+ "routes": [
20
+ {
21
+ "path": "/plugins/analytics",
22
+ "view": "plugin-analytics",
23
+ "title": "Analytics - Domma CMS"
24
+ }
25
+ ],
26
+ "views": {
27
+ "plugin-analytics": {
28
+ "entry": "analytics/admin/views/analytics.js?v=80339cdc",
29
+ "exportName": "analyticsView"
30
+ }
34
31
  }
32
+ },
33
+ "inject": {
34
+ "head": "public/inject-head.html",
35
+ "bodyEnd": "public/inject-body.html"
36
+ },
37
+ "scaffold": {
38
+ "reset": [
39
+ {
40
+ "path": "stats.json",
41
+ "content": "{}"
42
+ }
43
+ ]
44
+ }
35
45
  }
@@ -0,0 +1,22 @@
1
+ <div class="view-header d-flex align-items-center justify-content-between mb-4">
2
+ <h1 class="h3 mb-0">Blog Posts</h1>
3
+ <button id="btn-new-post" class="btn btn-primary">
4
+ <span data-icon="plus"></span> New Post
5
+ </button>
6
+ </div>
7
+
8
+ <div class="card mb-3">
9
+ <div class="card-body">
10
+ <div class="d-flex gap-2 flex-wrap">
11
+ <select id="filter-status" class="form-select" style="width:auto;">
12
+ <option value="">All statuses</option>
13
+ <option value="draft">Draft</option>
14
+ <option value="scheduled">Scheduled</option>
15
+ <option value="published">Published</option>
16
+ </select>
17
+ <input id="filter-search" type="text" class="form-control" style="width:200px;" placeholder="Search posts…">
18
+ </div>
19
+ </div>
20
+ </div>
21
+
22
+ <div id="posts-table"></div>
@@ -0,0 +1,7 @@
1
+ <div class="view-header d-flex align-items-center justify-content-between mb-4">
2
+ <h1 class="h3 mb-0">Blog Categories</h1>
3
+ <button id="btn-new-category" class="btn btn-primary">
4
+ <span data-icon="plus"></span> New Category
5
+ </button>
6
+ </div>
7
+ <div id="categories-table"></div>
@@ -0,0 +1,11 @@
1
+ <div class="view-header mb-4">
2
+ <h1 class="h3 mb-0">Blog Comments</h1>
3
+ </div>
4
+
5
+ <div class="tab-list mb-3">
6
+ <button class="tab-item active" data-tab="pending">Pending</button>
7
+ <button class="tab-item" data-tab="approved">Approved</button>
8
+ <button class="tab-item" data-tab="spam">Spam</button>
9
+ </div>
10
+
11
+ <div id="comments-table"></div>
@@ -0,0 +1,97 @@
1
+ <div class="view-header d-flex align-items-center justify-content-between mb-4">
2
+ <h1 class="h3 mb-0" id="editor-title">New Post</h1>
3
+ <div class="d-flex gap-2">
4
+ <button id="btn-save-draft" class="btn btn-secondary">Save Draft</button>
5
+ <button id="btn-schedule" class="btn btn-warning" style="display:none;">Schedule</button>
6
+ <button id="btn-publish" class="btn btn-primary">Publish Now</button>
7
+ </div>
8
+ </div>
9
+
10
+ <div class="d-flex gap-4" style="align-items:flex-start;">
11
+ <!-- Main content area -->
12
+ <div style="flex:1; min-width:0;">
13
+ <div class="card mb-3">
14
+ <div class="card-body">
15
+ <div class="mb-3">
16
+ <label class="form-label fw-bold">Title</label>
17
+ <input type="text" id="post-title" class="form-control form-control-lg" placeholder="Post title…">
18
+ </div>
19
+ <div class="mb-3">
20
+ <label class="form-label">Slug</label>
21
+ <div class="d-flex gap-2 align-items-center">
22
+ <input type="text" id="post-slug" class="form-control font-monospace">
23
+ <label class="d-flex align-items-center gap-1 text-muted small" style="white-space:nowrap;">
24
+ <input type="checkbox" id="slug-auto"> Auto
25
+ </label>
26
+ </div>
27
+ <div id="slug-warning" class="text-warning small mt-1" style="display:none;">
28
+ ⚠ Changing slug breaks existing links
29
+ </div>
30
+ </div>
31
+ <div class="mb-3">
32
+ <label class="form-label">Excerpt</label>
33
+ <textarea id="post-excerpt" class="form-control" rows="2" placeholder="Short summary…"></textarea>
34
+ </div>
35
+ <div class="mb-3">
36
+ <label class="form-label">Content</label>
37
+ <textarea id="post-content" class="form-control font-monospace" rows="20" placeholder="Write in Markdown…"></textarea>
38
+ </div>
39
+ </div>
40
+ </div>
41
+
42
+ <!-- SEO accordion -->
43
+ <div class="card mb-3">
44
+ <div class="card-header" id="seo-toggle" style="cursor:pointer;">
45
+ SEO <span class="text-muted small">(click to expand)</span>
46
+ </div>
47
+ <div class="card-body" id="seo-panel" style="display:none;">
48
+ <div class="mb-3">
49
+ <label class="form-label">SEO Title</label>
50
+ <input type="text" id="seo-title" class="form-control" placeholder="Overrides page title">
51
+ </div>
52
+ <div class="mb-3">
53
+ <label class="form-label">SEO Description</label>
54
+ <textarea id="seo-description" class="form-control" rows="2" placeholder="Overrides excerpt for meta description"></textarea>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </div>
59
+
60
+ <!-- Sidebar -->
61
+ <div style="width:280px; flex-shrink:0;">
62
+ <div class="card mb-3">
63
+ <div class="card-header">Status</div>
64
+ <div class="card-body">
65
+ <div id="schedule-picker" style="display:none;" class="mb-3">
66
+ <label class="form-label">Scheduled for</label>
67
+ <input type="datetime-local" id="post-scheduled-at" class="form-control">
68
+ </div>
69
+ <span id="post-status-badge" class="badge bg-secondary">draft</span>
70
+ </div>
71
+ </div>
72
+
73
+ <div class="card mb-3">
74
+ <div class="card-header">Categories</div>
75
+ <div class="card-body" id="categories-checklist">
76
+ <p class="text-muted small">Loading…</p>
77
+ </div>
78
+ </div>
79
+
80
+ <div class="card mb-3">
81
+ <div class="card-header">Tags</div>
82
+ <div class="card-body">
83
+ <input type="text" id="post-tags" class="form-control" placeholder="tag1, tag2, …">
84
+ <div class="text-muted small mt-1">Comma-separated</div>
85
+ </div>
86
+ </div>
87
+
88
+ <div class="card mb-3">
89
+ <div class="card-header">Featured Image</div>
90
+ <div class="card-body">
91
+ <div id="featured-image-preview" class="mb-2"></div>
92
+ <button id="btn-pick-image" class="btn btn-sm btn-secondary">Pick Image</button>
93
+ <button id="btn-clear-image" class="btn btn-sm btn-outline-secondary" style="display:none;">Clear</button>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </div>
@@ -0,0 +1,11 @@
1
+ <div class="view-header mb-4">
2
+ <h1 class="h3 mb-0">Blog Settings</h1>
3
+ </div>
4
+
5
+ <div class="alert alert-warning mb-4">
6
+ Base path and posts-per-page changes require a server restart to take effect.
7
+ </div>
8
+
9
+ <div class="card" style="max-width:600px;">
10
+ <div class="card-body" id="settings-form"></div>
11
+ </div>
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Blog posts list view.
3
+ *
4
+ * @module blog/admin/views/blog
5
+ */
6
+
7
+ function escapeHtml(s) {
8
+ return String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
9
+ }
10
+
11
+ const BASE = '/api/plugins/blog';
12
+
13
+ const STATUS_BADGE = {
14
+ draft: 'badge-secondary',
15
+ scheduled: 'badge-warning',
16
+ published: 'badge-success'
17
+ };
18
+
19
+ export const blogView = {
20
+ templateUrl: '/plugins/blog/admin/templates/blog.html',
21
+
22
+ /**
23
+ * Mount the blog posts list view.
24
+ *
25
+ * @param {object} $container - Domma-wrapped container element
26
+ * @returns {Promise<void>}
27
+ */
28
+ async onMount($container) {
29
+ let allPosts = [];
30
+ let postsTable = null;
31
+
32
+ // ---------------------------------------------------------------
33
+ // Table
34
+ // ---------------------------------------------------------------
35
+
36
+ function buildTable(data) {
37
+ const tableEl = $container.find('#posts-table').get(0);
38
+ if (!tableEl) return;
39
+
40
+ if (postsTable) {
41
+ postsTable.setData(data);
42
+ return;
43
+ }
44
+
45
+ postsTable = T.create(tableEl, {
46
+ data,
47
+ columns: [
48
+ {
49
+ key: 'title',
50
+ title: 'Title',
51
+ sortable: true,
52
+ render: (v, row) =>
53
+ `<a href="#/plugins/blog/posts/${escapeHtml(row.id)}" style="font-weight:600;">${escapeHtml(v || 'Untitled')}</a>`
54
+ },
55
+ {
56
+ key: 'status',
57
+ title: 'Status',
58
+ sortable: true,
59
+ render: (v) => {
60
+ const cls = STATUS_BADGE[v] || 'badge-secondary';
61
+ return `<span class="badge ${cls}">${escapeHtml(v ?? 'draft')}</span>`;
62
+ }
63
+ },
64
+ {
65
+ key: 'authorId',
66
+ title: 'Author',
67
+ sortable: false,
68
+ render: (v) => escapeHtml(v) || '—'
69
+ },
70
+ {
71
+ key: 'publishedAt',
72
+ title: 'Published',
73
+ sortable: true,
74
+ render: (v) => v
75
+ ? `<span title="${escapeHtml(D(v).format('DD MMM YYYY HH:mm'))}">${escapeHtml(D(v).format('DD MMM YYYY'))}</span>`
76
+ : '<span style="opacity:0.4;">—</span>'
77
+ },
78
+ {
79
+ key: 'id',
80
+ title: 'Actions',
81
+ sortable: false,
82
+ render: (id) =>
83
+ `<div style="display:flex;gap:.4rem;">` +
84
+ `<button class="btn btn-sm btn-outline post-edit-btn" data-id="${escapeHtml(id)}">` +
85
+ `<span data-icon="edit" data-icon-size="14"></span> Edit</button>` +
86
+ `<button class="btn btn-sm btn-danger post-delete-btn" data-id="${escapeHtml(id)}">` +
87
+ `<span data-icon="trash" data-icon-size="14"></span> Delete</button>` +
88
+ `</div>`
89
+ }
90
+ ],
91
+ emptyMessage: 'No posts found.'
92
+ });
93
+
94
+ // Event delegation for action buttons
95
+ tableEl.addEventListener('click', async (e) => {
96
+ const editBtn = e.target.closest('.post-edit-btn');
97
+ const deleteBtn = e.target.closest('.post-delete-btn');
98
+
99
+ if (editBtn) {
100
+ R.navigate('#/plugins/blog/posts/' + editBtn.dataset.id);
101
+ }
102
+
103
+ if (deleteBtn) {
104
+ const id = deleteBtn.dataset.id;
105
+ const post = allPosts.find((p) => p.id === id);
106
+ const ok = await E.confirm(`Delete "${post?.title ?? 'this post'}"?`);
107
+ if (!ok) return;
108
+ try {
109
+ await H.delete(`${BASE}/posts/${id}`);
110
+ E.toast('Post deleted.', { type: 'success' });
111
+ await reload();
112
+ } catch {
113
+ E.toast('Failed to delete post.', { type: 'error' });
114
+ }
115
+ }
116
+ });
117
+
118
+ Domma.icons.scan();
119
+ }
120
+
121
+ // ---------------------------------------------------------------
122
+ // Filter
123
+ // ---------------------------------------------------------------
124
+
125
+ function applyFilters() {
126
+ const statusEl = $container.find('#filter-status').get(0);
127
+ const searchEl = $container.find('#filter-search').get(0);
128
+ const status = statusEl?.value ?? '';
129
+ const query = (searchEl?.value ?? '').toLowerCase().trim();
130
+
131
+ let filtered = allPosts;
132
+ if (status) filtered = filtered.filter((p) => p.status === status);
133
+ if (query) {
134
+ filtered = filtered.filter((p) =>
135
+ (p.title ?? '').toLowerCase().includes(query) ||
136
+ (p.excerpt ?? '').toLowerCase().includes(query)
137
+ );
138
+ }
139
+ buildTable(filtered);
140
+ }
141
+
142
+ // ---------------------------------------------------------------
143
+ // Load
144
+ // ---------------------------------------------------------------
145
+
146
+ async function reload() {
147
+ try {
148
+ const res = await H.get(`${BASE}/posts`);
149
+ if (res.error) throw new Error(res.error);
150
+ allPosts = res.data ?? [];
151
+ } catch {
152
+ allPosts = [];
153
+ E.toast('Failed to load posts.', { type: 'error' });
154
+ }
155
+ applyFilters();
156
+ }
157
+
158
+ // ---------------------------------------------------------------
159
+ // Event bindings
160
+ // ---------------------------------------------------------------
161
+
162
+ const newPostBtn = $container.find('#btn-new-post').get(0);
163
+ if (newPostBtn) newPostBtn.addEventListener('click', () => R.navigate('#/plugins/blog/posts/new'));
164
+
165
+ const filterStatus = $container.find('#filter-status').get(0);
166
+ if (filterStatus) filterStatus.addEventListener('change', applyFilters);
167
+
168
+ const filterSearch = $container.find('#filter-search').get(0);
169
+ if (filterSearch) {
170
+ let searchTimer = null;
171
+ filterSearch.addEventListener('input', () => {
172
+ clearTimeout(searchTimer);
173
+ searchTimer = setTimeout(applyFilters, 250);
174
+ });
175
+ }
176
+
177
+ // ---------------------------------------------------------------
178
+ // Initial load
179
+ // ---------------------------------------------------------------
180
+ await reload();
181
+ Domma.icons.scan();
182
+ }
183
+ };