domma-cms 0.1.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 (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +469 -0
  3. package/admin/css/admin.css +1123 -0
  4. package/admin/index.html +72 -0
  5. package/admin/js/api.js +210 -0
  6. package/admin/js/app.js +270 -0
  7. package/admin/js/config/sidebar-config.js +107 -0
  8. package/admin/js/lib/card.js +63 -0
  9. package/admin/js/lib/image-editor.js +869 -0
  10. package/admin/js/lib/markdown-toolbar.js +421 -0
  11. package/admin/js/templates/dashboard.html +50 -0
  12. package/admin/js/templates/documentation.html +237 -0
  13. package/admin/js/templates/layouts.html +11 -0
  14. package/admin/js/templates/login.html +58 -0
  15. package/admin/js/templates/media.html +16 -0
  16. package/admin/js/templates/navigation.html +50 -0
  17. package/admin/js/templates/page-editor.html +126 -0
  18. package/admin/js/templates/pages.html +18 -0
  19. package/admin/js/templates/plugins.html +12 -0
  20. package/admin/js/templates/settings.html +190 -0
  21. package/admin/js/templates/tutorials.html +233 -0
  22. package/admin/js/templates/user-editor.html +12 -0
  23. package/admin/js/templates/users.html +10 -0
  24. package/admin/js/views/dashboard.js +48 -0
  25. package/admin/js/views/documentation.js +12 -0
  26. package/admin/js/views/index.js +33 -0
  27. package/admin/js/views/layouts.js +49 -0
  28. package/admin/js/views/login.js +254 -0
  29. package/admin/js/views/media.js +240 -0
  30. package/admin/js/views/navigation.js +152 -0
  31. package/admin/js/views/page-editor.js +479 -0
  32. package/admin/js/views/pages.js +64 -0
  33. package/admin/js/views/plugins.js +100 -0
  34. package/admin/js/views/settings.js +64 -0
  35. package/admin/js/views/tutorials.js +12 -0
  36. package/admin/js/views/user-editor.js +88 -0
  37. package/admin/js/views/users.js +73 -0
  38. package/bin/cli.js +334 -0
  39. package/config/auth.json +20 -0
  40. package/config/content.json +10 -0
  41. package/config/navigation.json +63 -0
  42. package/config/plugins.json +47 -0
  43. package/config/presets.json +34 -0
  44. package/config/server.json +6 -0
  45. package/config/site.json +33 -0
  46. package/package.json +67 -0
  47. package/plugins/back-to-top/admin/templates/back-to-top-settings.html +55 -0
  48. package/plugins/back-to-top/admin/views/back-to-top-settings.js +44 -0
  49. package/plugins/back-to-top/config.js +10 -0
  50. package/plugins/back-to-top/plugin.js +24 -0
  51. package/plugins/back-to-top/plugin.json +36 -0
  52. package/plugins/back-to-top/public/inject-body.html +105 -0
  53. package/plugins/cookie-consent/admin/templates/cookie-consent-settings.html +113 -0
  54. package/plugins/cookie-consent/admin/views/cookie-consent-settings.js +73 -0
  55. package/plugins/cookie-consent/config.js +30 -0
  56. package/plugins/cookie-consent/plugin.js +24 -0
  57. package/plugins/cookie-consent/plugin.json +36 -0
  58. package/plugins/cookie-consent/public/inject-body.html +69 -0
  59. package/plugins/custom-css/admin/templates/custom-css.html +17 -0
  60. package/plugins/custom-css/admin/views/custom-css.js +35 -0
  61. package/plugins/custom-css/config.js +1 -0
  62. package/plugins/custom-css/data/custom.css +0 -0
  63. package/plugins/custom-css/plugin.js +63 -0
  64. package/plugins/custom-css/plugin.json +32 -0
  65. package/plugins/custom-css/public/inject-head.html +1 -0
  66. package/plugins/domma-effects/admin/templates/domma-effects.html +488 -0
  67. package/plugins/domma-effects/admin/views/domma-effects.js +56 -0
  68. package/plugins/domma-effects/config.js +9 -0
  69. package/plugins/domma-effects/plugin.js +22 -0
  70. package/plugins/domma-effects/plugin.json +36 -0
  71. package/plugins/domma-effects/public/celebrations/core/canvas.js +111 -0
  72. package/plugins/domma-effects/public/celebrations/core/particles.js +144 -0
  73. package/plugins/domma-effects/public/celebrations/core/physics.js +166 -0
  74. package/plugins/domma-effects/public/celebrations/index.js +535 -0
  75. package/plugins/domma-effects/public/celebrations/themes/christmas.js +1805 -0
  76. package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1477 -0
  77. package/plugins/domma-effects/public/celebrations/themes/halloween.js +1837 -0
  78. package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1175 -0
  79. package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1258 -0
  80. package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1754 -0
  81. package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1290 -0
  82. package/plugins/domma-effects/public/celebrations/themes/valentines.js +1361 -0
  83. package/plugins/domma-effects/public/inject-body.html +268 -0
  84. package/plugins/example-analytics/admin/templates/analytics.html +10 -0
  85. package/plugins/example-analytics/admin/views/analytics.js +51 -0
  86. package/plugins/example-analytics/config.js +6 -0
  87. package/plugins/example-analytics/plugin.js +58 -0
  88. package/plugins/example-analytics/plugin.json +27 -0
  89. package/plugins/example-analytics/public/inject-body.html +13 -0
  90. package/plugins/example-analytics/public/inject-head.html +1 -0
  91. package/plugins/example-analytics/stats.json +1 -0
  92. package/plugins/form-builder/admin/templates/form-editor.html +158 -0
  93. package/plugins/form-builder/admin/templates/form-settings.html +29 -0
  94. package/plugins/form-builder/admin/templates/form-submissions.html +30 -0
  95. package/plugins/form-builder/admin/templates/forms-list.html +17 -0
  96. package/plugins/form-builder/admin/views/form-editor.js +817 -0
  97. package/plugins/form-builder/admin/views/form-settings.js +38 -0
  98. package/plugins/form-builder/admin/views/form-submissions.js +295 -0
  99. package/plugins/form-builder/admin/views/forms-list.js +164 -0
  100. package/plugins/form-builder/config.js +9 -0
  101. package/plugins/form-builder/data/forms/contact-details.json +63 -0
  102. package/plugins/form-builder/data/forms/contact.json +52 -0
  103. package/plugins/form-builder/data/submissions/contact-details.json +1 -0
  104. package/plugins/form-builder/data/submissions/contact.json +14 -0
  105. package/plugins/form-builder/email.js +103 -0
  106. package/plugins/form-builder/plugin.js +454 -0
  107. package/plugins/form-builder/plugin.json +56 -0
  108. package/plugins/form-builder/public/inject-body.html +270 -0
  109. package/plugins/form-builder/public/inject-head.html +42 -0
  110. package/public/css/site.css +189 -0
  111. package/public/js/site.js +109 -0
  112. package/scripts/copy-domma.js +48 -0
  113. package/scripts/fresh.js +41 -0
  114. package/scripts/reset.js +124 -0
  115. package/scripts/seed.js +666 -0
  116. package/scripts/setup.js +263 -0
  117. package/server/config.js +56 -0
  118. package/server/middleware/auth.js +97 -0
  119. package/server/routes/api/auth.js +116 -0
  120. package/server/routes/api/layouts.js +25 -0
  121. package/server/routes/api/media.js +93 -0
  122. package/server/routes/api/navigation.js +37 -0
  123. package/server/routes/api/pages.js +118 -0
  124. package/server/routes/api/plugins.js +46 -0
  125. package/server/routes/api/settings.js +25 -0
  126. package/server/routes/api/users.js +110 -0
  127. package/server/routes/public.js +108 -0
  128. package/server/server.js +169 -0
  129. package/server/services/content.js +298 -0
  130. package/server/services/images.js +334 -0
  131. package/server/services/markdown.js +297 -0
  132. package/server/services/plugins.js +246 -0
  133. package/server/services/renderer.js +80 -0
  134. package/server/services/users.js +212 -0
  135. package/server/templates/page.html +78 -0
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Form Builder Plugin — Settings View
3
+ * SMTP is now managed in global Site Settings.
4
+ * This view provides a test email button only.
5
+ */
6
+ import { apiRequest } from '/admin/js/api.js';
7
+
8
+ export const formSettingsView = {
9
+ templateUrl: '/plugins/form-builder/admin/templates/form-settings.html',
10
+
11
+ async onMount($container) {
12
+ $container.find('#test-email-btn').off('click').on('click', async () => {
13
+ const btn = $container.find('#test-email-btn');
14
+ const resultEl = $container.find('#test-email-result').get(0);
15
+ btn.prop('disabled', true);
16
+ try {
17
+ const data = await apiRequest('/plugins/form-builder/test-email', { method: 'POST' });
18
+ E.toast(data?.message || 'Test email sent.', { type: 'success' });
19
+ if (resultEl) {
20
+ resultEl.textContent = data?.message || 'Test email sent successfully.';
21
+ resultEl.className = 'form-hint text-success';
22
+ resultEl.style.display = '';
23
+ }
24
+ } catch (err) {
25
+ E.toast(err.message || 'Test email failed.', { type: 'error' });
26
+ if (resultEl) {
27
+ resultEl.textContent = err.message || 'Test email failed. Check your SMTP settings in Site Settings.';
28
+ resultEl.className = 'form-hint text-danger';
29
+ resultEl.style.display = '';
30
+ }
31
+ } finally {
32
+ btn.prop('disabled', false);
33
+ }
34
+ });
35
+
36
+ Domma.icons.scan();
37
+ }
38
+ };
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Form Builder Plugin — Form Submissions View
3
+ * Dynamic columns built from the form's field definitions.
4
+ * Export CSV, clear all, delete single submission.
5
+ * Search/filter by text + date range with live count.
6
+ * Row click opens a detail modal.
7
+ */
8
+ import { apiRequest } from '/admin/js/api.js';
9
+
10
+ let currentSlug = null;
11
+ let currentFields = [];
12
+ let allSubmissions = [];
13
+
14
+ export const formSubmissionsView = {
15
+ templateUrl: '/plugins/form-builder/admin/templates/form-submissions.html',
16
+
17
+ async onMount($container) {
18
+ // Parse slug from route
19
+ const hash = window.location.hash;
20
+ const match = hash.match(/\/plugins\/form-builder\/([^/?#]+)\/submissions/);
21
+ currentSlug = match ? match[1] : null;
22
+
23
+ if (!currentSlug) {
24
+ E.toast('No form selected.', { type: 'error' });
25
+ return;
26
+ }
27
+
28
+ // Load form definition for column headers
29
+ try {
30
+ const form = await apiRequest(`/plugins/form-builder/forms/${currentSlug}`);
31
+ currentFields = (form.fields || []).filter(f => f.type !== 'page-break');
32
+ $container.find('#submissions-title').get(0).textContent = `${form.title} — Submissions`;
33
+ } catch {
34
+ E.toast('Could not load form definition.', { type: 'error' });
35
+ }
36
+
37
+ await loadSubmissions($container);
38
+
39
+ // Export — opens a Domma slideover to choose CSV or JSON format
40
+ $container.find('#export-btn').off('click').on('click', () => {
41
+ const so = E.slideover({ title: 'Export Submissions', size: 'sm', position: 'right' });
42
+
43
+ const panel = document.createElement('div');
44
+ panel.style.cssText = 'padding:1.25rem;display:flex;flex-direction:column;gap:1rem;';
45
+
46
+ function makeExportBtn(label, icon, url, filename) {
47
+ const wrap = document.createElement('div');
48
+ wrap.style.cssText = 'display:flex;flex-direction:column;gap:.4rem;';
49
+
50
+ const btn = document.createElement('button');
51
+ btn.className = 'btn btn-ghost';
52
+ btn.style.cssText = 'justify-content:flex-start;gap:.5rem;';
53
+ const ic = document.createElement('span');
54
+ ic.setAttribute('data-icon', icon);
55
+ btn.appendChild(ic);
56
+ btn.appendChild(document.createTextNode(' ' + label));
57
+
58
+ btn.addEventListener('click', () => {
59
+ fetch(url, { headers: { 'Authorization': 'Bearer ' + (S.get('auth_token') || '') } })
60
+ .then(r => r.blob())
61
+ .then(blob => {
62
+ const a = document.createElement('a');
63
+ a.href = URL.createObjectURL(blob);
64
+ a.download = filename;
65
+ document.body.appendChild(a);
66
+ a.click();
67
+ document.body.removeChild(a);
68
+ URL.revokeObjectURL(a.href);
69
+ so.close();
70
+ })
71
+ .catch(() => E.toast('Export failed.', { type: 'error' }));
72
+ });
73
+ wrap.appendChild(btn);
74
+ return wrap;
75
+ }
76
+
77
+ panel.appendChild(makeExportBtn(
78
+ 'Export as CSV',
79
+ 'file-text',
80
+ `/api/plugins/form-builder/forms/${currentSlug}/submissions/export`,
81
+ `${currentSlug}-submissions.csv`
82
+ ));
83
+ panel.appendChild(makeExportBtn(
84
+ 'Export as JSON',
85
+ 'code',
86
+ `/api/plugins/form-builder/forms/${currentSlug}/submissions/export/json`,
87
+ `${currentSlug}-submissions.json`
88
+ ));
89
+
90
+ so.setContent(panel);
91
+ Domma.icons.scan(panel);
92
+ so.open();
93
+ });
94
+
95
+ // Clear all
96
+ $container.find('#clear-all-btn').off('click').on('click', async () => {
97
+ const confirmed = await E.confirm('Delete all submissions? This cannot be undone.');
98
+ if (!confirmed) return;
99
+ try {
100
+ await apiRequest(`/plugins/form-builder/forms/${currentSlug}/submissions`, {
101
+ method: 'DELETE'
102
+ });
103
+ allSubmissions = [];
104
+ E.toast('All submissions cleared.', { type: 'success' });
105
+ renderTable([], $container);
106
+ } catch {
107
+ E.toast('Failed to clear submissions.', { type: 'error' });
108
+ }
109
+ });
110
+
111
+ // Search + date filters
112
+ const searchEl = $container.find('#sub-search').get(0);
113
+ const dateFromEl = $container.find('#sub-date-from').get(0);
114
+ const dateToEl = $container.find('#sub-date-to').get(0);
115
+
116
+ function applyFilters() {
117
+ const term = searchEl?.value.toLowerCase() || '';
118
+ const from = dateFromEl?.value ? new Date(dateFromEl.value) : null;
119
+ const to = dateToEl?.value ? new Date(dateToEl.value + 'T23:59:59') : null;
120
+
121
+ const filtered = allSubmissions.filter(sub => {
122
+ // Text search across all data values
123
+ if (term) {
124
+ const values = Object.values(sub.data || {}).map(v => String(v).toLowerCase());
125
+ if (!values.some(v => v.includes(term))) return false;
126
+ }
127
+ // Date range
128
+ if (from || to) {
129
+ const created = sub.meta?.createdAt ? new Date(sub.meta.createdAt) : null;
130
+ if (!created) return false;
131
+ if (from && created < from) return false;
132
+ if (to && created > to) return false;
133
+ }
134
+ return true;
135
+ });
136
+
137
+ renderTable(filtered, $container);
138
+ }
139
+
140
+ if (searchEl) searchEl.addEventListener('input', applyFilters);
141
+ if (dateFromEl) dateFromEl.addEventListener('change', applyFilters);
142
+ if (dateToEl) dateToEl.addEventListener('change', applyFilters);
143
+
144
+ Domma.icons.scan();
145
+ }
146
+ };
147
+
148
+ async function loadSubmissions($container) {
149
+ try {
150
+ allSubmissions = await apiRequest(`/plugins/form-builder/forms/${currentSlug}/submissions`);
151
+ } catch {
152
+ allSubmissions = [];
153
+ E.toast('Could not load submissions.', { type: 'error' });
154
+ }
155
+ renderTable(allSubmissions, $container);
156
+ }
157
+
158
+ function renderTable(submissions, $container) {
159
+ // Update count badge
160
+ const countEl = $container.find('#sub-count').get(0);
161
+ if (countEl) {
162
+ if (submissions.length === allSubmissions.length) {
163
+ countEl.textContent = `${allSubmissions.length} submission${allSubmissions.length !== 1 ? 's' : ''}`;
164
+ } else {
165
+ countEl.textContent = `Showing ${submissions.length} of ${allSubmissions.length}`;
166
+ }
167
+ }
168
+
169
+ // Build dynamic columns from form field definitions (excludes page-break fields)
170
+ const fieldCols = currentFields.map(field => ({
171
+ key: `data.${field.name}`,
172
+ label: field.label || field.name,
173
+ render: (val, row) => {
174
+ const raw = row.data?.[field.name] ?? '';
175
+ const str = String(raw);
176
+ const span = document.createElement('span');
177
+ span.title = str;
178
+ span.textContent = str.length > 80 ? str.slice(0, 80) + '\u2026' : str;
179
+ return span.outerHTML;
180
+ }
181
+ }));
182
+
183
+ const columns = [
184
+ ...fieldCols,
185
+ {
186
+ key: 'meta',
187
+ label: 'Date',
188
+ render: (val) => D(val?.createdAt).format('DD MMM YYYY HH:mm')
189
+ },
190
+ {
191
+ key: 'id',
192
+ label: '',
193
+ render: (val) => {
194
+ const btn = document.createElement('button');
195
+ btn.className = 'btn btn-sm btn-danger js-delete-submission';
196
+ btn.dataset.id = val;
197
+ btn.textContent = 'Delete';
198
+ btn.style.whiteSpace = 'nowrap';
199
+ return btn.outerHTML;
200
+ }
201
+ }
202
+ ];
203
+
204
+ T.create('#submissions-table', {
205
+ data: submissions,
206
+ columns,
207
+ emptyMessage: 'No submissions yet.'
208
+ });
209
+
210
+ // Bind delete buttons directly — namespaced delegation is broken in Domma
211
+ const tableEl = document.querySelector('#submissions-table');
212
+ if (tableEl) {
213
+ tableEl.querySelectorAll('.js-delete-submission').forEach(btn => {
214
+ btn.addEventListener('click', async (e) => {
215
+ e.stopPropagation();
216
+ const id = btn.dataset.id;
217
+ const confirmed = await E.confirm('Delete this submission?');
218
+ if (!confirmed) return;
219
+ try {
220
+ await apiRequest(`/plugins/form-builder/forms/${currentSlug}/submissions/${id}`, {
221
+ method: 'DELETE'
222
+ });
223
+ allSubmissions = allSubmissions.filter(s => s.id !== id);
224
+ E.toast('Submission deleted.', { type: 'success' });
225
+ renderTable(allSubmissions, $container);
226
+ } catch {
227
+ E.toast('Failed to delete submission.', { type: 'error' });
228
+ }
229
+ });
230
+ });
231
+
232
+ // Row click → detail modal (skip delete button column)
233
+ tableEl.querySelectorAll('tbody tr').forEach((row, idx) => {
234
+ const sub = submissions[idx];
235
+ if (!sub) return;
236
+ row.style.cursor = 'pointer';
237
+ row.addEventListener('click', (e) => {
238
+ if (e.target.closest('.js-delete-submission')) return;
239
+ showDetailModal(sub);
240
+ });
241
+ });
242
+ }
243
+ }
244
+
245
+ function showDetailModal(sub) {
246
+ const content = document.createElement('div');
247
+
248
+ // Field values
249
+ const fieldsSection = document.createElement('div');
250
+ fieldsSection.style.cssText = 'display:flex;flex-direction:column;gap:.75rem;margin-bottom:1.25rem;';
251
+
252
+ currentFields.forEach(field => {
253
+ const val = sub.data?.[field.name];
254
+ if (val === undefined || val === null || val === '') return;
255
+
256
+ const row = document.createElement('div');
257
+ row.style.cssText = 'border-bottom:1px solid var(--border-color,#333);padding-bottom:.6rem;';
258
+
259
+ const lbl = document.createElement('strong');
260
+ lbl.textContent = field.label || field.name;
261
+ lbl.style.cssText = 'display:block;font-size:.8rem;color:var(--text-muted,#888);margin-bottom:.2rem;';
262
+
263
+ const val_el = document.createElement('p');
264
+ val_el.textContent = String(val);
265
+ val_el.style.cssText = 'margin:0;word-break:break-word;';
266
+
267
+ row.appendChild(lbl);
268
+ row.appendChild(val_el);
269
+ fieldsSection.appendChild(row);
270
+ });
271
+
272
+ // Meta section
273
+ const meta = document.createElement('div');
274
+ meta.style.cssText = 'font-size:.8rem;color:var(--text-muted,#888);display:flex;flex-direction:column;gap:.3rem;border-top:1px solid var(--border-color,#333);padding-top:.75rem;';
275
+
276
+ if (sub.meta?.createdAt) {
277
+ const dateRow = document.createElement('span');
278
+ dateRow.textContent = `Submitted: ${D(sub.meta.createdAt).format('DD MMM YYYY HH:mm')}`;
279
+ meta.appendChild(dateRow);
280
+ }
281
+ if (sub.meta?.ip) {
282
+ const ipRow = document.createElement('span');
283
+ ipRow.textContent = `IP: ${sub.meta.ip}`;
284
+ meta.appendChild(ipRow);
285
+ }
286
+
287
+ content.appendChild(fieldsSection);
288
+ content.appendChild(meta);
289
+
290
+ // Use slot approach — modal.element is the domma-modal host, light DOM children
291
+ // project into the body <slot>, so Domma CSS applies correctly
292
+ const detailModal = E.modal({ title: 'Submission Details', size: 'md' });
293
+ detailModal.element.appendChild(content);
294
+ detailModal.open();
295
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Form Builder Plugin — Forms List View
3
+ * Displays all forms with field counts, submission counts, and action buttons.
4
+ */
5
+ import {apiRequest} from '/admin/js/api.js';
6
+
7
+ function esc(str) {
8
+ return String(str)
9
+ .replace(/&/g, '&amp;')
10
+ .replace(/</g, '&lt;')
11
+ .replace(/>/g, '&gt;')
12
+ .replace(/"/g, '&quot;')
13
+ .replace(/'/g, '&#39;');
14
+ }
15
+
16
+ export const formsListView = {
17
+ templateUrl: '/plugins/form-builder/admin/templates/forms-list.html',
18
+
19
+ async onMount($container) {
20
+ await loadForms($container);
21
+
22
+ $container.find('#create-form-btn').off('click').on('click', () => {
23
+ // Don't pass content — modal body gets a <slot>, so light DOM children
24
+ // project into it and receive Domma's global CSS
25
+ const modal = E.modal({title: 'Create Form', size: 'sm'});
26
+
27
+ const wrapper = document.createElement('div');
28
+ wrapper.style.cssText = 'padding:.25rem 0 .5rem;';
29
+
30
+ // Use F.create() for a properly styled Domma form field
31
+ const formContainer = document.createElement('div');
32
+ F.create(
33
+ { title: { type: 'string', label: 'Form Title', placeholder: 'e.g. Contact, Feedback…', required: true } },
34
+ {},
35
+ { showSubmitButton: false }
36
+ ).renderTo(formContainer);
37
+ wrapper.appendChild(formContainer);
38
+
39
+ const btnWrap = document.createElement('div');
40
+ btnWrap.style.cssText = 'display:flex;justify-content:flex-end;gap:.5rem;margin-top:.75rem;';
41
+ const cancelBtn = document.createElement('button');
42
+ cancelBtn.className = 'btn btn-ghost';
43
+ cancelBtn.textContent = 'Cancel';
44
+ const createBtn = document.createElement('button');
45
+ createBtn.className = 'btn btn-primary';
46
+ createBtn.textContent = 'Create';
47
+ btnWrap.appendChild(cancelBtn);
48
+ btnWrap.appendChild(createBtn);
49
+ wrapper.appendChild(btnWrap);
50
+
51
+ // Append to modal.element (light DOM) — slots into modal body via <slot>
52
+ modal.element.appendChild(wrapper);
53
+ modal.open();
54
+
55
+ const titleInput = formContainer.querySelector('input[name="title"]');
56
+ setTimeout(() => titleInput?.focus(), 50);
57
+
58
+ async function doCreate() {
59
+ const title = titleInput?.value.trim();
60
+ if (!title) return;
61
+ try {
62
+ const form = await apiRequest('/plugins/form-builder/forms', {
63
+ method: 'POST',
64
+ body: JSON.stringify({title})
65
+ });
66
+ modal.close();
67
+ R.navigate(`/plugins/form-builder/edit/${form.slug}`);
68
+ } catch (err) {
69
+ E.toast(err.message || 'Failed to create form.', {type: 'error'});
70
+ }
71
+ }
72
+
73
+ cancelBtn.addEventListener('click', () => modal.close());
74
+ createBtn.addEventListener('click', doCreate);
75
+ titleInput?.addEventListener('keydown', (e) => { if (e.key === 'Enter') doCreate(); });
76
+ });
77
+
78
+ Domma.icons.scan();
79
+ }
80
+ };
81
+
82
+ async function loadForms($container) {
83
+ let forms = [];
84
+ try {
85
+ forms = await apiRequest('/plugins/form-builder/forms');
86
+ } catch {
87
+ E.toast('Could not load forms.', { type: 'error' });
88
+ }
89
+
90
+ T.create('#forms-table', {
91
+ data: forms,
92
+ columns: [
93
+ {
94
+ key: 'title',
95
+ label: 'Title',
96
+ render: (val, row) => {
97
+ const a = document.createElement('a');
98
+ a.href = `#/plugins/form-builder/edit/${esc(row.slug)}`;
99
+ a.textContent = val;
100
+ a.style.fontWeight = '600';
101
+ return a.outerHTML;
102
+ }
103
+ },
104
+ {
105
+ key: 'slug',
106
+ label: 'Slug',
107
+ render: val => {
108
+ const code = document.createElement('code');
109
+ code.textContent = val;
110
+ return code.outerHTML;
111
+ }
112
+ },
113
+ { key: 'fields', label: 'Fields', render: val => String(val?.length ?? 0) },
114
+ { key: 'submissionCount', label: 'Submissions', render: val => String(val ?? 0) },
115
+ {
116
+ key: 'slug',
117
+ label: '',
118
+ render: (val) => {
119
+ const wrap = document.createElement('div');
120
+ wrap.style.cssText = 'display:flex;gap:.4rem;justify-content:flex-end;';
121
+
122
+ const editBtn = document.createElement('a');
123
+ editBtn.href = `#/plugins/form-builder/edit/${esc(val)}`;
124
+ editBtn.className = 'btn btn-sm btn-ghost';
125
+ editBtn.textContent = 'Edit';
126
+
127
+ const subsBtn = document.createElement('a');
128
+ subsBtn.href = `#/plugins/form-builder/${esc(val)}/submissions`;
129
+ subsBtn.className = 'btn btn-sm btn-ghost';
130
+ subsBtn.textContent = 'Submissions';
131
+
132
+ const delBtn = document.createElement('button');
133
+ delBtn.className = 'btn btn-sm btn-danger js-delete-form';
134
+ delBtn.dataset.slug = val;
135
+ delBtn.textContent = 'Delete';
136
+
137
+ wrap.appendChild(editBtn);
138
+ wrap.appendChild(subsBtn);
139
+ wrap.appendChild(delBtn);
140
+ return wrap.outerHTML;
141
+ }
142
+ }
143
+ ],
144
+ emptyMessage: 'No forms yet. Click "Create Form" to get started.'
145
+ });
146
+
147
+ // Bind delete buttons directly after table renders (namespaced delegation broken in Domma)
148
+ document.querySelectorAll('.js-delete-form').forEach(btn => {
149
+ btn.addEventListener('click', async () => {
150
+ const slug = btn.dataset.slug;
151
+ const confirmed = await E.confirm(`Delete form "${slug}" and all its submissions? This cannot be undone.`);
152
+ if (!confirmed) return;
153
+ try {
154
+ await apiRequest(`/plugins/form-builder/forms/${slug}`, { method: 'DELETE' });
155
+ E.toast('Form deleted.', { type: 'success' });
156
+ await loadForms($container);
157
+ } catch {
158
+ E.toast('Failed to delete form.', { type: 'error' });
159
+ }
160
+ });
161
+ });
162
+
163
+ Domma.icons.scan();
164
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Form Builder Plugin — Default Configuration
3
+ * Global SMTP settings shared across all forms.
4
+ */
5
+ export default {
6
+ smtp: { host: '', port: 587, secure: false, user: '', pass: '' },
7
+ fromAddress: 'noreply@example.com',
8
+ fromName: 'Website Forms'
9
+ };
@@ -0,0 +1,63 @@
1
+ {
2
+ "slug": "contact-details",
3
+ "title": "Contact Details",
4
+ "description": "",
5
+ "fields": [
6
+ {
7
+ "name": "are_you_going",
8
+ "type": "checkbox-group",
9
+ "label": "Are you going?",
10
+ "required": false,
11
+ "placeholder": "",
12
+ "helper": "",
13
+ "options": [
14
+ {
15
+ "value": "yes",
16
+ "label": "Yes"
17
+ },
18
+ {
19
+ "value": "no",
20
+ "label": "No"
21
+ },
22
+ {
23
+ "value": "maybe",
24
+ "label": "Maybe"
25
+ }
26
+ ]
27
+ },
28
+ {
29
+ "type": "page-break",
30
+ "label": "Email",
31
+ "description": "Personal Details"
32
+ },
33
+ {
34
+ "name": "name",
35
+ "type": "string",
36
+ "label": "Name",
37
+ "required": false,
38
+ "placeholder": "Name",
39
+ "helper": "Enter your email address"
40
+ }
41
+ ],
42
+ "settings": {
43
+ "submitText": "Submit",
44
+ "successMessage": "Thank you for your submission.",
45
+ "layout": "stacked",
46
+ "honeypot": true,
47
+ "rateLimitPerMinute": 3
48
+ },
49
+ "actions": {
50
+ "email": {
51
+ "enabled": false,
52
+ "recipients": "",
53
+ "subjectPrefix": "[contact-details]"
54
+ },
55
+ "webhook": {
56
+ "enabled": false,
57
+ "url": "",
58
+ "method": "POST"
59
+ }
60
+ },
61
+ "createdAt": "2026-03-03T11:05:45.630Z",
62
+ "updatedAt": "2026-03-03T12:31:49.644Z"
63
+ }
@@ -0,0 +1,52 @@
1
+ {
2
+ "slug": "contact",
3
+ "title": "Contact",
4
+ "description": "Get in touch with us.",
5
+ "fields": [
6
+ {
7
+ "id": "field-name",
8
+ "name": "name",
9
+ "label": "Full Name",
10
+ "type": "text",
11
+ "required": true,
12
+ "placeholder": "Your full name"
13
+ },
14
+ {
15
+ "id": "field-email",
16
+ "name": "email",
17
+ "label": "Email Address",
18
+ "type": "email",
19
+ "required": true,
20
+ "placeholder": "you@example.com"
21
+ },
22
+ {
23
+ "id": "field-subject",
24
+ "name": "subject",
25
+ "label": "Subject",
26
+ "type": "text",
27
+ "required": false,
28
+ "placeholder": "What's this about?"
29
+ },
30
+ {
31
+ "id": "field-message",
32
+ "name": "message",
33
+ "label": "Message",
34
+ "type": "textarea",
35
+ "required": true,
36
+ "placeholder": "Your message…"
37
+ }
38
+ ],
39
+ "settings": {
40
+ "submitText": "Send Message",
41
+ "successMessage": "Thank you for your message. We'll be in touch shortly.",
42
+ "layout": "stacked",
43
+ "honeypot": true,
44
+ "rateLimitPerMinute": 3
45
+ },
46
+ "actions": {
47
+ "email": { "enabled": false, "recipients": "", "subjectPrefix": "[Contact]" },
48
+ "webhook": { "enabled": false, "url": "", "method": "POST" }
49
+ },
50
+ "createdAt": "2026-03-03T00:00:00.000Z",
51
+ "updatedAt": "2026-03-03T00:00:00.000Z"
52
+ }
@@ -0,0 +1,14 @@
1
+ [
2
+ {
3
+ "id": "f2feb7a7-5fdd-4c1c-9421-fdb17fc92ad5",
4
+ "data": {
5
+ "name": "Darryl Waterhouse",
6
+ "email": "darryl@dcbw-it.co.uk",
7
+ "message": "This is another test!"
8
+ },
9
+ "meta": {
10
+ "ip": "127.0.0.1",
11
+ "createdAt": "2026-03-03T12:35:16.274Z"
12
+ }
13
+ }
14
+ ]