domma-cms 0.15.0 → 0.16.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/package.json +2 -2
- package/plugins/analytics/admin/templates/analytics.html +52 -1
- package/plugins/analytics/admin/views/analytics.js +157 -32
- package/plugins/analytics/config.js +10 -2
- package/plugins/analytics/plugin.js +214 -25
- package/plugins/analytics/plugin.json +9 -5
- package/plugins/analytics/public/inject-body.html +25 -7
- package/plugins/blog/admin/templates/blog.html +25 -2
- package/plugins/blog/admin/views/blog.js +72 -56
- package/plugins/blog/admin/views/post-editor.js +98 -79
- package/plugins/blog/plugin.js +133 -0
- package/plugins/blog/plugin.json +3 -3
- package/plugins/blog/templates/post.html +2 -1
- package/plugins/invoice/admin/templates/editor.html +129 -0
- package/plugins/invoice/admin/templates/index.html +43 -0
- package/plugins/invoice/admin/templates/issuers.html +5 -0
- package/plugins/invoice/admin/templates/receivers.html +5 -0
- package/plugins/invoice/admin/views/editor.js +267 -0
- package/plugins/invoice/admin/views/index.js +155 -0
- package/plugins/invoice/admin/views/issuers.js +23 -0
- package/plugins/invoice/admin/views/party-view.js +148 -0
- package/plugins/invoice/admin/views/receivers.js +22 -0
- package/plugins/invoice/collections/invoice-issuers/schema.json +16 -0
- package/plugins/invoice/collections/invoice-receivers/schema.json +15 -0
- package/plugins/invoice/collections/invoices/schema.json +27 -0
- package/plugins/invoice/config.js +16 -0
- package/plugins/invoice/plugin.js +283 -0
- package/plugins/invoice/plugin.json +85 -0
- package/plugins/invoice/templates/invoice-print.html +213 -0
- package/server/services/markdown.js +24 -4
- package/server/services/renderer.js +9 -3
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
<div class="view-header d-flex align-items-center justify-content-between mb-4">
|
|
2
|
-
<h1 class="h3 mb-0">Blog Posts</h1>
|
|
2
|
+
<h1 class="h3 mb-0"><span data-icon="file-text"></span> Blog Posts</h1>
|
|
3
3
|
<button id="btn-new-post" class="btn btn-primary">
|
|
4
4
|
<span data-icon="plus"></span> New Post
|
|
5
5
|
</button>
|
|
6
6
|
</div>
|
|
7
7
|
|
|
8
|
+
<div class="grid grid-cols-4 gap-3 mb-4">
|
|
9
|
+
<div class="card"><div class="card-body">
|
|
10
|
+
<div class="text-muted small">Total</div>
|
|
11
|
+
<div class="h3 mb-0" id="kpi-total">—</div>
|
|
12
|
+
</div></div>
|
|
13
|
+
<div class="card"><div class="card-body">
|
|
14
|
+
<div class="text-muted small">Published</div>
|
|
15
|
+
<div class="h3 mb-0" id="kpi-published">—</div>
|
|
16
|
+
</div></div>
|
|
17
|
+
<div class="card"><div class="card-body">
|
|
18
|
+
<div class="text-muted small">Drafts</div>
|
|
19
|
+
<div class="h3 mb-0" id="kpi-drafts">—</div>
|
|
20
|
+
</div></div>
|
|
21
|
+
<div class="card"><div class="card-body">
|
|
22
|
+
<div class="text-muted small">Scheduled</div>
|
|
23
|
+
<div class="h3 mb-0" id="kpi-scheduled">—</div>
|
|
24
|
+
</div></div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
8
27
|
<div class="card mb-3">
|
|
9
28
|
<div class="card-body">
|
|
10
29
|
<div class="d-flex gap-2 flex-wrap">
|
|
@@ -14,7 +33,11 @@
|
|
|
14
33
|
<option value="scheduled">Scheduled</option>
|
|
15
34
|
<option value="published">Published</option>
|
|
16
35
|
</select>
|
|
17
|
-
<
|
|
36
|
+
<select id="filter-category" class="form-select" style="width:auto;">
|
|
37
|
+
<option value="">All categories</option>
|
|
38
|
+
</select>
|
|
39
|
+
<input id="filter-search" type="text" class="form-control" style="width:240px;" placeholder="Search title or excerpt…">
|
|
40
|
+
<button id="filter-reset" class="btn btn-ghost btn-sm">Reset</button>
|
|
18
41
|
</div>
|
|
19
42
|
</div>
|
|
20
43
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Blog posts
|
|
2
|
+
* Blog posts overview — KPI cards + filterable, sortable post table.
|
|
3
3
|
*
|
|
4
4
|
* @module blog/admin/views/blog
|
|
5
5
|
*/
|
|
@@ -19,29 +19,23 @@ const STATUS_BADGE = {
|
|
|
19
19
|
export const blogView = {
|
|
20
20
|
templateUrl: '/plugins/blog/admin/templates/blog.html',
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
* Mount the blog posts list view.
|
|
24
|
-
*
|
|
25
|
-
* @param {object} $container - Domma-wrapped container element
|
|
26
|
-
* @returns {Promise<void>}
|
|
27
|
-
*/
|
|
28
22
|
async onMount($container) {
|
|
29
23
|
let allPosts = [];
|
|
24
|
+
let categories = [];
|
|
30
25
|
let postsTable = null;
|
|
31
26
|
|
|
32
|
-
// ---------------------------------------------------------------
|
|
33
|
-
// Table
|
|
34
|
-
// ---------------------------------------------------------------
|
|
35
|
-
|
|
36
27
|
function buildTable(data) {
|
|
37
28
|
const tableEl = $container.find('#posts-table').get(0);
|
|
38
29
|
if (!tableEl) return;
|
|
39
30
|
|
|
40
31
|
if (postsTable) {
|
|
41
32
|
postsTable.setData(data);
|
|
33
|
+
Domma.icons.scan(tableEl);
|
|
42
34
|
return;
|
|
43
35
|
}
|
|
44
36
|
|
|
37
|
+
const catNameById = (id) => categories.find((c) => c.id === id)?.name ?? '';
|
|
38
|
+
|
|
45
39
|
postsTable = T.create(tableEl, {
|
|
46
40
|
data,
|
|
47
41
|
columns: [
|
|
@@ -62,10 +56,12 @@ export const blogView = {
|
|
|
62
56
|
}
|
|
63
57
|
},
|
|
64
58
|
{
|
|
65
|
-
key: '
|
|
66
|
-
title: '
|
|
59
|
+
key: 'categories',
|
|
60
|
+
title: 'Categories',
|
|
67
61
|
sortable: false,
|
|
68
|
-
render: (
|
|
62
|
+
render: (val) => Array.isArray(val) && val.length
|
|
63
|
+
? val.map((id) => `<span class="badge badge-secondary mr-1">${escapeHtml(catNameById(id) || id)}</span>`).join('')
|
|
64
|
+
: '<span style="opacity:0.4;">—</span>'
|
|
69
65
|
},
|
|
70
66
|
{
|
|
71
67
|
key: 'publishedAt',
|
|
@@ -79,19 +75,22 @@ export const blogView = {
|
|
|
79
75
|
key: 'id',
|
|
80
76
|
title: 'Actions',
|
|
81
77
|
sortable: false,
|
|
82
|
-
render: (id) =>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
`<
|
|
88
|
-
|
|
78
|
+
render: (id, row) => {
|
|
79
|
+
const slug = escapeHtml(row.slug || '');
|
|
80
|
+
const viewLink = row.status === 'published' && slug
|
|
81
|
+
? `<a href="/blog/${slug}" target="_blank" class="btn btn-sm btn-outline" title="View on site"><span data-icon="external-link" data-icon-size="14"></span></a>`
|
|
82
|
+
: '';
|
|
83
|
+
return `<div style="display:flex;gap:.4rem;">` +
|
|
84
|
+
`<button class="btn btn-sm btn-outline post-edit-btn" data-id="${escapeHtml(id)}"><span data-icon="edit" data-icon-size="14"></span></button>` +
|
|
85
|
+
viewLink +
|
|
86
|
+
`<button class="btn btn-sm btn-danger post-delete-btn" data-id="${escapeHtml(id)}"><span data-icon="trash" data-icon-size="14"></span></button>` +
|
|
87
|
+
`</div>`;
|
|
88
|
+
}
|
|
89
89
|
}
|
|
90
90
|
],
|
|
91
91
|
emptyMessage: 'No posts found.'
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
-
// Event delegation for action buttons
|
|
95
94
|
tableEl.addEventListener('click', async (e) => {
|
|
96
95
|
const editBtn = e.target.closest('.post-edit-btn');
|
|
97
96
|
const deleteBtn = e.target.closest('.post-delete-btn');
|
|
@@ -115,21 +114,30 @@ export const blogView = {
|
|
|
115
114
|
}
|
|
116
115
|
});
|
|
117
116
|
|
|
118
|
-
Domma.icons.scan();
|
|
117
|
+
Domma.icons.scan(tableEl);
|
|
119
118
|
}
|
|
120
119
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
function refreshKpis() {
|
|
121
|
+
const counts = { total: allPosts.length, published: 0, draft: 0, scheduled: 0 };
|
|
122
|
+
for (const p of allPosts) {
|
|
123
|
+
if (p.status === 'published') counts.published++;
|
|
124
|
+
else if (p.status === 'scheduled') counts.scheduled++;
|
|
125
|
+
else counts.draft++;
|
|
126
|
+
}
|
|
127
|
+
$container.find('#kpi-total').text(counts.total);
|
|
128
|
+
$container.find('#kpi-published').text(counts.published);
|
|
129
|
+
$container.find('#kpi-drafts').text(counts.draft);
|
|
130
|
+
$container.find('#kpi-scheduled').text(counts.scheduled);
|
|
131
|
+
}
|
|
124
132
|
|
|
125
133
|
function applyFilters() {
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
const query = (searchEl?.value ?? '').toLowerCase().trim();
|
|
134
|
+
const status = $container.find('#filter-status').val() || '';
|
|
135
|
+
const cat = $container.find('#filter-category').val() || '';
|
|
136
|
+
const query = ($container.find('#filter-search').val() || '').toString().toLowerCase().trim();
|
|
130
137
|
|
|
131
138
|
let filtered = allPosts;
|
|
132
139
|
if (status) filtered = filtered.filter((p) => p.status === status);
|
|
140
|
+
if (cat) filtered = filtered.filter((p) => Array.isArray(p.categories) && p.categories.includes(cat));
|
|
133
141
|
if (query) {
|
|
134
142
|
filtered = filtered.filter((p) =>
|
|
135
143
|
(p.title ?? '').toLowerCase().includes(query) ||
|
|
@@ -139,44 +147,52 @@ export const blogView = {
|
|
|
139
147
|
buildTable(filtered);
|
|
140
148
|
}
|
|
141
149
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
150
|
+
function populateCategoryFilter() {
|
|
151
|
+
const select = $container.find('#filter-category').get(0);
|
|
152
|
+
if (!select) return;
|
|
153
|
+
for (const cat of categories) {
|
|
154
|
+
const opt = document.createElement('option');
|
|
155
|
+
opt.value = cat.id;
|
|
156
|
+
opt.textContent = cat.name ?? cat.slug ?? cat.id;
|
|
157
|
+
select.appendChild(opt);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
145
160
|
|
|
146
161
|
async function reload() {
|
|
147
162
|
try {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
163
|
+
const [postsRes, catsRes] = await Promise.all([
|
|
164
|
+
H.get(`${BASE}/posts`),
|
|
165
|
+
H.get(`${BASE}/categories`).catch(() => ({ data: [] }))
|
|
166
|
+
]);
|
|
167
|
+
if (postsRes.error) throw new Error(postsRes.error);
|
|
168
|
+
allPosts = postsRes.data ?? [];
|
|
169
|
+
categories = catsRes.data ?? catsRes ?? [];
|
|
151
170
|
} catch {
|
|
152
171
|
allPosts = [];
|
|
172
|
+
categories = [];
|
|
153
173
|
E.toast('Failed to load posts.', { type: 'error' });
|
|
154
174
|
}
|
|
175
|
+
refreshKpis();
|
|
176
|
+
populateCategoryFilter();
|
|
155
177
|
applyFilters();
|
|
156
178
|
}
|
|
157
179
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
180
|
+
$container.find('#btn-new-post').on('click', () => R.navigate('#/plugins/blog/posts/new'));
|
|
181
|
+
$container.find('#filter-status').on('change', applyFilters);
|
|
182
|
+
$container.find('#filter-category').on('change', applyFilters);
|
|
183
|
+
$container.find('#filter-reset').on('click', () => {
|
|
184
|
+
$container.find('#filter-status').val('');
|
|
185
|
+
$container.find('#filter-category').val('');
|
|
186
|
+
$container.find('#filter-search').val('');
|
|
187
|
+
applyFilters();
|
|
188
|
+
});
|
|
167
189
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
searchTimer = setTimeout(applyFilters, 250);
|
|
174
|
-
});
|
|
175
|
-
}
|
|
190
|
+
let searchTimer = null;
|
|
191
|
+
$container.find('#filter-search').on('input', () => {
|
|
192
|
+
clearTimeout(searchTimer);
|
|
193
|
+
searchTimer = setTimeout(applyFilters, 250);
|
|
194
|
+
});
|
|
176
195
|
|
|
177
|
-
// ---------------------------------------------------------------
|
|
178
|
-
// Initial load
|
|
179
|
-
// ---------------------------------------------------------------
|
|
180
196
|
await reload();
|
|
181
197
|
Domma.icons.scan();
|
|
182
198
|
}
|
|
@@ -17,29 +17,17 @@ const BASE = '/api/plugins/blog';
|
|
|
17
17
|
export const postEditorView = {
|
|
18
18
|
templateUrl: '/plugins/blog/admin/templates/post-editor.html',
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
* Mount the post editor view.
|
|
22
|
-
*
|
|
23
|
-
* @param {object} $container - Domma-wrapped container element
|
|
24
|
-
* @returns {Promise<void>}
|
|
25
|
-
*/
|
|
26
20
|
async onMount($container) {
|
|
27
|
-
// Resolve post id from router params
|
|
28
21
|
const id = R.current?.params?.id !== 'new' ? R.current?.params?.id : null;
|
|
29
22
|
const isEdit = Boolean(id);
|
|
30
23
|
|
|
31
24
|
let post = null;
|
|
32
25
|
let categories = [];
|
|
33
26
|
let selectedCategoryIds = [];
|
|
34
|
-
|
|
35
|
-
// ---------------------------------------------------------------
|
|
36
|
-
// Load data
|
|
37
|
-
// ---------------------------------------------------------------
|
|
27
|
+
let featuredImageUrl = '';
|
|
38
28
|
|
|
39
29
|
try {
|
|
40
|
-
const
|
|
41
|
-
H.get(`${BASE}/categories`).catch(() => ({ data: [] }))
|
|
42
|
-
]);
|
|
30
|
+
const catsRes = await H.get(`${BASE}/categories`).catch(() => ({ data: [] }));
|
|
43
31
|
categories = catsRes.data ?? catsRes ?? [];
|
|
44
32
|
} catch {
|
|
45
33
|
categories = [];
|
|
@@ -57,10 +45,6 @@ export const postEditorView = {
|
|
|
57
45
|
}
|
|
58
46
|
}
|
|
59
47
|
|
|
60
|
-
// ---------------------------------------------------------------
|
|
61
|
-
// DOM refs
|
|
62
|
-
// ---------------------------------------------------------------
|
|
63
|
-
|
|
64
48
|
const titleEl = $container.find('#editor-title').get(0);
|
|
65
49
|
const postTitleEl = $container.find('#post-title').get(0);
|
|
66
50
|
const postSlugEl = $container.find('#post-slug').get(0);
|
|
@@ -75,10 +59,8 @@ export const postEditorView = {
|
|
|
75
59
|
const scheduleEl = $container.find('#schedule-picker').get(0);
|
|
76
60
|
const schedAtEl = $container.find('#post-scheduled-at').get(0);
|
|
77
61
|
const catsListEl = $container.find('#categories-checklist').get(0);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// Populate
|
|
81
|
-
// ---------------------------------------------------------------
|
|
62
|
+
const imagePreviewEl = $container.find('#featured-image-preview').get(0);
|
|
63
|
+
const clearImageBtn = $container.find('#btn-clear-image').get(0);
|
|
82
64
|
|
|
83
65
|
if (titleEl) titleEl.textContent = isEdit ? 'Edit Post' : 'New Post';
|
|
84
66
|
|
|
@@ -88,19 +70,33 @@ export const postEditorView = {
|
|
|
88
70
|
if (excerptEl) excerptEl.value = post.excerpt ?? '';
|
|
89
71
|
if (contentEl) contentEl.value = post.content ?? '';
|
|
90
72
|
if (tagsEl) tagsEl.value = Array.isArray(post.tags) ? post.tags.join(', ') : (post.tags ?? '');
|
|
91
|
-
if (seoTitleEl) seoTitleEl.value = post.
|
|
92
|
-
if (seoDescEl) seoDescEl.value = post.
|
|
73
|
+
if (seoTitleEl) seoTitleEl.value = post.seo?.title ?? '';
|
|
74
|
+
if (seoDescEl) seoDescEl.value = post.seo?.description ?? '';
|
|
93
75
|
if (statusBadge) statusBadge.textContent = post.status ?? 'draft';
|
|
94
|
-
selectedCategoryIds = Array.isArray(post.
|
|
76
|
+
selectedCategoryIds = Array.isArray(post.categories) ? [...post.categories] : [];
|
|
77
|
+
featuredImageUrl = post.featuredImage ?? '';
|
|
78
|
+
renderImagePreview();
|
|
95
79
|
} else {
|
|
96
|
-
// New post: enable auto-slug by default
|
|
97
80
|
if (slugAutoEl) slugAutoEl.checked = true;
|
|
98
81
|
}
|
|
99
82
|
|
|
100
|
-
|
|
83
|
+
function renderImagePreview() {
|
|
84
|
+
if (!imagePreviewEl) return;
|
|
85
|
+
while (imagePreviewEl.firstChild) imagePreviewEl.removeChild(imagePreviewEl.firstChild);
|
|
86
|
+
if (featuredImageUrl) {
|
|
87
|
+
const img = document.createElement('img');
|
|
88
|
+
img.src = featuredImageUrl;
|
|
89
|
+
img.alt = 'Featured image';
|
|
90
|
+
img.style.cssText = 'max-width:100%;border-radius:.25rem;display:block;margin-bottom:.5rem;';
|
|
91
|
+
imagePreviewEl.appendChild(img);
|
|
92
|
+
if (clearImageBtn) clearImageBtn.style.display = '';
|
|
93
|
+
} else {
|
|
94
|
+
if (clearImageBtn) clearImageBtn.style.display = 'none';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
101
98
|
if (catsListEl) {
|
|
102
99
|
while (catsListEl.firstChild) catsListEl.removeChild(catsListEl.firstChild);
|
|
103
|
-
|
|
104
100
|
if (categories.length === 0) {
|
|
105
101
|
const p = document.createElement('p');
|
|
106
102
|
p.className = 'text-muted small';
|
|
@@ -134,27 +130,22 @@ export const postEditorView = {
|
|
|
134
130
|
}
|
|
135
131
|
}
|
|
136
132
|
|
|
137
|
-
// ---------------------------------------------------------------
|
|
138
|
-
// Collect form values
|
|
139
|
-
// ---------------------------------------------------------------
|
|
140
|
-
|
|
141
133
|
function collectValues() {
|
|
142
134
|
return {
|
|
143
|
-
title:
|
|
144
|
-
slug:
|
|
145
|
-
excerpt:
|
|
146
|
-
content:
|
|
147
|
-
tags:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
135
|
+
title: postTitleEl?.value?.trim() ?? '',
|
|
136
|
+
slug: postSlugEl?.value?.trim() ?? '',
|
|
137
|
+
excerpt: excerptEl?.value?.trim() ?? '',
|
|
138
|
+
content: contentEl?.value ?? '',
|
|
139
|
+
tags: (tagsEl?.value ?? '').split(',').map((t) => t.trim()).filter(Boolean),
|
|
140
|
+
categories: [...selectedCategoryIds],
|
|
141
|
+
featuredImage: featuredImageUrl || null,
|
|
142
|
+
seo: {
|
|
143
|
+
title: seoTitleEl?.value?.trim() ?? '',
|
|
144
|
+
description: seoDescEl?.value?.trim() ?? ''
|
|
145
|
+
}
|
|
151
146
|
};
|
|
152
147
|
}
|
|
153
148
|
|
|
154
|
-
// ---------------------------------------------------------------
|
|
155
|
-
// Save helpers
|
|
156
|
-
// ---------------------------------------------------------------
|
|
157
|
-
|
|
158
149
|
async function doSave(extra = {}) {
|
|
159
150
|
const body = { ...collectValues(), ...extra };
|
|
160
151
|
if (!body.title) {
|
|
@@ -162,37 +153,25 @@ export const postEditorView = {
|
|
|
162
153
|
return null;
|
|
163
154
|
}
|
|
164
155
|
try {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
} else {
|
|
171
|
-
const res = await H.post(`${BASE}/posts`, body);
|
|
172
|
-
if (res.error) throw new Error(res.error);
|
|
173
|
-
saved = res.data ?? res;
|
|
174
|
-
}
|
|
175
|
-
return saved;
|
|
156
|
+
const res = isEdit
|
|
157
|
+
? await H.put(`${BASE}/posts/${id}`, body)
|
|
158
|
+
: await H.post(`${BASE}/posts`, body);
|
|
159
|
+
if (res.error) throw new Error(res.error);
|
|
160
|
+
return res.data ?? res;
|
|
176
161
|
} catch (err) {
|
|
177
162
|
E.toast(err?.message ?? 'Failed to save post.', { type: 'error' });
|
|
178
163
|
return null;
|
|
179
164
|
}
|
|
180
165
|
}
|
|
181
166
|
|
|
182
|
-
// ---------------------------------------------------------------
|
|
183
|
-
// Event bindings
|
|
184
|
-
// ---------------------------------------------------------------
|
|
185
|
-
|
|
186
|
-
// Auto-slug from title
|
|
187
167
|
if (postTitleEl) {
|
|
188
168
|
postTitleEl.addEventListener('input', () => {
|
|
189
|
-
if (slugAutoEl?.checked) {
|
|
190
|
-
|
|
169
|
+
if (slugAutoEl?.checked && postSlugEl) {
|
|
170
|
+
postSlugEl.value = slugify(postTitleEl.value);
|
|
191
171
|
}
|
|
192
172
|
});
|
|
193
173
|
}
|
|
194
174
|
|
|
195
|
-
// Slug-auto checkbox
|
|
196
175
|
if (slugAutoEl) {
|
|
197
176
|
slugAutoEl.addEventListener('change', () => {
|
|
198
177
|
if (!slugAutoEl.checked && isEdit && post?.status === 'published') {
|
|
@@ -200,13 +179,12 @@ export const postEditorView = {
|
|
|
200
179
|
} else {
|
|
201
180
|
if (slugWarnEl) slugWarnEl.style.display = 'none';
|
|
202
181
|
}
|
|
203
|
-
if (slugAutoEl.checked && postTitleEl) {
|
|
204
|
-
|
|
182
|
+
if (slugAutoEl.checked && postTitleEl && postSlugEl) {
|
|
183
|
+
postSlugEl.value = slugify(postTitleEl.value);
|
|
205
184
|
}
|
|
206
185
|
});
|
|
207
186
|
}
|
|
208
187
|
|
|
209
|
-
// SEO panel toggle
|
|
210
188
|
const seoToggle = $container.find('#seo-toggle').get(0);
|
|
211
189
|
const seoPanel = $container.find('#seo-panel').get(0);
|
|
212
190
|
if (seoToggle && seoPanel) {
|
|
@@ -215,19 +193,17 @@ export const postEditorView = {
|
|
|
215
193
|
});
|
|
216
194
|
}
|
|
217
195
|
|
|
218
|
-
// Save draft
|
|
219
196
|
const saveDraftBtn = $container.find('#btn-save-draft').get(0);
|
|
220
197
|
if (saveDraftBtn) {
|
|
221
198
|
saveDraftBtn.addEventListener('click', async () => {
|
|
222
199
|
const saved = await doSave({ status: 'draft' });
|
|
223
200
|
if (saved) {
|
|
224
201
|
E.toast('Draft saved.', { type: 'success' });
|
|
225
|
-
R.navigate('#/plugins/blog');
|
|
202
|
+
R.navigate('#/plugins/blog/posts');
|
|
226
203
|
}
|
|
227
204
|
});
|
|
228
205
|
}
|
|
229
206
|
|
|
230
|
-
// Publish now
|
|
231
207
|
const publishBtn = $container.find('#btn-publish').get(0);
|
|
232
208
|
if (publishBtn) {
|
|
233
209
|
publishBtn.addEventListener('click', async () => {
|
|
@@ -238,22 +214,18 @@ export const postEditorView = {
|
|
|
238
214
|
const res = await H.post(`${BASE}/posts/${savedId}/publish`);
|
|
239
215
|
if (res.error) throw new Error(res.error);
|
|
240
216
|
E.toast('Post published.', { type: 'success' });
|
|
241
|
-
R.navigate('#/plugins/blog');
|
|
217
|
+
R.navigate('#/plugins/blog/posts');
|
|
242
218
|
} catch (err) {
|
|
243
219
|
E.toast(err?.message ?? 'Failed to publish post.', { type: 'error' });
|
|
244
220
|
}
|
|
245
221
|
});
|
|
246
222
|
}
|
|
247
223
|
|
|
248
|
-
// Schedule
|
|
249
224
|
const scheduleBtn = $container.find('#btn-schedule').get(0);
|
|
250
225
|
if (scheduleBtn) {
|
|
251
226
|
scheduleBtn.style.display = '';
|
|
252
227
|
scheduleBtn.addEventListener('click', async () => {
|
|
253
|
-
// Toggle schedule picker visibility
|
|
254
228
|
if (scheduleEl) scheduleEl.style.display = scheduleEl.style.display === 'none' ? '' : 'none';
|
|
255
|
-
|
|
256
|
-
// If picker is now visible, wait for user to confirm
|
|
257
229
|
if (scheduleEl && scheduleEl.style.display !== 'none') return;
|
|
258
230
|
|
|
259
231
|
const publishedAt = schedAtEl?.value;
|
|
@@ -270,22 +242,69 @@ export const postEditorView = {
|
|
|
270
242
|
const res = await H.post(`${BASE}/posts/${savedId}/schedule`, { publishedAt });
|
|
271
243
|
if (res.error) throw new Error(res.error);
|
|
272
244
|
E.toast('Post scheduled.', { type: 'success' });
|
|
273
|
-
R.navigate('#/plugins/blog');
|
|
245
|
+
R.navigate('#/plugins/blog/posts');
|
|
274
246
|
} catch (err) {
|
|
275
247
|
E.toast(err?.message ?? 'Failed to schedule post.', { type: 'error' });
|
|
276
248
|
}
|
|
277
249
|
});
|
|
278
250
|
}
|
|
279
251
|
|
|
280
|
-
// Pick image (TBD)
|
|
281
252
|
const pickImageBtn = $container.find('#btn-pick-image').get(0);
|
|
282
253
|
if (pickImageBtn) {
|
|
283
|
-
pickImageBtn.addEventListener('click', () =>
|
|
284
|
-
|
|
285
|
-
|
|
254
|
+
pickImageBtn.addEventListener('click', () => openMediaPicker());
|
|
255
|
+
}
|
|
256
|
+
if (clearImageBtn) {
|
|
257
|
+
clearImageBtn.addEventListener('click', () => {
|
|
258
|
+
featuredImageUrl = '';
|
|
259
|
+
renderImagePreview();
|
|
286
260
|
});
|
|
287
261
|
}
|
|
288
262
|
|
|
263
|
+
async function openMediaPicker() {
|
|
264
|
+
let media = [];
|
|
265
|
+
try {
|
|
266
|
+
const res = await H.get('/api/media');
|
|
267
|
+
media = (res.data ?? res ?? []).filter((m) => /\.(png|jpe?g|gif|webp|svg)$/i.test(m.name || m.url || ''));
|
|
268
|
+
} catch {
|
|
269
|
+
E.toast('Could not load media library.', { type: 'error' });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const grid = document.createElement('div');
|
|
274
|
+
grid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fill, minmax(120px, 1fr));gap:.75rem;padding:.5rem;';
|
|
275
|
+
|
|
276
|
+
if (!media.length) {
|
|
277
|
+
const p = document.createElement('p');
|
|
278
|
+
p.className = 'text-muted p-3';
|
|
279
|
+
p.textContent = 'No images uploaded yet. Use the Media admin to upload one first.';
|
|
280
|
+
grid.appendChild(p);
|
|
281
|
+
} else {
|
|
282
|
+
media.forEach((m) => {
|
|
283
|
+
const tile = document.createElement('button');
|
|
284
|
+
tile.type = 'button';
|
|
285
|
+
tile.style.cssText = 'border:1px solid var(--dm-border,#333);background:transparent;border-radius:.25rem;padding:.25rem;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:.25rem;';
|
|
286
|
+
const img = document.createElement('img');
|
|
287
|
+
img.src = m.url;
|
|
288
|
+
img.alt = m.name;
|
|
289
|
+
img.style.cssText = 'width:100%;height:90px;object-fit:cover;border-radius:.15rem;';
|
|
290
|
+
const cap = document.createElement('span');
|
|
291
|
+
cap.textContent = m.name;
|
|
292
|
+
cap.style.cssText = 'font-size:.7rem;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;width:100%;';
|
|
293
|
+
tile.appendChild(img);
|
|
294
|
+
tile.appendChild(cap);
|
|
295
|
+
tile.addEventListener('click', () => {
|
|
296
|
+
featuredImageUrl = m.url;
|
|
297
|
+
renderImagePreview();
|
|
298
|
+
slideover.close();
|
|
299
|
+
});
|
|
300
|
+
grid.appendChild(tile);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const slideover = E.slideover({ title: 'Pick Featured Image', size: 'lg', position: 'right', content: grid });
|
|
305
|
+
slideover.open();
|
|
306
|
+
}
|
|
307
|
+
|
|
289
308
|
Domma.icons.scan();
|
|
290
309
|
}
|
|
291
310
|
};
|