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
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blog categories admin view.
|
|
3
|
+
*
|
|
4
|
+
* @module blog/admin/views/categories
|
|
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
|
+
export const categoriesView = {
|
|
14
|
+
templateUrl: '/plugins/blog/admin/templates/categories.html',
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Mount the blog categories view.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} $container - Domma-wrapped container element
|
|
20
|
+
* @returns {Promise<void>}
|
|
21
|
+
*/
|
|
22
|
+
async onMount($container) {
|
|
23
|
+
let categories = [];
|
|
24
|
+
let editingId = null;
|
|
25
|
+
let modal = null;
|
|
26
|
+
let nameInput = null;
|
|
27
|
+
let slugInput = null;
|
|
28
|
+
let descInput = null;
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------
|
|
31
|
+
// Modal setup (built with DOM APIs — not innerHTML)
|
|
32
|
+
// ---------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
function buildModal() {
|
|
35
|
+
const form = document.createElement('div');
|
|
36
|
+
form.style.display = 'flex';
|
|
37
|
+
form.style.flexDirection = 'column';
|
|
38
|
+
form.style.gap = '1rem';
|
|
39
|
+
|
|
40
|
+
const mkField = (labelText, id, tag = 'input', rows) => {
|
|
41
|
+
const group = document.createElement('div');
|
|
42
|
+
|
|
43
|
+
const lbl = document.createElement('label');
|
|
44
|
+
lbl.className = 'form-label';
|
|
45
|
+
lbl.textContent = labelText;
|
|
46
|
+
lbl.setAttribute('for', id);
|
|
47
|
+
group.appendChild(lbl);
|
|
48
|
+
|
|
49
|
+
const el = document.createElement(tag);
|
|
50
|
+
el.id = id;
|
|
51
|
+
el.className = 'form-control';
|
|
52
|
+
if (tag === 'textarea' && rows) el.rows = rows;
|
|
53
|
+
group.appendChild(el);
|
|
54
|
+
|
|
55
|
+
return { group, el };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const { group: nameGroup, el: nameEl } = mkField('Name', 'cat-name-input');
|
|
59
|
+
const { group: slugGroup, el: slugEl } = mkField('Slug', 'cat-slug-input');
|
|
60
|
+
const { group: descGroup, el: descEl } = mkField('Description', 'cat-desc-input', 'textarea', 3);
|
|
61
|
+
|
|
62
|
+
form.appendChild(nameGroup);
|
|
63
|
+
form.appendChild(slugGroup);
|
|
64
|
+
form.appendChild(descGroup);
|
|
65
|
+
|
|
66
|
+
// Footer buttons
|
|
67
|
+
const footer = document.createElement('div');
|
|
68
|
+
footer.style.display = 'flex';
|
|
69
|
+
footer.style.justifyContent = 'flex-end';
|
|
70
|
+
footer.style.gap = '0.5rem';
|
|
71
|
+
footer.style.marginTop = '0.5rem';
|
|
72
|
+
|
|
73
|
+
const cancelBtn = document.createElement('button');
|
|
74
|
+
cancelBtn.className = 'btn btn-secondary';
|
|
75
|
+
cancelBtn.textContent = 'Cancel';
|
|
76
|
+
|
|
77
|
+
const saveBtn = document.createElement('button');
|
|
78
|
+
saveBtn.className = 'btn btn-primary';
|
|
79
|
+
saveBtn.textContent = 'Save';
|
|
80
|
+
|
|
81
|
+
footer.appendChild(cancelBtn);
|
|
82
|
+
footer.appendChild(saveBtn);
|
|
83
|
+
form.appendChild(footer);
|
|
84
|
+
|
|
85
|
+
modal = E.modal({ title: 'Category' });
|
|
86
|
+
modal.element.appendChild(form);
|
|
87
|
+
|
|
88
|
+
nameInput = nameEl;
|
|
89
|
+
slugInput = slugEl;
|
|
90
|
+
descInput = descEl;
|
|
91
|
+
|
|
92
|
+
// Auto-slug from name
|
|
93
|
+
nameEl.addEventListener('input', () => {
|
|
94
|
+
if (!editingId) {
|
|
95
|
+
slugEl.value = nameEl.value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
cancelBtn.addEventListener('click', () => modal.close());
|
|
100
|
+
|
|
101
|
+
saveBtn.addEventListener('click', async () => {
|
|
102
|
+
const name = nameEl.value.trim();
|
|
103
|
+
const slug = slugEl.value.trim();
|
|
104
|
+
if (!name) {
|
|
105
|
+
E.toast('Name is required.', { type: 'error' });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const body = { name, slug, description: descInput.value.trim() };
|
|
109
|
+
try {
|
|
110
|
+
if (editingId) {
|
|
111
|
+
const res = await H.put(`${BASE}/categories/${editingId}`, body);
|
|
112
|
+
if (res.error) throw new Error(res.error);
|
|
113
|
+
E.toast('Category updated.', { type: 'success' });
|
|
114
|
+
} else {
|
|
115
|
+
const res = await H.post(`${BASE}/categories`, body);
|
|
116
|
+
if (res.error) throw new Error(res.error);
|
|
117
|
+
E.toast('Category created.', { type: 'success' });
|
|
118
|
+
}
|
|
119
|
+
modal.close();
|
|
120
|
+
await reload();
|
|
121
|
+
} catch (err) {
|
|
122
|
+
E.toast(err?.message ?? 'Failed to save category.', { type: 'error' });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function openModal(cat = null) {
|
|
128
|
+
editingId = cat?.id ?? null;
|
|
129
|
+
if (nameInput) nameInput.value = cat?.name ?? '';
|
|
130
|
+
if (slugInput) slugInput.value = cat?.slug ?? '';
|
|
131
|
+
if (descInput) descInput.value = cat?.description ?? '';
|
|
132
|
+
modal?.open();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------
|
|
136
|
+
// Table
|
|
137
|
+
// ---------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
function buildTable() {
|
|
140
|
+
const tableEl = $container.find('#categories-table').get(0);
|
|
141
|
+
if (!tableEl) return;
|
|
142
|
+
|
|
143
|
+
T.create(tableEl, {
|
|
144
|
+
data: categories,
|
|
145
|
+
columns: [
|
|
146
|
+
{
|
|
147
|
+
key: 'name',
|
|
148
|
+
title: 'Name',
|
|
149
|
+
sortable: true,
|
|
150
|
+
render: (v) => `<strong>${escapeHtml(v)}</strong>`
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
key: 'slug',
|
|
154
|
+
title: 'Slug',
|
|
155
|
+
sortable: true,
|
|
156
|
+
render: (v) => `<code>${escapeHtml(v)}</code>`
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
key: 'description',
|
|
160
|
+
title: 'Description',
|
|
161
|
+
sortable: false,
|
|
162
|
+
render: (v) => escapeHtml(v) || '<span style="opacity:0.4;">—</span>'
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
key: 'id',
|
|
166
|
+
title: 'Actions',
|
|
167
|
+
sortable: false,
|
|
168
|
+
render: (id) =>
|
|
169
|
+
`<div style="display:flex;gap:.4rem;">` +
|
|
170
|
+
`<button class="btn btn-sm btn-outline cat-edit-btn" data-id="${escapeHtml(id)}">` +
|
|
171
|
+
`<span data-icon="edit" data-icon-size="14"></span> Edit</button>` +
|
|
172
|
+
`<button class="btn btn-sm btn-danger cat-delete-btn" data-id="${escapeHtml(id)}">` +
|
|
173
|
+
`<span data-icon="trash" data-icon-size="14"></span> Delete</button>` +
|
|
174
|
+
`</div>`
|
|
175
|
+
}
|
|
176
|
+
],
|
|
177
|
+
emptyMessage: 'No categories yet.'
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
tableEl.addEventListener('click', async (e) => {
|
|
181
|
+
const editBtn = e.target.closest('.cat-edit-btn');
|
|
182
|
+
const deleteBtn = e.target.closest('.cat-delete-btn');
|
|
183
|
+
|
|
184
|
+
if (editBtn) {
|
|
185
|
+
const cat = categories.find((c) => c.id === editBtn.dataset.id);
|
|
186
|
+
if (cat) openModal(cat);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (deleteBtn) {
|
|
190
|
+
const id = deleteBtn.dataset.id;
|
|
191
|
+
const cat = categories.find((c) => c.id === id);
|
|
192
|
+
const ok = await E.confirm(`Delete category "${cat?.name ?? id}"?`);
|
|
193
|
+
if (!ok) return;
|
|
194
|
+
try {
|
|
195
|
+
await H.delete(`${BASE}/categories/${id}`);
|
|
196
|
+
E.toast('Category deleted.', { type: 'success' });
|
|
197
|
+
await reload();
|
|
198
|
+
} catch {
|
|
199
|
+
E.toast('Failed to delete category.', { type: 'error' });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
Domma.icons.scan();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------
|
|
208
|
+
// Load
|
|
209
|
+
// ---------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
async function reload() {
|
|
212
|
+
try {
|
|
213
|
+
const res = await H.get(`${BASE}/categories`);
|
|
214
|
+
if (res.error) throw new Error(res.error);
|
|
215
|
+
categories = res.data ?? (Array.isArray(res) ? res : []);
|
|
216
|
+
} catch {
|
|
217
|
+
categories = [];
|
|
218
|
+
E.toast('Failed to load categories.', { type: 'error' });
|
|
219
|
+
}
|
|
220
|
+
buildTable();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------
|
|
224
|
+
// Wire up
|
|
225
|
+
// ---------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
buildModal();
|
|
228
|
+
|
|
229
|
+
const newBtn = $container.find('#btn-new-category').get(0);
|
|
230
|
+
if (newBtn) newBtn.addEventListener('click', () => openModal(null));
|
|
231
|
+
|
|
232
|
+
await reload();
|
|
233
|
+
Domma.icons.scan();
|
|
234
|
+
}
|
|
235
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blog comments admin view.
|
|
3
|
+
*
|
|
4
|
+
* @module blog/admin/views/comments
|
|
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
|
+
export const commentsView = {
|
|
14
|
+
templateUrl: '/plugins/blog/admin/templates/comments.html',
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Mount the blog comments view.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} $container - Domma-wrapped container element
|
|
20
|
+
* @returns {Promise<void>}
|
|
21
|
+
*/
|
|
22
|
+
async onMount($container) {
|
|
23
|
+
let currentTab = 'pending';
|
|
24
|
+
let comments = [];
|
|
25
|
+
let commentsTable = null;
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------
|
|
28
|
+
// Load + render
|
|
29
|
+
// ---------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
async function loadComments() {
|
|
32
|
+
try {
|
|
33
|
+
const res = await H.get(`${BASE}/comments?status=${encodeURIComponent(currentTab)}`);
|
|
34
|
+
if (res.error) throw new Error(res.error);
|
|
35
|
+
comments = res.data ?? (Array.isArray(res) ? res : []);
|
|
36
|
+
} catch {
|
|
37
|
+
comments = [];
|
|
38
|
+
E.toast('Failed to load comments.', { type: 'error' });
|
|
39
|
+
}
|
|
40
|
+
buildTable();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildTable() {
|
|
44
|
+
const tableEl = $container.find('#comments-table').get(0);
|
|
45
|
+
if (!tableEl) return;
|
|
46
|
+
|
|
47
|
+
if (commentsTable) {
|
|
48
|
+
commentsTable.setData(comments);
|
|
49
|
+
Domma.icons.scan();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
commentsTable = T.create(tableEl, {
|
|
54
|
+
data: comments,
|
|
55
|
+
columns: [
|
|
56
|
+
{
|
|
57
|
+
key: 'authorName',
|
|
58
|
+
title: 'Author',
|
|
59
|
+
sortable: true,
|
|
60
|
+
render: (v) => `<strong>${escapeHtml(v)}</strong>`
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
key: 'authorEmail',
|
|
64
|
+
title: 'Email',
|
|
65
|
+
sortable: false,
|
|
66
|
+
render: (v) => v
|
|
67
|
+
? `<a href="mailto:${escapeHtml(v)}">${escapeHtml(v)}</a>`
|
|
68
|
+
: '<span style="opacity:0.4;">—</span>'
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: 'body',
|
|
72
|
+
title: 'Comment',
|
|
73
|
+
sortable: false,
|
|
74
|
+
render: (v) => {
|
|
75
|
+
const preview = String(v ?? '').slice(0, 80);
|
|
76
|
+
const ellipsis = String(v ?? '').length > 80 ? '\u2026' : '';
|
|
77
|
+
return `<span style="font-size:0.87rem;">${escapeHtml(preview)}${ellipsis}</span>`;
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
key: 'postId',
|
|
82
|
+
title: 'Post',
|
|
83
|
+
sortable: false,
|
|
84
|
+
render: (v) => v
|
|
85
|
+
? `<a href="#/plugins/blog/posts/${escapeHtml(v)}">${escapeHtml(v)}</a>`
|
|
86
|
+
: '<span style="opacity:0.4;">—</span>'
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
key: 'createdAt',
|
|
90
|
+
title: 'Date',
|
|
91
|
+
sortable: true,
|
|
92
|
+
render: (v) => v
|
|
93
|
+
? `<span title="${escapeHtml(D(v).format('DD MMM YYYY HH:mm'))}">${escapeHtml(D(v).fromNow())}</span>`
|
|
94
|
+
: '<span style="opacity:0.4;">—</span>'
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
key: 'id',
|
|
98
|
+
title: 'Actions',
|
|
99
|
+
sortable: false,
|
|
100
|
+
render: (id) => {
|
|
101
|
+
const btns = [];
|
|
102
|
+
if (currentTab !== 'approved') {
|
|
103
|
+
btns.push(`<button class="btn btn-sm btn-success comment-approve-btn" data-id="${escapeHtml(id)}">Approve</button>`);
|
|
104
|
+
}
|
|
105
|
+
if (currentTab !== 'spam') {
|
|
106
|
+
btns.push(`<button class="btn btn-sm btn-warning comment-spam-btn" data-id="${escapeHtml(id)}">Spam</button>`);
|
|
107
|
+
}
|
|
108
|
+
if (currentTab !== 'rejected') {
|
|
109
|
+
btns.push(`<button class="btn btn-sm btn-outline comment-reject-btn" data-id="${escapeHtml(id)}">Reject</button>`);
|
|
110
|
+
}
|
|
111
|
+
btns.push(`<button class="btn btn-sm btn-danger comment-delete-btn" data-id="${escapeHtml(id)}">` +
|
|
112
|
+
`<span data-icon="trash" data-icon-size="14"></span></button>`);
|
|
113
|
+
return `<div style="display:flex;gap:.3rem;flex-wrap:wrap;">${btns.join('')}</div>`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
emptyMessage: `No ${currentTab} comments.`
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Event delegation — attach once
|
|
121
|
+
if (!tableEl.dataset.listenerAttached) {
|
|
122
|
+
tableEl.dataset.listenerAttached = '1';
|
|
123
|
+
tableEl.addEventListener('click', async (e) => {
|
|
124
|
+
const approveBtn = e.target.closest('.comment-approve-btn');
|
|
125
|
+
const rejectBtn = e.target.closest('.comment-reject-btn');
|
|
126
|
+
const spamBtn = e.target.closest('.comment-spam-btn');
|
|
127
|
+
const deleteBtn = e.target.closest('.comment-delete-btn');
|
|
128
|
+
|
|
129
|
+
if (approveBtn) await updateStatus(approveBtn.dataset.id, 'approved');
|
|
130
|
+
if (rejectBtn) await updateStatus(rejectBtn.dataset.id, 'rejected');
|
|
131
|
+
if (spamBtn) await updateStatus(spamBtn.dataset.id, 'spam');
|
|
132
|
+
|
|
133
|
+
if (deleteBtn) {
|
|
134
|
+
const ok = await E.confirm('Delete this comment?');
|
|
135
|
+
if (!ok) return;
|
|
136
|
+
try {
|
|
137
|
+
await H.delete(`${BASE}/comments/${deleteBtn.dataset.id}`);
|
|
138
|
+
E.toast('Comment deleted.', { type: 'success' });
|
|
139
|
+
await loadComments();
|
|
140
|
+
} catch {
|
|
141
|
+
E.toast('Failed to delete comment.', { type: 'error' });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
Domma.icons.scan();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function updateStatus(commentId, status) {
|
|
151
|
+
try {
|
|
152
|
+
const res = await H.put(`${BASE}/comments/${commentId}`, { status });
|
|
153
|
+
if (res.error) throw new Error(res.error);
|
|
154
|
+
E.toast(`Comment marked as ${status}.`, { type: 'success' });
|
|
155
|
+
await loadComments();
|
|
156
|
+
} catch {
|
|
157
|
+
E.toast('Failed to update comment.', { type: 'error' });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------
|
|
162
|
+
// Tab switching
|
|
163
|
+
// ---------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
const tabList = $container.find('.tab-list').get(0);
|
|
166
|
+
if (tabList) {
|
|
167
|
+
tabList.addEventListener('click', async (e) => {
|
|
168
|
+
const tab = e.target.closest('.tab-item');
|
|
169
|
+
if (!tab) return;
|
|
170
|
+
currentTab = tab.dataset.tab;
|
|
171
|
+
|
|
172
|
+
// Update active class
|
|
173
|
+
tabList.querySelectorAll('.tab-item').forEach((t) => {
|
|
174
|
+
t.classList.toggle('active', t === tab);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await loadComments();
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------
|
|
182
|
+
// Initial load
|
|
183
|
+
// ---------------------------------------------------------------
|
|
184
|
+
await loadComments();
|
|
185
|
+
Domma.icons.scan();
|
|
186
|
+
}
|
|
187
|
+
};
|