source: Klonkt/src/assets/js/mod/admin-site-edit.js@ ed5e7ab

main
Last change on this file since ed5e7ab was 0475b13, checked in by roboburr <roboburr@…>, 5 weeks ago

Acht schone pagina's uit inline script naar modules (shaer-bqr, stap 3b)

Alle acht zonder EJS-interpolatie, dus ongewijzigd te verplaatsen: admin-help,
admin-epk, admin-paid, admin-settings, auth-register, admin-push, admin-site-edit
(2 blokken) en authorize-interaction (3 blokken).

Elke route zegt nu welke module hij wil via pageJs; de bootstrap in de shell
haalt hem op. Meerdere blokken uit een pagina belanden in EEN module, gescheiden
door een streep -- ze deelden toch al een pagina en een levensduur.

Templates compileren, modules syntactisch ok, suite 565/565. Het echte bewijs
per pagina is er via een LINK naartoe gaan; na een herlading werkt alles toch al.

Commit met expliciete paden, niet met -A: eerder vandaag veegde ik daarmee
andermans werk uit dezelfde werkmap mee.

  • Property mode set to 100644
File size: 4.9 KB
Line 
1// Site bewerken in beheer (pages/admin-site-edit.ejs) -- verplaatst uit inline script, shaer-bqr.
2//
3// Inline script in een pagina wordt door de CSP geweigerd zodra je die pagina via
4// een link BINNEN de site opent: de nonce rouleert per verzoek (shaer-0i6). Dit
5// bestand wordt door de bootstrap in shell.ejs geladen en heeft dat probleem niet.
6//
7// Alles hier hoort GEDELEGEERD te luisteren (op document, niet op een element dat
8// er nu staat) en tegen een tweede aanroep te kunnen.
9
10 (function(){
11 if (window.__themePreviewWired) return; window.__themePreviewWired = true;
12 var html = document.documentElement;
13 function applyAccent(hex){
14 if(!/^#[0-9a-fA-F]{6}$/.test(hex)) return;
15 var sa = document.getElementById('pcms-site-accent');
16 if(!sa){ sa = document.createElement('style'); sa.id = 'pcms-site-accent'; document.head.appendChild(sa); }
17 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);}';
18 }
19 document.addEventListener('change', function(e){
20 var t = e.target; if(!t || !t.name) return;
21 if(t.name === 'palette'){ html.setAttribute('data-palette', t.value); }
22 else if(t.name === 'accent'){ applyAccent(t.value); }
23 else if(t.name === 'theme_override'){
24 if(t.value === 'light' || t.value === 'dark'){ html.setAttribute('data-theme', t.value); }
25 else { var dk = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; html.setAttribute('data-theme', dk ? 'dark' : 'light'); }
26 }
27 });
28 })();
29
30
31// ── volgend blok ──
32
33(function() {
34 // Slug URL field: show the real host as a dimmed prefix (the slug itself is
35 // coloured via CSS). Hub → host/user/<slug>, otherwise host/<slug>.
36 var sh = document.getElementById('slug-host');
37 if (sh) sh.textContent = location.host + (sh.dataset.prefix || '/');
38})();
39(function() {
40 // Profile-links repeater: add row from <template>, remove on click.
41 var rows = document.getElementById('profile-links-rows');
42 var tpl = document.getElementById('profile-link-template');
43 var add = document.getElementById('profile-link-add');
44 if (!rows || !tpl || !add) return;
45
46 add.addEventListener('click', function() {
47 var clone = tpl.content.cloneNode(true);
48 rows.appendChild(clone);
49 });
50 rows.addEventListener('click', function(e) {
51 if (e.target && e.target.classList.contains('pl-remove')) {
52 var row = e.target.closest('.profile-link-row');
53 if (row) row.remove();
54 }
55 });
56})();
57
58// P63 — Profile photo picker: upload via /admin/sites/upload-photo,
59// then write the returned URL into the visible input. Live thumb preview.
60(function() {
61 var picker = document.getElementById('photo-picker');
62 if (!picker) return;
63 var thumb = document.getElementById('photo-thumb');
64 var preview = document.getElementById('photo-preview');
65 var urlEl = document.getElementById('photo-url');
66 var trigger = document.getElementById('photo-upload-trigger');
67 var clear = document.getElementById('photo-clear');
68 var field = document.getElementById('photo-upload-field');
69 var status = document.getElementById('photo-status');
70
71 function showPreview(url) {
72 if (url) {
73 preview.src = url;
74 preview.hidden = false;
75 thumb.removeAttribute('data-empty');
76 clear.hidden = false;
77 } else {
78 preview.src = '';
79 preview.hidden = true;
80 thumb.setAttribute('data-empty', '');
81 clear.hidden = true;
82 }
83 }
84
85 if (urlEl) {
86 urlEl.addEventListener('input', function() {
87 showPreview(urlEl.value.trim());
88 });
89 }
90
91 if (trigger && field) {
92 trigger.addEventListener('click', function() { field.click(); });
93 field.addEventListener('change', async function() {
94 var file = field.files && field.files[0];
95 if (!file) return;
96 status.classList.remove('is-error');
97 status.textContent = 'Uploaden…';
98 try {
99 var fd = new FormData();
100 fd.append('photo', file);
101 var r = await fetch('/admin/sites/upload-photo', {
102 method: 'POST',
103 body: fd,
104 credentials: 'same-origin',
105 });
106 var j = await r.json();
107 if (!r.ok || !j.ok) throw new Error(j.error || ('Upload mislukt (' + r.status + ')'));
108 urlEl.value = j.url;
109 showPreview(j.url);
110 status.textContent = 'Geüpload ✓';
111 setTimeout(function() { status.textContent = ''; }, 2000);
112 } catch (e) {
113 status.classList.add('is-error');
114 status.textContent = 'Mislukt: ' + e.message;
115 } finally {
116 field.value = '';
117 }
118 });
119 }
120
121 if (clear) {
122 clear.addEventListener('click', function() {
123 urlEl.value = '';
124 showPreview('');
125 status.textContent = '';
126 // Note: doesn't delete the file from disk — saving the form with empty
127 // URL leaves the file orphaned on the server. Acceptable for now.
128 });
129 }
130})();
Note: See TracBrowser for help on using the repository browser.