Changeset 0475b13 in Klonkt
- Timestamp:
- 08/07/2026 01:38:51 PM (5 weeks ago)
- Branches:
- main
- Children:
- 54ee51c
- Parents:
- 792af53
- git-author:
- Robin <roboburr@…> (08/07/2026 01:04:55 PM)
- git-committer:
- roboburr <roboburr@…> (08/07/2026 01:38:51 PM)
- Location:
- src
- Files:
-
- 8 added
- 16 edited
-
assets/js/mod/admin-epk.js (added)
-
assets/js/mod/admin-help.js (added)
-
assets/js/mod/admin-paid.js (added)
-
assets/js/mod/admin-push.js (added)
-
assets/js/mod/admin-settings.js (added)
-
assets/js/mod/admin-site-edit.js (added)
-
assets/js/mod/auth-register.js (added)
-
assets/js/mod/authorize-interaction.js (added)
-
routes/admin-epk.js (modified) (1 diff)
-
routes/admin-paid.js (modified) (1 diff)
-
routes/admin-push.js (modified) (1 diff)
-
routes/admin-settings.js (modified) (1 diff)
-
routes/admin-sites.js (modified) (2 diffs)
-
routes/admin.js (modified) (1 diff)
-
routes/auth.js (modified) (2 diffs)
-
routes/posts.js (modified) (1 diff)
-
views/pages/admin-epk.ejs (modified) (1 diff)
-
views/pages/admin-help.ejs (modified) (1 diff)
-
views/pages/admin-paid.ejs (modified) (1 diff)
-
views/pages/admin-push.ejs (modified) (1 diff)
-
views/pages/admin-settings.ejs (modified) (1 diff)
-
views/pages/admin-site-edit.ejs (modified) (2 diffs)
-
views/pages/auth-register.ejs (modified) (1 diff)
-
views/pages/authorize-interaction.ejs (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/admin-epk.js
r792af53 r0475b13 33 33 try { const r = JSON.parse(getSetting('epk_tracks_' + site.id, '') || '[]'); if (Array.isArray(r)) chosen = r; } catch (e) {} 34 34 renderPage(req, res, 'pages/admin-epk', { 35 pageJs: 'admin-epk', 35 36 pageTitleKey: 'admin.t_epk', 36 37 bodyClass: 'on-admin', -
src/routes/admin-paid.js
r792af53 r0475b13 36 36 if (!gate(req, res)) return; 37 37 renderPage(req, res, 'pages/admin-paid', { 38 pageJs: 'admin-paid', 38 39 pageTitleKey: 'apaid.t', 39 40 bodyClass: 'on-admin', -
src/routes/admin-push.js
r792af53 r0475b13 12 12 router.get('/', requireSiteManager, async (req, res) => { 13 13 renderPage(req, res, 'pages/admin-push', { 14 pageJs: 'admin-push', 14 15 pageTitleKey: 'push.t', 15 16 bodyClass: 'on-admin', -
src/routes/admin-settings.js
r792af53 r0475b13 66 66 router.get('/', requireGod, (req, res) => { 67 67 renderPage(req, res, 'pages/admin-settings', { 68 pageJs: 'admin-settings', 68 69 pageTitleKey: 'admin.t_settings', 69 70 bodyClass: 'on-admin', -
src/routes/admin-sites.js
r792af53 r0475b13 200 200 router.get('/new', requireGod, (req, res) => { 201 201 renderPage(req, res, 'pages/admin-site-edit', { 202 pageJs: 'admin-site-edit', 202 203 pageTitleKey: 'admin.t_newsite', 203 204 bodyClass: 'on-admin', … … 283 284 284 285 renderPage(req, res, 'pages/admin-site-edit', { 286 pageJs: 'admin-site-edit', 285 287 pageTitleKey: 'admin.t_editsite', pageTitleVars: { title: site.title }, 286 288 bodyClass: 'on-admin', -
src/routes/admin.js
r792af53 r0475b13 87 87 router.get('/handleiding', requireAuth, (req, res) => { 88 88 renderPage(req, res, 'pages/admin-help', { 89 pageJs: 'admin-help', 89 90 pageTitleKey: 'admin.t_manual', 90 91 bodyClass: 'on-admin', -
src/routes/auth.js
r792af53 r0475b13 115 115 if (!isSetupMode()) return res.redirect('/auth/login' + (next ? '?next=' + encodeURIComponent(next) : '')); 116 116 renderPage(req, res, 'pages/auth-register', { 117 pageJs: 'auth-register', 117 118 pageTitle: t(resolveLang(req), 'setup.title'), bodyClass: 'on-special', 118 119 error: null, username: '', email: '', siteName: '', next, … … 124 125 const next = safeNext(req.body.next) || ''; 125 126 const renderErr = (error) => renderPage(req, res, 'pages/auth-register', { 127 pageJs: 'auth-register', 126 128 pageTitle: t(resolveLang(req), 'setup.title'), bodyClass: 'on-special', 127 129 error, username: username || '', email: email || '', siteName: siteName || '', next, -
src/routes/posts.js
r792af53 r0475b13 795 795 } 796 796 renderPage(req, res, 'pages/authorize-interaction', { 797 pageJs: 'authorize-interaction', 797 798 pageTitleKey: 'fedi.remote_interact', // i18n: was hardcoded Dutch on non-NL sites 798 799 bodyClass: 'on-special', -
src/views/pages/admin-epk.ejs
r792af53 r0475b13 90 90 .admin-page .epk-pick-item input:disabled ~ .epk-pick-title { opacity: 0.45; } 91 91 </style> 92 <script> 93 (function () { 94 var box = document.querySelector('.epk-track-pick'); 95 if (!box) return; 96 var max = parseInt(box.dataset.max, 10) || 5; 97 function sync() { 98 var checks = box.querySelectorAll('input[type=checkbox]'); 99 var n = box.querySelectorAll('input[type=checkbox]:checked').length; 100 checks.forEach(function (c) { c.disabled = (!c.checked && n >= max); }); 101 } 102 box.addEventListener('change', sync); 103 sync(); 104 })(); 105 </script> 92 <%# Het script van deze pagina staat in assets/js/mod/admin-epk.js (shaer-bqr). %> -
src/views/pages/admin-help.ejs
r792af53 r0475b13 145 145 .hb-empty { color: var(--ink-muted, var(--ink-soft)); padding: 1rem; text-align: center; } 146 146 </style> 147 <script> 148 (function () { 149 var q = document.getElementById('hb-q'); 150 var items = [].slice.call(document.querySelectorAll('.hb-item')); 151 var empty = document.getElementById('hb-empty'); 152 var count = document.getElementById('hb-count'); 153 if (!q) return; 154 // Bewaar de originele tekst per item (voor highlight-reset). 155 items.forEach(function (it) { it._txt = it.textContent.toLowerCase(); }); 156 function run() { 157 var term = q.value.trim().toLowerCase(); 158 var shown = 0; 159 items.forEach(function (it) { 160 var hit = !term || it._txt.indexOf(term) >= 0; 161 it.classList.toggle('hb-hidden', !hit); 162 if (hit) shown++; 163 }); 164 empty.hidden = shown !== 0; 165 count.textContent = term ? (shown + ' onderwerp' + (shown === 1 ? '' : 'en') + ' gevonden') : ''; 166 } 167 q.addEventListener('input', run); 168 })(); 169 </script> 147 <%# Het script van deze pagina staat in assets/js/mod/admin-help.js (shaer-bqr). %> -
src/views/pages/admin-paid.ejs
r792af53 r0475b13 72 72 </div> 73 73 74 <script> 75 (function () { 76 var field = document.getElementById('pd-redirect'); 77 var btn = document.getElementById('pd-copy'); 78 if (!field || !btn) return; 79 field.addEventListener('focus', function () { field.select(); }); 80 btn.addEventListener('click', function () { 81 field.select(); 82 var done = function () { var t = btn.textContent; btn.textContent = document.getElementById('pd-copy').getAttribute('data-copied'); setTimeout(function () { btn.textContent = t; }, 1400); }; 83 if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(field.value).then(done, function () { try { document.execCommand('copy'); done(); } catch (e) {} }); } 84 else { try { document.execCommand('copy'); done(); } catch (e) {} } 85 }); 86 })(); 87 </script> 74 <%# Het script van deze pagina staat in assets/js/mod/admin-paid.js (shaer-bqr). %> -
src/views/pages/admin-push.ejs
r792af53 r0475b13 58 58 i18n: { on: t('push.state_on'), off: t('push.state_off'), denied: t('push.state_denied'), unknown: t('push.state_unknown'), unsupported: t('push.state_unsupported'), failed: t('push.enable_failed') }, 59 59 }).replace(/</g, '\\u003c') %></script> 60 <script> 61 (function () { 62 var cfg = JSON.parse(document.getElementById('np-data').textContent); 63 var elState = document.getElementById('np-state'); 64 var btnOn = document.getElementById('np-on'), btnOff = document.getElementById('np-off'), btnTest = document.getElementById('np-test'); 65 var alertsBox = document.getElementById('np-alerts'), savedMsg = document.getElementById('np-saved'); 66 var currentEndpoint = null; 67 68 var isIos = /iPad|iPhone|iPod/.test(navigator.userAgent); 69 var standalone = window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true; 70 if (isIos && !standalone) document.getElementById('np-ios-hint').hidden = false; 71 72 if (!('serviceWorker' in navigator) || !('PushManager' in window) || !('Notification' in window)) { 73 document.getElementById('np-unsupported').hidden = false; 74 elState.textContent = cfg.i18n.unsupported; 75 return; 76 } 77 78 function b64ToU8(s) { 79 var pad = '='.repeat((4 - (s.length % 4)) % 4); 80 var raw = atob((s + pad).replace(/-/g, '+').replace(/_/g, '/')); 81 var out = new Uint8Array(raw.length); 82 for (var i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i); 83 return out; 84 } 85 function post(url, body) { 86 return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) }); 87 } 88 function deviceLabel() { 89 var ua = navigator.userAgent; 90 var browser = /Firefox\//.test(ua) ? 'Firefox' : /Edg\//.test(ua) ? 'Edge' : /Chrome\//.test(ua) ? 'Chrome' : /Safari\//.test(ua) ? 'Safari' : 'Browser'; 91 var os = /Android/.test(ua) ? 'Android' : /iPad|iPhone|iPod/.test(ua) ? 'iOS' : /Mac/.test(ua) ? 'macOS' : /Win/.test(ua) ? 'Windows' : /Linux/.test(ua) ? 'Linux' : ''; 92 return (browser + (os ? ' op ' + os : '')); 93 } 94 function readAlertBoxes() { 95 var out = {}; 96 alertsBox.querySelectorAll('input[data-alert]').forEach(function (cb) { out[cb.getAttribute('data-alert')] = cb.checked ? 1 : 0; }); 97 return out; 98 } 99 function setAlertBoxes(alerts) { 100 alertsBox.querySelectorAll('input[data-alert]').forEach(function (cb) { 101 cb.checked = !!alerts[cb.getAttribute('data-alert')]; 102 }); 103 } 104 105 function render(sub) { 106 currentEndpoint = sub ? sub.endpoint : null; 107 elState.textContent = sub ? cfg.i18n.on : (Notification.permission === 'denied' ? cfg.i18n.denied : cfg.i18n.off); 108 btnOn.hidden = !!sub || Notification.permission === 'denied'; 109 btnOff.hidden = !sub; 110 btnTest.hidden = !sub; 111 alertsBox.hidden = !sub; 112 } 113 114 navigator.serviceWorker.ready.then(function (reg) { 115 return reg.pushManager.getSubscription(); 116 }).then(function (sub) { 117 // Show this device's SAVED prefs when we know them, defaults otherwise. 118 setAlertBoxes((sub && cfg.saved[sub.endpoint]) ? Object.assign({}, cfg.alerts, cfg.saved[sub.endpoint]) : cfg.alerts); 119 render(sub); 120 }).catch(function () { elState.textContent = cfg.i18n.unknown; }); 121 122 btnOn.addEventListener('click', function () { 123 btnOn.disabled = true; 124 Notification.requestPermission().then(function (perm) { 125 if (perm !== 'granted') { btnOn.disabled = false; render(null); return; } 126 navigator.serviceWorker.ready.then(function (reg) { 127 return reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: b64ToU8(cfg.vapid) }); 128 }).then(function (sub) { 129 return post('/push/subscribe', { subscription: sub.toJSON(), alerts: readAlertBoxes(), uaLabel: deviceLabel() }) 130 .then(function (r) { if (!r.ok) throw new Error('subscribe failed'); render(sub); location.reload(); }); 131 }).catch(function () { btnOn.disabled = false; elState.textContent = cfg.i18n.failed; }); 132 }); 133 }); 134 135 btnOff.addEventListener('click', function () { 136 navigator.serviceWorker.ready.then(function (reg) { return reg.pushManager.getSubscription(); }).then(function (sub) { 137 if (!sub) { render(null); return; } 138 var ep = sub.endpoint; 139 sub.unsubscribe().then(function () { return post('/push/unsubscribe', { endpoint: ep }); }) 140 .then(function () { location.reload(); }); 141 }); 142 }); 143 144 btnTest.addEventListener('click', function () { 145 btnTest.disabled = true; 146 post('/push/test').then(function () { setTimeout(function () { btnTest.disabled = false; }, 2000); }); 147 }); 148 149 alertsBox.addEventListener('change', function () { 150 if (!currentEndpoint) return; 151 post('/push/alerts', { endpoint: currentEndpoint, alerts: readAlertBoxes() }).then(function (r) { 152 if (r.ok) { savedMsg.hidden = false; setTimeout(function () { savedMsg.hidden = true; }, 1500); } 153 }); 154 }); 155 156 document.querySelectorAll('.np-remove').forEach(function (btn) { 157 btn.addEventListener('click', function () { 158 post('/push/unsubscribe', { endpoint: btn.getAttribute('data-endpoint') }).then(function () { location.reload(); }); 159 }); 160 }); 161 })(); 162 </script> 60 <%# Het script van deze pagina staat in assets/js/mod/admin-push.js (shaer-bqr). %> 163 61 <% } %> -
src/views/pages/admin-settings.ejs
r792af53 r0475b13 27 27 <div class="set-actions"><button type="submit" class="btn btn-primary"><%= t('aset.save') %></button></div> 28 28 </form> 29 <script> 30 (function () { 31 var f = document.getElementById('mode-form'); 32 if (!f || f.__age18wired) return; f.__age18wired = true; 33 f.addEventListener('submit', function (e) { 34 var sel = f.querySelector('input[name=mode]:checked'); 35 if (sel && sel.value === 'cirkels' && f.getAttribute('data-was-cirkels') === '0') { 36 if (!window.confirm(f.getAttribute('data-confirm'))) e.preventDefault(); 37 } 38 }); 39 })(); 40 </script> 29 <%# Het script van deze pagina staat in assets/js/mod/admin-settings.js (shaer-bqr). %> 41 30 </section> 42 31 -
src/views/pages/admin-site-edit.ejs
r792af53 r0475b13 126 126 <%# Live preview: selecting an accent / theme / palette re-themes this page 127 127 instantly (reverts on reload; persists on save). %> 128 <script> 129 (function(){ 130 if (window.__themePreviewWired) return; window.__themePreviewWired = true; 131 var html = document.documentElement; 132 function applyAccent(hex){ 133 if(!/^#[0-9a-fA-F]{6}$/.test(hex)) return; 134 var sa = document.getElementById('pcms-site-accent'); 135 if(!sa){ sa = document.createElement('style'); sa.id = 'pcms-site-accent'; document.head.appendChild(sa); } 136 sa.textContent = ':root,[data-palette]{--accent:'+hex+';--accent-soft:color-mix(in srgb,'+hex+' 80%,white);--accent-tint:color-mix(in srgb,'+hex+' 12%,transparent);}'; 137 } 138 document.addEventListener('change', function(e){ 139 var t = e.target; if(!t || !t.name) return; 140 if(t.name === 'palette'){ html.setAttribute('data-palette', t.value); } 141 else if(t.name === 'accent'){ applyAccent(t.value); } 142 else if(t.name === 'theme_override'){ 143 if(t.value === 'light' || t.value === 'dark'){ html.setAttribute('data-theme', t.value); } 144 else { var dk = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; html.setAttribute('data-theme', dk ? 'dark' : 'light'); } 145 } 146 }); 147 })(); 148 </script> 128 <%# Het script van deze pagina staat in assets/js/mod/admin-site-edit.js (shaer-bqr). %> 149 129 150 130 <fieldset> … … 275 255 </div> 276 256 277 <script> 278 (function() { 279 // Slug URL field: show the real host as a dimmed prefix (the slug itself is 280 // coloured via CSS). Hub → host/user/<slug>, otherwise host/<slug>. 281 var sh = document.getElementById('slug-host'); 282 if (sh) sh.textContent = location.host + (sh.dataset.prefix || '/'); 283 })(); 284 (function() { 285 // Profile-links repeater: add row from <template>, remove on click. 286 var rows = document.getElementById('profile-links-rows'); 287 var tpl = document.getElementById('profile-link-template'); 288 var add = document.getElementById('profile-link-add'); 289 if (!rows || !tpl || !add) return; 290 291 add.addEventListener('click', function() { 292 var clone = tpl.content.cloneNode(true); 293 rows.appendChild(clone); 294 }); 295 rows.addEventListener('click', function(e) { 296 if (e.target && e.target.classList.contains('pl-remove')) { 297 var row = e.target.closest('.profile-link-row'); 298 if (row) row.remove(); 299 } 300 }); 301 })(); 302 303 // P63 — Profile photo picker: upload via /admin/sites/upload-photo, 304 // then write the returned URL into the visible input. Live thumb preview. 305 (function() { 306 var picker = document.getElementById('photo-picker'); 307 if (!picker) return; 308 var thumb = document.getElementById('photo-thumb'); 309 var preview = document.getElementById('photo-preview'); 310 var urlEl = document.getElementById('photo-url'); 311 var trigger = document.getElementById('photo-upload-trigger'); 312 var clear = document.getElementById('photo-clear'); 313 var field = document.getElementById('photo-upload-field'); 314 var status = document.getElementById('photo-status'); 315 316 function showPreview(url) { 317 if (url) { 318 preview.src = url; 319 preview.hidden = false; 320 thumb.removeAttribute('data-empty'); 321 clear.hidden = false; 322 } else { 323 preview.src = ''; 324 preview.hidden = true; 325 thumb.setAttribute('data-empty', ''); 326 clear.hidden = true; 327 } 328 } 329 330 if (urlEl) { 331 urlEl.addEventListener('input', function() { 332 showPreview(urlEl.value.trim()); 333 }); 334 } 335 336 if (trigger && field) { 337 trigger.addEventListener('click', function() { field.click(); }); 338 field.addEventListener('change', async function() { 339 var file = field.files && field.files[0]; 340 if (!file) return; 341 status.classList.remove('is-error'); 342 status.textContent = 'Uploaden…'; 343 try { 344 var fd = new FormData(); 345 fd.append('photo', file); 346 var r = await fetch('/admin/sites/upload-photo', { 347 method: 'POST', 348 body: fd, 349 credentials: 'same-origin', 350 }); 351 var j = await r.json(); 352 if (!r.ok || !j.ok) throw new Error(j.error || ('Upload mislukt (' + r.status + ')')); 353 urlEl.value = j.url; 354 showPreview(j.url); 355 status.textContent = 'Geüpload ✓'; 356 setTimeout(function() { status.textContent = ''; }, 2000); 357 } catch (e) { 358 status.classList.add('is-error'); 359 status.textContent = 'Mislukt: ' + e.message; 360 } finally { 361 field.value = ''; 362 } 363 }); 364 } 365 366 if (clear) { 367 clear.addEventListener('click', function() { 368 urlEl.value = ''; 369 showPreview(''); 370 status.textContent = ''; 371 // Note: doesn't delete the file from disk — saving the form with empty 372 // URL leaves the file orphaned on the server. Acceptable for now. 373 }); 374 } 375 })(); 376 </script> 257 377 258 378 259 <style> -
src/views/pages/auth-register.ejs
r792af53 r0475b13 41 41 .setup-handle-hint code { color: var(--accent); font-weight: 600; background: color-mix(in srgb, var(--accent) 12%, transparent); padding: .05rem .35rem; border-radius: 6px; white-space: nowrap; } 42 42 </style> 43 <script> 44 (function () { 45 var u = document.getElementById('setup-username'), out = document.getElementById('setup-handle'); 46 if (!u || !out) return; 47 function upd() { 48 var v = (u.value || '').toLowerCase().replace(/[^a-z0-9_-]/g, '') || 'username'; 49 out.textContent = '@' + v + '@' + location.host; 50 } 51 u.addEventListener('input', upd); 52 upd(); 53 })(); 54 </script> 43 <%# Het script van deze pagina staat in assets/js/mod/auth-register.js (shaer-bqr). %> -
src/views/pages/authorize-interaction.ejs
r792af53 r0475b13 12 12 <p class="fedi-bm-help"><%= t('fedi.bm_help') %></p> 13 13 </div> 14 <script> 15 (function () { 16 if (window.__fediBmWired) return; window.__fediBmWired = true; 17 var a = document.getElementById('fedi-bm-btn'); if (!a) return; 18 a.setAttribute('href', "javascript:void(window.open('" + location.origin + "/authorize_interaction?uri='+encodeURIComponent(window.location.href)))"); 19 a.addEventListener('click', function (e) { e.preventDefault(); a.classList.add('nudge'); setTimeout(function(){ a.classList.remove('nudge'); }, 600); }); 20 })(); 21 </script> 14 <%# Het script van deze pagina staat in assets/js/mod/authorize-interaction.js (shaer-bqr). %> 22 15 <% if (!manage.length) { %> 23 16 <p class="auth-interact-note"><%= t('fedi.manage_empty') %></p> … … 56 49 <% }); %> 57 50 </ul> 58 <script> 59 (function () { 60 if (window.__fediEditWired) return; window.__fediEditWired = true; 61 document.addEventListener('click', function (e) { 62 var b = e.target.closest && e.target.closest('.fedi-edit-btn'); 63 if (!b) return; 64 var li = b.closest('.fedi-manage-item'); if (!li) return; 65 var form = li.querySelector('.fedi-edit-form'); if (!form) return; 66 var open = form.classList.toggle('is-open'); 67 b.classList.toggle('is-open', open); 68 b.setAttribute('aria-expanded', open ? 'true' : 'false'); 69 if (open) { var ed = form.querySelector('.re-editor') || form.querySelector('textarea'); if (ed) ed.focus(); } 70 }); 71 })(); 72 </script> 51 73 52 <% } %> 74 53 <p class="auth-interact-note"><a href="/"><%= t('fedi.remote_back') %></a></p> … … 331 310 </style> 332 311 333 <script> 334 /* Like/Boost toggle in place — POST via fetch, flip the button, stay on the page. */ 335 (function(){ 336 if (window.__fediReactWired) return; window.__fediReactWired = true; 337 document.addEventListener('submit', function(e){ 338 var f = e.target.closest && e.target.closest('.fedi-react-form'); 339 if (!f) return; 340 e.preventDefault(); 341 var btn = f.querySelector('button'); if (!btn || btn.disabled) return; 342 btn.disabled = true; 343 var body = new URLSearchParams(); 344 new FormData(f).forEach(function(v, k){ body.append(k, v); }); 345 fetch(f.action, { method: 'POST', body: body, headers: { 'X-Requested-With': 'fetch' }, credentials: 'same-origin' }) 346 .then(function(r){ return r.ok ? r.json() : null; }) 347 .then(function(j){ 348 if (j) { 349 var on = !!j.on; 350 btn.classList.toggle('is-on', on); 351 var lbl = btn.querySelector('.fedi-bigact-label'); 352 if (lbl) lbl.textContent = on ? (btn.getAttribute('data-on') || lbl.textContent) : (btn.getAttribute('data-off') || lbl.textContent); 353 } 354 }) 355 .catch(function(){}) 356 .then(function(){ btn.disabled = false; }); 357 }); 358 })(); 359 </script> 312
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)