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.
- package/LICENSE +21 -0
- package/README.md +469 -0
- package/admin/css/admin.css +1123 -0
- package/admin/index.html +72 -0
- package/admin/js/api.js +210 -0
- package/admin/js/app.js +270 -0
- package/admin/js/config/sidebar-config.js +107 -0
- package/admin/js/lib/card.js +63 -0
- package/admin/js/lib/image-editor.js +869 -0
- package/admin/js/lib/markdown-toolbar.js +421 -0
- package/admin/js/templates/dashboard.html +50 -0
- package/admin/js/templates/documentation.html +237 -0
- package/admin/js/templates/layouts.html +11 -0
- package/admin/js/templates/login.html +58 -0
- package/admin/js/templates/media.html +16 -0
- package/admin/js/templates/navigation.html +50 -0
- package/admin/js/templates/page-editor.html +126 -0
- package/admin/js/templates/pages.html +18 -0
- package/admin/js/templates/plugins.html +12 -0
- package/admin/js/templates/settings.html +190 -0
- package/admin/js/templates/tutorials.html +233 -0
- package/admin/js/templates/user-editor.html +12 -0
- package/admin/js/templates/users.html +10 -0
- package/admin/js/views/dashboard.js +48 -0
- package/admin/js/views/documentation.js +12 -0
- package/admin/js/views/index.js +33 -0
- package/admin/js/views/layouts.js +49 -0
- package/admin/js/views/login.js +254 -0
- package/admin/js/views/media.js +240 -0
- package/admin/js/views/navigation.js +152 -0
- package/admin/js/views/page-editor.js +479 -0
- package/admin/js/views/pages.js +64 -0
- package/admin/js/views/plugins.js +100 -0
- package/admin/js/views/settings.js +64 -0
- package/admin/js/views/tutorials.js +12 -0
- package/admin/js/views/user-editor.js +88 -0
- package/admin/js/views/users.js +73 -0
- package/bin/cli.js +334 -0
- package/config/auth.json +20 -0
- package/config/content.json +10 -0
- package/config/navigation.json +63 -0
- package/config/plugins.json +47 -0
- package/config/presets.json +34 -0
- package/config/server.json +6 -0
- package/config/site.json +33 -0
- package/package.json +67 -0
- package/plugins/back-to-top/admin/templates/back-to-top-settings.html +55 -0
- package/plugins/back-to-top/admin/views/back-to-top-settings.js +44 -0
- package/plugins/back-to-top/config.js +10 -0
- package/plugins/back-to-top/plugin.js +24 -0
- package/plugins/back-to-top/plugin.json +36 -0
- package/plugins/back-to-top/public/inject-body.html +105 -0
- package/plugins/cookie-consent/admin/templates/cookie-consent-settings.html +113 -0
- package/plugins/cookie-consent/admin/views/cookie-consent-settings.js +73 -0
- package/plugins/cookie-consent/config.js +30 -0
- package/plugins/cookie-consent/plugin.js +24 -0
- package/plugins/cookie-consent/plugin.json +36 -0
- package/plugins/cookie-consent/public/inject-body.html +69 -0
- package/plugins/custom-css/admin/templates/custom-css.html +17 -0
- package/plugins/custom-css/admin/views/custom-css.js +35 -0
- package/plugins/custom-css/config.js +1 -0
- package/plugins/custom-css/data/custom.css +0 -0
- package/plugins/custom-css/plugin.js +63 -0
- package/plugins/custom-css/plugin.json +32 -0
- package/plugins/custom-css/public/inject-head.html +1 -0
- package/plugins/domma-effects/admin/templates/domma-effects.html +488 -0
- package/plugins/domma-effects/admin/views/domma-effects.js +56 -0
- package/plugins/domma-effects/config.js +9 -0
- package/plugins/domma-effects/plugin.js +22 -0
- package/plugins/domma-effects/plugin.json +36 -0
- package/plugins/domma-effects/public/celebrations/core/canvas.js +111 -0
- package/plugins/domma-effects/public/celebrations/core/particles.js +144 -0
- package/plugins/domma-effects/public/celebrations/core/physics.js +166 -0
- package/plugins/domma-effects/public/celebrations/index.js +535 -0
- package/plugins/domma-effects/public/celebrations/themes/christmas.js +1805 -0
- package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1477 -0
- package/plugins/domma-effects/public/celebrations/themes/halloween.js +1837 -0
- package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1175 -0
- package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1258 -0
- package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1754 -0
- package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1290 -0
- package/plugins/domma-effects/public/celebrations/themes/valentines.js +1361 -0
- package/plugins/domma-effects/public/inject-body.html +268 -0
- package/plugins/example-analytics/admin/templates/analytics.html +10 -0
- package/plugins/example-analytics/admin/views/analytics.js +51 -0
- package/plugins/example-analytics/config.js +6 -0
- package/plugins/example-analytics/plugin.js +58 -0
- package/plugins/example-analytics/plugin.json +27 -0
- package/plugins/example-analytics/public/inject-body.html +13 -0
- package/plugins/example-analytics/public/inject-head.html +1 -0
- package/plugins/example-analytics/stats.json +1 -0
- package/plugins/form-builder/admin/templates/form-editor.html +158 -0
- package/plugins/form-builder/admin/templates/form-settings.html +29 -0
- package/plugins/form-builder/admin/templates/form-submissions.html +30 -0
- package/plugins/form-builder/admin/templates/forms-list.html +17 -0
- package/plugins/form-builder/admin/views/form-editor.js +817 -0
- package/plugins/form-builder/admin/views/form-settings.js +38 -0
- package/plugins/form-builder/admin/views/form-submissions.js +295 -0
- package/plugins/form-builder/admin/views/forms-list.js +164 -0
- package/plugins/form-builder/config.js +9 -0
- package/plugins/form-builder/data/forms/contact-details.json +63 -0
- package/plugins/form-builder/data/forms/contact.json +52 -0
- package/plugins/form-builder/data/submissions/contact-details.json +1 -0
- package/plugins/form-builder/data/submissions/contact.json +14 -0
- package/plugins/form-builder/email.js +103 -0
- package/plugins/form-builder/plugin.js +454 -0
- package/plugins/form-builder/plugin.json +56 -0
- package/plugins/form-builder/public/inject-body.html +270 -0
- package/plugins/form-builder/public/inject-head.html +42 -0
- package/public/css/site.css +189 -0
- package/public/js/site.js +109 -0
- package/scripts/copy-domma.js +48 -0
- package/scripts/fresh.js +41 -0
- package/scripts/reset.js +124 -0
- package/scripts/seed.js +666 -0
- package/scripts/setup.js +263 -0
- package/server/config.js +56 -0
- package/server/middleware/auth.js +97 -0
- package/server/routes/api/auth.js +116 -0
- package/server/routes/api/layouts.js +25 -0
- package/server/routes/api/media.js +93 -0
- package/server/routes/api/navigation.js +37 -0
- package/server/routes/api/pages.js +118 -0
- package/server/routes/api/plugins.js +46 -0
- package/server/routes/api/settings.js +25 -0
- package/server/routes/api/users.js +110 -0
- package/server/routes/public.js +108 -0
- package/server/server.js +169 -0
- package/server/services/content.js +298 -0
- package/server/services/images.js +334 -0
- package/server/services/markdown.js +297 -0
- package/server/services/plugins.js +246 -0
- package/server/services/renderer.js +80 -0
- package/server/services/users.js +212 -0
- package/server/templates/page.html +78 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
(function () {
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
fetch('/api/plugins/domma-effects/settings')
|
|
6
|
+
.then(function (r) {
|
|
7
|
+
return r.json();
|
|
8
|
+
})
|
|
9
|
+
.then(function (cfg) {
|
|
10
|
+
var respectMotion = cfg.respectMotion !== false;
|
|
11
|
+
var defaultDuration = cfg.defaultDuration || 600;
|
|
12
|
+
var defaultAnim = cfg.defaultAnimation || 'fade';
|
|
13
|
+
var defaultThresh = cfg.defaultThreshold != null ? cfg.defaultThreshold : 0.1;
|
|
14
|
+
var reducedMotion = respectMotion && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
15
|
+
|
|
16
|
+
var body = document.querySelector('.page-body');
|
|
17
|
+
if (!body) return;
|
|
18
|
+
|
|
19
|
+
function attr(el, key) {
|
|
20
|
+
return el.getAttribute('data-fx-' + key);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function numAttr(el, key, fb) {
|
|
24
|
+
var v = attr(el, key);
|
|
25
|
+
return v !== null ? parseFloat(v) : fb;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function boolAttr(el, key, fb) {
|
|
29
|
+
var v = attr(el, key);
|
|
30
|
+
if (v === null) return fb;
|
|
31
|
+
return v === 'true' || v === '1';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function onVisible(el, thresh, cb) {
|
|
35
|
+
var o = new IntersectionObserver(function (es) {
|
|
36
|
+
if (es[0].isIntersecting) {
|
|
37
|
+
o.unobserve(el);
|
|
38
|
+
cb();
|
|
39
|
+
}
|
|
40
|
+
}, {threshold: thresh});
|
|
41
|
+
o.observe(el);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── REVEAL ──────────────────────────────────────────────────────────
|
|
45
|
+
body.querySelectorAll('.dm-fx-reveal').forEach(function (el) {
|
|
46
|
+
var animation = attr(el, 'animation') || defaultAnim;
|
|
47
|
+
var duration = numAttr(el, 'duration', defaultDuration);
|
|
48
|
+
var delay = numAttr(el, 'delay', 0);
|
|
49
|
+
var threshold = numAttr(el, 'threshold', defaultThresh);
|
|
50
|
+
var once = boolAttr(el, 'once', true);
|
|
51
|
+
|
|
52
|
+
if (reducedMotion) return;
|
|
53
|
+
|
|
54
|
+
// Set initial hidden state before any paint
|
|
55
|
+
el.style.opacity = '0';
|
|
56
|
+
el.style.transition = 'none';
|
|
57
|
+
if (animation === 'slide-up') el.style.transform = 'translateY(30px)';
|
|
58
|
+
else if (animation === 'slide-down') el.style.transform = 'translateY(-30px)';
|
|
59
|
+
else if (animation === 'zoom') el.style.transform = 'scale(0.85)';
|
|
60
|
+
else if (animation === 'flip') el.style.transform = 'rotateY(90deg)';
|
|
61
|
+
|
|
62
|
+
// Double rAF: ensures the hidden state is painted before the
|
|
63
|
+
// IntersectionObserver can fire and trigger the reveal transition.
|
|
64
|
+
requestAnimationFrame(function () {
|
|
65
|
+
requestAnimationFrame(function () {
|
|
66
|
+
el.style.transition = 'opacity ' + duration + 'ms ease ' + delay + 'ms, transform ' + duration + 'ms ease ' + delay + 'ms';
|
|
67
|
+
|
|
68
|
+
var obs = new IntersectionObserver(function (entries) {
|
|
69
|
+
entries.forEach(function (entry) {
|
|
70
|
+
if (entry.isIntersecting) {
|
|
71
|
+
el.style.opacity = '1';
|
|
72
|
+
el.style.transform = '';
|
|
73
|
+
if (once) obs.unobserve(el);
|
|
74
|
+
} else if (!once) {
|
|
75
|
+
el.style.opacity = '0';
|
|
76
|
+
if (animation === 'slide-up') el.style.transform = 'translateY(30px)';
|
|
77
|
+
else if (animation === 'slide-down') el.style.transform = 'translateY(-30px)';
|
|
78
|
+
else if (animation === 'zoom') el.style.transform = 'scale(0.85)';
|
|
79
|
+
else if (animation === 'flip') el.style.transform = 'rotateY(90deg)';
|
|
80
|
+
else el.style.transform = '';
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}, {threshold: threshold});
|
|
84
|
+
obs.observe(el);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ── COUNTER ─────────────────────────────────────────────────────────
|
|
90
|
+
body.querySelectorAll('.dm-fx-counter').forEach(function (el) {
|
|
91
|
+
var to = numAttr(el, 'to', 0);
|
|
92
|
+
var from = numAttr(el, 'from', 0);
|
|
93
|
+
var duration = numAttr(el, 'duration', 2000);
|
|
94
|
+
var prefix = attr(el, 'prefix') || '';
|
|
95
|
+
var suffix = attr(el, 'suffix') || '';
|
|
96
|
+
var decimals = numAttr(el, 'decimals', 0);
|
|
97
|
+
var separator = attr(el, 'separator') || '';
|
|
98
|
+
|
|
99
|
+
function fmt(n) {
|
|
100
|
+
var s = n.toFixed(decimals);
|
|
101
|
+
if (separator) {
|
|
102
|
+
var parts = s.split('.');
|
|
103
|
+
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, separator);
|
|
104
|
+
s = parts.join('.');
|
|
105
|
+
}
|
|
106
|
+
return prefix + s + suffix;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (reducedMotion) {
|
|
110
|
+
el.textContent = fmt(to);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
onVisible(el, 0.5, function () {
|
|
115
|
+
var start = null;
|
|
116
|
+
requestAnimationFrame(function step(ts) {
|
|
117
|
+
if (!start) start = ts;
|
|
118
|
+
var p = Math.min((ts - start) / duration, 1);
|
|
119
|
+
var eased = 1 - Math.pow(1 - p, 3); // ease-out cubic
|
|
120
|
+
el.textContent = fmt(from + (to - from) * eased);
|
|
121
|
+
if (p < 1) requestAnimationFrame(step);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ── SCRAMBLE ────────────────────────────────────────────────────────
|
|
127
|
+
if (!reducedMotion) {
|
|
128
|
+
body.querySelectorAll('.dm-fx-scramble').forEach(function (el) {
|
|
129
|
+
var speed = numAttr(el, 'speed', 50);
|
|
130
|
+
var original = el.textContent;
|
|
131
|
+
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%&';
|
|
132
|
+
var revealed = 0;
|
|
133
|
+
|
|
134
|
+
onVisible(el, 0.3, function () {
|
|
135
|
+
var iv = setInterval(function () {
|
|
136
|
+
var out = '';
|
|
137
|
+
for (var i = 0; i < original.length; i++) {
|
|
138
|
+
if (i < revealed || original[i] === ' ') out += original[i];
|
|
139
|
+
else out += chars[Math.floor(Math.random() * chars.length)];
|
|
140
|
+
}
|
|
141
|
+
el.textContent = out;
|
|
142
|
+
if (revealed < original.length) revealed++;
|
|
143
|
+
else clearInterval(iv);
|
|
144
|
+
}, speed);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── SHAKE ───────────────────────────────────────────────────────────
|
|
150
|
+
if (!reducedMotion) {
|
|
151
|
+
body.querySelectorAll('.dm-fx-shake').forEach(function (el) {
|
|
152
|
+
var intensity = numAttr(el, 'intensity', 5);
|
|
153
|
+
var duration = numAttr(el, 'duration', 500);
|
|
154
|
+
var dir = attr(el, 'direction') || 'horizontal';
|
|
155
|
+
var axis = dir === 'vertical' ? 'translateY' : 'translateX';
|
|
156
|
+
|
|
157
|
+
el.animate([
|
|
158
|
+
{transform: axis + '(0)'},
|
|
159
|
+
{transform: axis + '(-' + intensity + 'px)'},
|
|
160
|
+
{transform: axis + '(' + intensity + 'px)'},
|
|
161
|
+
{transform: axis + '(-' + intensity + 'px)'},
|
|
162
|
+
{transform: axis + '(' + intensity + 'px)'},
|
|
163
|
+
{transform: axis + '(0)'}
|
|
164
|
+
], {duration: duration, easing: 'ease-in-out'});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── RIPPLE ──────────────────────────────────────────────────────────
|
|
169
|
+
body.querySelectorAll('.dm-fx-ripple').forEach(function (el) {
|
|
170
|
+
el.style.position = 'relative';
|
|
171
|
+
el.style.overflow = 'hidden';
|
|
172
|
+
|
|
173
|
+
el.addEventListener('click', function (e) {
|
|
174
|
+
var colour = attr(el, 'colour') || 'rgba(255,255,255,0.3)';
|
|
175
|
+
var duration = numAttr(el, 'duration', 600);
|
|
176
|
+
var rect = el.getBoundingClientRect();
|
|
177
|
+
var size = Math.max(rect.width, rect.height) * 2;
|
|
178
|
+
var r = document.createElement('span');
|
|
179
|
+
r.style.cssText = 'position:absolute;border-radius:50%;pointer-events:none;'
|
|
180
|
+
+ 'width:' + size + 'px;height:' + size + 'px;'
|
|
181
|
+
+ 'left:' + (e.clientX - rect.left - size / 2) + 'px;'
|
|
182
|
+
+ 'top:' + (e.clientY - rect.top - size / 2) + 'px;'
|
|
183
|
+
+ 'background:' + colour + ';transform:scale(0);opacity:1;'
|
|
184
|
+
+ 'transition:transform ' + duration + 'ms ease,opacity ' + duration + 'ms ease;';
|
|
185
|
+
el.appendChild(r);
|
|
186
|
+
requestAnimationFrame(function () {
|
|
187
|
+
r.style.transform = 'scale(1)';
|
|
188
|
+
r.style.opacity = '0';
|
|
189
|
+
});
|
|
190
|
+
setTimeout(function () {
|
|
191
|
+
r.remove();
|
|
192
|
+
}, duration);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ── DOMMA NATIVE EFFECTS (breathe, pulse, scribe, twinkle) ──────────
|
|
197
|
+
var E = window.Domma && window.Domma.effects;
|
|
198
|
+
if (E) {
|
|
199
|
+
body.querySelectorAll('.dm-fx-breathe').forEach(function (el) {
|
|
200
|
+
if (reducedMotion) return;
|
|
201
|
+
var opts = {};
|
|
202
|
+
if (attr(el, 'amplitude')) opts.amplitude = numAttr(el, 'amplitude', 6);
|
|
203
|
+
if (attr(el, 'duration')) opts.duration = numAttr(el, 'duration', 3000);
|
|
204
|
+
if (attr(el, 'easing')) opts.easing = attr(el, 'easing');
|
|
205
|
+
if (attr(el, 'stagger')) opts.stagger = numAttr(el, 'stagger', 0);
|
|
206
|
+
E.breathe(el, opts);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
body.querySelectorAll('.dm-fx-pulse').forEach(function (el) {
|
|
210
|
+
if (reducedMotion) return;
|
|
211
|
+
var opts = {};
|
|
212
|
+
if (attr(el, 'scale')) opts.scale = numAttr(el, 'scale', 1.05);
|
|
213
|
+
if (attr(el, 'duration')) opts.duration = numAttr(el, 'duration', 2000);
|
|
214
|
+
if (attr(el, 'easing')) opts.easing = attr(el, 'easing');
|
|
215
|
+
E.pulse(el, opts);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
body.querySelectorAll('.dm-fx-scribe').forEach(function (el) {
|
|
219
|
+
var opts = {};
|
|
220
|
+
if (attr(el, 'speed')) opts.speed = numAttr(el, 'speed', 50);
|
|
221
|
+
if (attr(el, 'cursor')) opts.cursor = boolAttr(el, 'cursor', true);
|
|
222
|
+
if (attr(el, 'mode')) opts.mode = attr(el, 'mode');
|
|
223
|
+
if (attr(el, 'loop')) opts.loop = boolAttr(el, 'loop', false);
|
|
224
|
+
E.scribe(el, opts);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
body.querySelectorAll('.dm-fx-twinkle').forEach(function (el) {
|
|
228
|
+
var opts = {};
|
|
229
|
+
if (attr(el, 'count')) opts.count = numAttr(el, 'count', 100);
|
|
230
|
+
if (attr(el, 'shape')) opts.shape = attr(el, 'shape');
|
|
231
|
+
if (attr(el, 'colour')) opts.colour = attr(el, 'colour');
|
|
232
|
+
if (attr(el, 'min-size')) opts.minSize = numAttr(el, 'min-size', 1);
|
|
233
|
+
if (attr(el, 'max-size')) opts.maxSize = numAttr(el, 'max-size', 3);
|
|
234
|
+
E.twinkle(el, opts);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── CELEBRATE ───────────────────────────────────────────────────────
|
|
239
|
+
var celebrateEls = body.querySelectorAll('.dm-fx-celebrate');
|
|
240
|
+
if (celebrateEls.length && !reducedMotion) {
|
|
241
|
+
import('/plugins/domma-effects/public/celebrations/index.js')
|
|
242
|
+
.then(function (mod) {
|
|
243
|
+
var CelebrationsEffect = mod.CelebrationsEffect;
|
|
244
|
+
celebrateEls.forEach(function (el) {
|
|
245
|
+
var theme = el.getAttribute('data-fx-theme') || 'auto';
|
|
246
|
+
var intensity = el.getAttribute('data-fx-intensity') || 'medium';
|
|
247
|
+
var zIndex = el.getAttribute('data-fx-z-index') ? parseInt(el.getAttribute('data-fx-z-index'), 10) : 999;
|
|
248
|
+
|
|
249
|
+
if (theme === 'auto') {
|
|
250
|
+
theme = CelebrationsEffect.getCurrentTheme();
|
|
251
|
+
if (!theme) return; // No active celebration season — skip silently
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
var effect = new CelebrationsEffect({theme: theme, intensity: intensity, enabled: true, zIndex: zIndex});
|
|
255
|
+
effect.init().then(function () {
|
|
256
|
+
effect.start();
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
})
|
|
260
|
+
.catch(function () {
|
|
261
|
+
// Module not available — degrade silently
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
.catch(function () {
|
|
266
|
+
});
|
|
267
|
+
})();
|
|
268
|
+
</script>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<div class="view-header">
|
|
2
|
+
<h1><span data-icon="chart-bar"></span> Analytics</h1>
|
|
3
|
+
<button id="reset-btn" class="btn btn-ghost btn-sm">Reset stats</button>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="card">
|
|
7
|
+
<div class="card-body">
|
|
8
|
+
<div id="analytics-table"></div>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Plugin — Admin View
|
|
3
|
+
* Shows a sortable table of page hit counts.
|
|
4
|
+
* Loaded dynamically from /plugins/ static path.
|
|
5
|
+
*/
|
|
6
|
+
export const analyticsView = {
|
|
7
|
+
templateUrl: '/plugins/example-analytics/admin/templates/analytics.html',
|
|
8
|
+
|
|
9
|
+
async onMount($container) {
|
|
10
|
+
await loadStats($container);
|
|
11
|
+
|
|
12
|
+
$container.find('#reset-btn').on('click', async () => {
|
|
13
|
+
const confirmed = await E.confirm('Reset all analytics data? This cannot be undone.');
|
|
14
|
+
if (!confirmed) return;
|
|
15
|
+
try {
|
|
16
|
+
await fetch('/api/plugins/example-analytics/stats', {
|
|
17
|
+
method: 'DELETE',
|
|
18
|
+
headers: {'Authorization': 'Bearer ' + (S.get('auth_token') || '')}
|
|
19
|
+
});
|
|
20
|
+
E.toast('Analytics reset.', { type: 'success' });
|
|
21
|
+
await loadStats($container);
|
|
22
|
+
} catch {
|
|
23
|
+
E.toast('Reset failed.', { type: 'error' });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
Domma.icons.scan();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
async function loadStats($container) {
|
|
32
|
+
let stats = [];
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch('/api/plugins/example-analytics/stats', {
|
|
35
|
+
headers: {'Authorization': 'Bearer ' + (S.get('auth_token') || '')}
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
38
|
+
stats = await res.json();
|
|
39
|
+
} catch {
|
|
40
|
+
E.toast('Could not load analytics data.', { type: 'error' });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
T.create('#analytics-table', {
|
|
44
|
+
data: stats,
|
|
45
|
+
columns: [
|
|
46
|
+
{ key: 'url', label: 'Page URL', render: (val) => `<code>${val}</code>` },
|
|
47
|
+
{ key: 'hits', label: 'Page views' }
|
|
48
|
+
],
|
|
49
|
+
emptyMessage: 'No page views recorded yet.'
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example Analytics Plugin — Server
|
|
3
|
+
* Tracks page hits in a JSON file alongside the plugin.
|
|
4
|
+
* Endpoints:
|
|
5
|
+
* POST /api/plugins/example-analytics/hit - public: record a hit { url }
|
|
6
|
+
* GET /api/plugins/example-analytics/stats - admin: return all hit counts
|
|
7
|
+
* DELETE /api/plugins/example-analytics/stats - admin: reset all stats
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'fs/promises';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const STATS_FILE = path.join(__dirname, 'stats.json');
|
|
15
|
+
|
|
16
|
+
async function readStats() {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(await fs.readFile(STATS_FILE, 'utf8'));
|
|
19
|
+
} catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function writeStats(stats) {
|
|
25
|
+
await fs.writeFile(STATS_FILE, JSON.stringify(stats, null, 2) + '\n', 'utf8');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default async function analyticsPlugin(fastify, options) {
|
|
29
|
+
const {authenticate, requireAdmin} = options.auth;
|
|
30
|
+
|
|
31
|
+
// Record a page hit — called by the client-side injection script (public)
|
|
32
|
+
fastify.post('/hit', async (request, reply) => {
|
|
33
|
+
const { url } = request.body || {};
|
|
34
|
+
if (!url || typeof url !== 'string') {
|
|
35
|
+
return reply.status(400).send({ error: 'url is required' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const normalised = url.split('?')[0].replace(/\/$/, '') || '/';
|
|
39
|
+
const stats = await readStats();
|
|
40
|
+
stats[normalised] = (stats[normalised] || 0) + 1;
|
|
41
|
+
await writeStats(stats);
|
|
42
|
+
return { ok: true };
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Return all stats — admin only
|
|
46
|
+
fastify.get('/stats', {preHandler: [authenticate, requireAdmin]}, async () => {
|
|
47
|
+
const stats = await readStats();
|
|
48
|
+
return Object.entries(stats)
|
|
49
|
+
.map(([url, hits]) => ({ url, hits }))
|
|
50
|
+
.sort((a, b) => b.hits - a.hits);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Reset stats — admin only
|
|
54
|
+
fastify.delete('/stats', {preHandler: [authenticate, requireAdmin]}, async () => {
|
|
55
|
+
await writeStats({});
|
|
56
|
+
return { ok: true };
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "example-analytics",
|
|
3
|
+
"displayName": "Analytics",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Basic page view analytics. Tracks hits per page using a simple JSON store.",
|
|
6
|
+
"author": "Darryl Waterhouse",
|
|
7
|
+
"date": "2026-03-01",
|
|
8
|
+
"icon": "chart-bar",
|
|
9
|
+
"admin": {
|
|
10
|
+
"sidebar": [
|
|
11
|
+
{ "id": "analytics", "text": "Analytics", "icon": "chart-bar", "url": "#/plugins/analytics", "section": "#/plugins/analytics" }
|
|
12
|
+
],
|
|
13
|
+
"routes": [
|
|
14
|
+
{ "path": "/plugins/analytics", "view": "plugin-analytics", "title": "Analytics - Domma CMS" }
|
|
15
|
+
],
|
|
16
|
+
"views": {
|
|
17
|
+
"plugin-analytics": {
|
|
18
|
+
"entry": "example-analytics/admin/views/analytics.js",
|
|
19
|
+
"exportName": "analyticsView"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"inject": {
|
|
24
|
+
"head": "public/inject-head.html",
|
|
25
|
+
"bodyEnd": "public/inject-body.html"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!-- example-analytics: page view tracker -->
|
|
2
|
+
<script>
|
|
3
|
+
(function () {
|
|
4
|
+
var url = window.location.pathname;
|
|
5
|
+
if (typeof fetch === 'function') {
|
|
6
|
+
fetch('/api/plugins/example-analytics/hit', {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
headers: { 'Content-Type': 'application/json' },
|
|
9
|
+
body: JSON.stringify({ url: url })
|
|
10
|
+
}).catch(function () { /* silent fail */ });
|
|
11
|
+
}
|
|
12
|
+
})();
|
|
13
|
+
</script>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<!-- example-analytics: head injection (empty — tracking is done via body script) -->
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
<div class="view-header">
|
|
2
|
+
<h1 id="editor-title"><span data-icon="edit-3"></span> Form Editor</h1>
|
|
3
|
+
<div style="display:flex;gap:.5rem;align-items:center;">
|
|
4
|
+
<a href="#/plugins/form-builder" class="btn btn-ghost btn-sm">
|
|
5
|
+
<span data-icon="arrow-left"></span> All Forms
|
|
6
|
+
</a>
|
|
7
|
+
<button id="preview-btn" class="btn btn-ghost btn-sm">
|
|
8
|
+
<span data-icon="eye"></span> Preview
|
|
9
|
+
</button>
|
|
10
|
+
<button id="save-form-btn" class="btn btn-primary">
|
|
11
|
+
<span data-icon="save"></span> Save
|
|
12
|
+
</button>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div class="row">
|
|
17
|
+
<!-- Left column: metadata + fields -->
|
|
18
|
+
<div class="col-8">
|
|
19
|
+
<div class="card mb-4">
|
|
20
|
+
<div class="card-header"><h2>Form Details</h2></div>
|
|
21
|
+
<div class="card-body">
|
|
22
|
+
<div class="row mb-3">
|
|
23
|
+
<div class="col-7">
|
|
24
|
+
<label class="form-label">Title</label>
|
|
25
|
+
<input id="field-title" type="text" class="form-input" placeholder="My Form">
|
|
26
|
+
</div>
|
|
27
|
+
<div class="col-5">
|
|
28
|
+
<label class="form-label">Slug</label>
|
|
29
|
+
<input id="field-slug" type="text" class="form-input" placeholder="my-form">
|
|
30
|
+
<p class="form-hint text-muted" style="margin-top:.3rem;font-size:.8rem;">Used in embed: <code>data-form="slug"</code></p>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="row">
|
|
34
|
+
<div class="col">
|
|
35
|
+
<label class="form-label">Description</label>
|
|
36
|
+
<textarea id="field-description" class="form-input" rows="2" placeholder="Optional form description..."></textarea>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="card mb-4">
|
|
43
|
+
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center;">
|
|
44
|
+
<h2>Fields</h2>
|
|
45
|
+
<div style="display:flex;gap:.4rem;">
|
|
46
|
+
<button id="add-field-btn" class="btn btn-ghost btn-sm">
|
|
47
|
+
<span data-icon="plus"></span> Add Field
|
|
48
|
+
</button>
|
|
49
|
+
<button id="add-page-break-btn" class="btn btn-ghost btn-sm" title="Add a wizard step separator">
|
|
50
|
+
<span data-icon="minus"></span> Add Page Break
|
|
51
|
+
</button>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="card-body" style="padding:0;">
|
|
55
|
+
<div id="fields-list" style="padding:1rem;">
|
|
56
|
+
<p class="text-muted" id="fields-empty-msg" style="text-align:center;padding:2rem 0;">No fields yet. Click "Add Field" to get started.</p>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div class="card mb-4" id="preview-card" style="display:none;">
|
|
62
|
+
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center;">
|
|
63
|
+
<h2><span data-icon="eye"></span> Preview</h2>
|
|
64
|
+
<span id="preview-test-badge"
|
|
65
|
+
style="display:none;font-size:.75rem;padding:.2rem .6rem;border-radius:999px;background:rgba(99,102,241,.15);color:var(--primary,#6366f1);">Test Mode — submissions are stored</span>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="card-body">
|
|
68
|
+
<div id="preview-container"></div>
|
|
69
|
+
<div id="preview-test-result"
|
|
70
|
+
style="display:none;margin-top:.75rem;padding:.6rem .9rem;border-radius:6px;font-size:.9rem;"></div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<!-- Right column: settings + actions -->
|
|
76
|
+
<div class="col-4">
|
|
77
|
+
<div class="card mb-4">
|
|
78
|
+
<div class="card-header"><h2>Settings</h2></div>
|
|
79
|
+
<div class="card-body">
|
|
80
|
+
<div class="mb-3">
|
|
81
|
+
<label class="form-label">Submit Button Text</label>
|
|
82
|
+
<input id="setting-submit-text" type="text" class="form-input" placeholder="Submit" value="Submit">
|
|
83
|
+
</div>
|
|
84
|
+
<div class="mb-3">
|
|
85
|
+
<label class="form-label">Success Message</label>
|
|
86
|
+
<textarea id="setting-success-message" class="form-input" rows="3" placeholder="Thank you for your submission."></textarea>
|
|
87
|
+
</div>
|
|
88
|
+
<div>
|
|
89
|
+
<label class="form-label">Layout</label>
|
|
90
|
+
<select id="setting-layout" class="form-input">
|
|
91
|
+
<option value="stacked">Stacked</option>
|
|
92
|
+
<option value="inline">Inline</option>
|
|
93
|
+
</select>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div class="card mb-4">
|
|
99
|
+
<div class="card-header"><h2>Email Action</h2></div>
|
|
100
|
+
<div class="card-body">
|
|
101
|
+
<div class="mb-3">
|
|
102
|
+
<label class="form-label" style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
|
|
103
|
+
<input id="action-email-enabled" type="checkbox"> Send email on submit
|
|
104
|
+
</label>
|
|
105
|
+
</div>
|
|
106
|
+
<div class="mb-3">
|
|
107
|
+
<label class="form-label">Recipients</label>
|
|
108
|
+
<input id="action-email-recipients" type="text" class="form-input" placeholder="admin@example.com">
|
|
109
|
+
<p class="form-hint text-muted" style="margin-top:.3rem;font-size:.8rem;">Comma-separated. Uses global SMTP settings.</p>
|
|
110
|
+
</div>
|
|
111
|
+
<div>
|
|
112
|
+
<label class="form-label">Subject Prefix</label>
|
|
113
|
+
<input id="action-email-subject-prefix" type="text" class="form-input" placeholder="[Form]">
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div class="card mb-4">
|
|
119
|
+
<div class="card-header"><h2>Webhook Action</h2></div>
|
|
120
|
+
<div class="card-body">
|
|
121
|
+
<div class="mb-3">
|
|
122
|
+
<label class="form-label" style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
|
|
123
|
+
<input id="action-webhook-enabled" type="checkbox"> POST to webhook on submit
|
|
124
|
+
</label>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="mb-3">
|
|
127
|
+
<label class="form-label">URL</label>
|
|
128
|
+
<input id="action-webhook-url" type="url" class="form-input" placeholder="https://hooks.example.com/form">
|
|
129
|
+
</div>
|
|
130
|
+
<div>
|
|
131
|
+
<label class="form-label">Method</label>
|
|
132
|
+
<select id="action-webhook-method" class="form-input">
|
|
133
|
+
<option value="POST">POST</option>
|
|
134
|
+
<option value="PUT">PUT</option>
|
|
135
|
+
</select>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div class="card mb-4">
|
|
141
|
+
<div class="card-header"><h2>Spam Protection</h2></div>
|
|
142
|
+
<div class="card-body">
|
|
143
|
+
<div class="mb-3">
|
|
144
|
+
<label class="form-label" style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
|
|
145
|
+
<input id="setting-honeypot" type="checkbox" checked> Enable honeypot field
|
|
146
|
+
</label>
|
|
147
|
+
<p class="form-hint text-muted" style="margin-top:.3rem;font-size:.8rem;">Silently discards bot submissions that fill a hidden field.</p>
|
|
148
|
+
</div>
|
|
149
|
+
<div>
|
|
150
|
+
<label class="form-label">Rate Limit (per minute)</label>
|
|
151
|
+
<input id="setting-rate-limit" type="number" class="form-input" min="1" max="60" value="3">
|
|
152
|
+
<p class="form-hint text-muted" style="margin-top:.3rem;font-size:.8rem;">Max submissions per IP per minute.</p>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<div class="view-header">
|
|
2
|
+
<h1><span data-icon="settings"></span> Form Builder Settings</h1>
|
|
3
|
+
<div style="display:flex;gap:.5rem;">
|
|
4
|
+
<a href="#/plugins/form-builder" class="btn btn-ghost btn-sm">
|
|
5
|
+
<span data-icon="layout"></span> All Forms
|
|
6
|
+
</a>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div class="card mb-4">
|
|
11
|
+
<div class="card-body" style="display:flex;align-items:center;gap:1rem;">
|
|
12
|
+
<span data-icon="info" style="flex-shrink:0;"></span>
|
|
13
|
+
<div>
|
|
14
|
+
<p style="margin:0 0 .25rem;">SMTP and email identity settings have moved to <a href="#/settings" class="link">Site Settings</a>.</p>
|
|
15
|
+
<p class="text-muted" style="margin:0;font-size:.85rem;">Configure your mail server there, then use the button below to verify the connection.</p>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="card">
|
|
21
|
+
<div class="card-header"><h2>Test Email</h2></div>
|
|
22
|
+
<div class="card-body">
|
|
23
|
+
<p class="text-muted" style="margin:0 0 1rem;font-size:.9rem;">Send a test email using the SMTP settings configured in Site Settings to verify your mail server connection.</p>
|
|
24
|
+
<button id="test-email-btn" class="btn btn-primary">
|
|
25
|
+
<span data-icon="send"></span> Send Test Email
|
|
26
|
+
</button>
|
|
27
|
+
<p id="test-email-result" class="form-hint" style="margin-top:.75rem;display:none;"></p>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|