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.
- package/CLAUDE.md +248 -159
- package/admin/css/admin.css +1 -1
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +7 -3
- package/admin/js/config/sidebar-config.js +1 -1
- package/admin/js/http-interceptor.js +1 -0
- package/admin/js/lib/safe-html.js +1 -0
- package/admin/js/templates/layouts.html +5 -4
- package/admin/js/templates/notifications.html +14 -0
- package/admin/js/templates/plugin-marketplace.html +16 -0
- package/admin/js/templates/plugins.html +17 -5
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/layouts.js +1 -16
- package/admin/js/views/notifications.js +1 -0
- package/admin/js/views/plugin-marketplace.js +1 -0
- package/admin/js/views/plugins.js +16 -16
- package/config/navigation.json +5 -72
- package/config/plugins.json +10 -14
- package/config/presets.json +50 -13
- package/config/site.json +11 -63
- package/package.json +2 -1
- package/plugins/_template/admin/templates/index.html +17 -0
- package/plugins/_template/admin/views/index.js +19 -0
- package/plugins/_template/config.js +8 -0
- package/plugins/_template/plugin.js +23 -0
- package/plugins/_template/plugin.json +34 -0
- package/plugins/analytics/plugin.json +41 -31
- package/plugins/blog/admin/templates/blog.html +22 -0
- package/plugins/blog/admin/templates/categories.html +7 -0
- package/plugins/blog/admin/templates/comments.html +11 -0
- package/plugins/blog/admin/templates/post-editor.html +97 -0
- package/plugins/blog/admin/templates/settings.html +11 -0
- package/plugins/blog/admin/views/blog.js +183 -0
- package/plugins/blog/admin/views/categories.js +235 -0
- package/plugins/blog/admin/views/comments.js +187 -0
- package/plugins/blog/admin/views/post-editor.js +291 -0
- package/plugins/blog/admin/views/settings.js +100 -0
- package/plugins/blog/collections/categories/schema.json +12 -0
- package/plugins/blog/collections/comments/schema.json +16 -0
- package/plugins/blog/collections/posts/schema.json +19 -0
- package/plugins/blog/config.js +8 -0
- package/plugins/blog/plugin.js +352 -0
- package/plugins/blog/plugin.json +96 -0
- package/plugins/blog/roles/blog-author.json +10 -0
- package/plugins/blog/roles/blog-editor.json +12 -0
- package/plugins/blog/templates/author.html +9 -0
- package/plugins/blog/templates/category.html +9 -0
- package/plugins/blog/templates/index.html +9 -0
- package/plugins/blog/templates/post.html +17 -0
- package/plugins/blog/templates/tag.html +9 -0
- package/plugins/contacts/collections/user-contact-groups/schema.json +1 -1
- package/plugins/contacts/collections/user-contacts/schema.json +1 -1
- package/plugins/contacts/plugin.js +4 -10
- package/plugins/contacts/plugin.json +13 -3
- package/plugins/notes/collections/user-notes/schema.json +1 -1
- package/plugins/notes/plugin.js +3 -9
- package/plugins/notes/plugin.json +13 -3
- package/plugins/site-search/plugin.json +5 -2
- package/plugins/theme-switcher/plugin.json +1 -1
- package/plugins/todo/collections/todos/schema.json +1 -1
- package/plugins/todo/plugin.js +3 -9
- package/plugins/todo/plugin.json +13 -3
- package/public/css/site.css +1 -1
- package/scripts/build.js +48 -0
- package/scripts/create-plugin.js +113 -0
- package/scripts/fresh.js +6 -7
- package/scripts/gen-instance-secret.js +46 -0
- package/scripts/reset.js +3 -3
- package/scripts/setup.js +31 -13
- package/server/middleware/auth.js +48 -0
- package/server/middleware/managerAuth.js +36 -0
- package/server/routes/api/actions.js +1 -1
- package/server/routes/api/auth.js +4 -3
- package/server/routes/api/layouts.js +173 -49
- package/server/routes/api/notifications.js +155 -0
- package/server/routes/api/plugin-marketplace.js +75 -0
- package/server/routes/api/users.js +1 -1
- package/server/routes/api/views.js +1 -1
- package/server/routes/public.js +4 -9
- package/server/server.js +32 -3
- package/server/services/actions.js +1 -1
- package/server/services/managerClient.js +182 -0
- package/server/services/permissionRegistry.js +245 -173
- package/server/services/pluginInstaller.js +301 -0
- package/server/services/plugins.js +117 -10
- package/server/services/presetCollections.js +66 -251
- package/server/services/renderer.js +99 -0
- package/server/services/roles.js +191 -39
- package/server/services/users.js +1 -1
- package/server/services/views.js +1 -1
- package/server/templates/page.html +2 -2
- package/plugins/docs/admin/templates/docs.html +0 -69
- package/plugins/docs/admin/views/docs.js +0 -276
- package/plugins/docs/config.js +0 -8
- package/plugins/docs/data/documents/57e003f0-68f2-47dc-9c36-ed4b10ed3deb.json +0 -11
- package/plugins/docs/data/folders.json +0 -9
- package/plugins/docs/data/templates.json +0 -1
- package/plugins/docs/data/versions/57e003f0-68f2-47dc-9c36-ed4b10ed3deb/1.json +0 -5
- package/plugins/docs/plugin.js +0 -375
- package/plugins/docs/plugin.json +0 -23
- package/plugins/form-builder/data/forms/contacts.json +0 -66
- package/plugins/form-builder/data/forms/enquiries.json +0 -103
- package/plugins/form-builder/data/forms/feedback.json +0 -131
- package/plugins/form-builder/data/forms/notes.json +0 -79
- package/plugins/form-builder/data/forms/to-do.json +0 -100
- package/plugins/form-builder/data/submissions/contacts.json +0 -1
- package/plugins/form-builder/data/submissions/enquiries.json +0 -1
- package/plugins/form-builder/data/submissions/feedback.json +0 -1
- package/plugins/form-builder/data/submissions/notes.json +0 -1
- package/plugins/form-builder/data/submissions/to-do.json +0 -1
- package/plugins/garage/admin/templates/garage.html +0 -111
- package/plugins/garage/admin/views/garage.js +0 -622
- package/plugins/garage/collections/garage-vehicles/schema.json +0 -101
- package/plugins/garage/config.js +0 -18
- package/plugins/garage/data/vehicles.json +0 -70
- package/plugins/garage/plugin.js +0 -398
- package/plugins/garage/plugin.json +0 -33
- package/scripts/seed.js +0 -1996
- package/server/services/userTypes.js +0 -227
package/config/site.json
CHANGED
|
@@ -1,27 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
"title": "My
|
|
3
|
-
"tagline": "
|
|
4
|
-
"
|
|
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
|
|
6
|
+
"defaultTitle": "My Bloggy Thing",
|
|
20
7
|
"titleSeparator": " | ",
|
|
21
8
|
"defaultDescription": "A site built with Domma CMS"
|
|
22
9
|
},
|
|
23
10
|
"footer": {
|
|
24
|
-
"copyright": "©
|
|
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": "
|
|
50
|
-
"port":
|
|
25
|
+
"host": "",
|
|
26
|
+
"port": 587,
|
|
51
27
|
"user": "",
|
|
52
28
|
"pass": "",
|
|
53
29
|
"secure": false,
|
|
54
|
-
"fromAddress": "
|
|
55
|
-
"fromName": "
|
|
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
|
|
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.
|
|
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,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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
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
|
+
};
|