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

main
Last change on this file since 6401681 was 6401681, checked in by Robin <roboburr@…>, 4 weeks ago

Het paneel hoort bij wie je bent, niet bij je bestanden (Robins correctie)

Verhuisd van Beheer -> Audio naar het profielscherm van de site-eigenaar, en
daar staat het NAAST de aliassen -- want dat is dezelfde vraag. Een alias zegt
wie je elders in de fediverse bent, een MBID zegt wie je in het muziekregister
bent. Bij de audiobestanden ging het over beheer van spullen; hier gaat het over
identiteit, en daar hoort het thuis.

DE VERHUIZING VERANDERDE DE VORM, en dat is de moeite waard om te weten. Het
profielscherm IS een formulier, en een formulier in een formulier bestaat niet
in HTML. Dus geen eigen opslaan-knop meer: de keuze zet een verborgen veld en
gaat mee met de Opslaan van de pagina, precies zoals de profielfoto. Dat leest
ook beter -- een halve wijziging die je nog kunt herzien voor je opslaat.

Ontkoppelen is nu het veld leegmaken, geen aparte route. Minder oppervlak.

Het zoek-endpoint ging mee naar /admin/sites/:slug/api/musicbrainz. Een endpoint
onder /admin/audio laten staan voor een scherm in /admin/sites is precies zo'n
naad die later niemand meer kan plaatsen. De teksten heten nu asite.mb_* in
plaats van aaud.mb_*, in alle drie de talen, met een placeholder erbij.

Wat NIET veranderde: de zoekopdracht draait server-side (hun regel geldt per
applicatie), wij kiezen niet voor de artiest, en alleen een echte MBID komt de
kolom in.

Co-Authored-By: Claude Opus 5 <noreply@…>

  • Property mode set to 100644
File size: 8.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// Element-bedrading leeft zo lang als de pagina; de bootstrap roept init()
11// aan bij elke paginawissel waarop deze module actief is (shaer-5s1).
12export function init() { run(); }
13
14function run() {
15 (function(){
16 if (window.__themePreviewWired) return; window.__themePreviewWired = true;
17 var html = document.documentElement;
18 function applyAccent(hex){
19 if(!/^#[0-9a-fA-F]{6}$/.test(hex)) return;
20 var sa = document.getElementById('pcms-site-accent');
21 if(!sa){ sa = document.createElement('style'); sa.id = 'pcms-site-accent'; document.head.appendChild(sa); }
22 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);}';
23 }
24 document.addEventListener('change', function(e){
25 var t = e.target; if(!t || !t.name) return;
26 if(t.name === 'palette'){ html.setAttribute('data-palette', t.value); }
27 else if(t.name === 'accent'){ applyAccent(t.value); }
28 else if(t.name === 'theme_override'){
29 if(t.value === 'light' || t.value === 'dark'){ html.setAttribute('data-theme', t.value); }
30 else { var dk = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; html.setAttribute('data-theme', dk ? 'dark' : 'light'); }
31 }
32 });
33 })();
34
35
36// ── volgend blok ──
37
38(function() {
39 // Slug URL field: show the real host as a dimmed prefix (the slug itself is
40 // coloured via CSS). Hub → host/user/<slug>, otherwise host/<slug>.
41 var sh = document.getElementById('slug-host');
42 if (sh) sh.textContent = location.host + (sh.dataset.prefix || '/');
43})();
44(function() {
45 // Profile-links repeater: add row from <template>, remove on click.
46 var rows = document.getElementById('profile-links-rows');
47 var tpl = document.getElementById('profile-link-template');
48 var add = document.getElementById('profile-link-add');
49 if (!rows || !tpl || !add) return;
50
51 add.addEventListener('click', function() {
52 var clone = tpl.content.cloneNode(true);
53 rows.appendChild(clone);
54 });
55 rows.addEventListener('click', function(e) {
56 if (e.target && e.target.classList.contains('pl-remove')) {
57 var row = e.target.closest('.profile-link-row');
58 if (row) row.remove();
59 }
60 });
61})();
62
63// P63 — Profile photo picker: upload via /admin/sites/upload-photo,
64// then write the returned URL into the visible input. Live thumb preview.
65(function() {
66 var picker = document.getElementById('photo-picker');
67 if (!picker) return;
68 var thumb = document.getElementById('photo-thumb');
69 var preview = document.getElementById('photo-preview');
70 var urlEl = document.getElementById('photo-url');
71 var trigger = document.getElementById('photo-upload-trigger');
72 var clear = document.getElementById('photo-clear');
73 var field = document.getElementById('photo-upload-field');
74 var status = document.getElementById('photo-status');
75
76 function showPreview(url) {
77 if (url) {
78 preview.src = url;
79 preview.hidden = false;
80 thumb.removeAttribute('data-empty');
81 clear.hidden = false;
82 } else {
83 preview.src = '';
84 preview.hidden = true;
85 thumb.setAttribute('data-empty', '');
86 clear.hidden = true;
87 }
88 }
89
90 if (urlEl) {
91 urlEl.addEventListener('input', function() {
92 showPreview(urlEl.value.trim());
93 });
94 }
95
96 if (trigger && field) {
97 trigger.addEventListener('click', function() { field.click(); });
98 field.addEventListener('change', async function() {
99 var file = field.files && field.files[0];
100 if (!file) return;
101 status.classList.remove('is-error');
102 status.textContent = 'Uploaden…';
103 try {
104 var fd = new FormData();
105 fd.append('photo', file);
106 var r = await fetch('/admin/sites/upload-photo', {
107 method: 'POST',
108 body: fd,
109 credentials: 'same-origin',
110 });
111 var j = await r.json();
112 if (!r.ok || !j.ok) throw new Error(j.error || ('Upload mislukt (' + r.status + ')'));
113 urlEl.value = j.url;
114 showPreview(j.url);
115 status.textContent = 'Geüpload ✓';
116 setTimeout(function() { status.textContent = ''; }, 2000);
117 } catch (e) {
118 status.classList.add('is-error');
119 status.textContent = 'Mislukt: ' + e.message;
120 } finally {
121 field.value = '';
122 }
123 });
124 }
125
126 if (clear) {
127 clear.addEventListener('click', function() {
128 urlEl.value = '';
129 showPreview('');
130 status.textContent = '';
131 // Note: doesn't delete the file from disk — saving the form with empty
132 // URL leaves the file orphaned on the server. Acceptable for now.
133 });
134 }
135})();
136
137 // MusicBrainz-paneel (shaer-mbz). Binnen run(), niet op moduleniveau: deze
138 // modules krijgen bij elke paginawissel opnieuw init(), en een blok dat maar
139 // een keer per sessie draait is precies wat shaer-5s1 opleverde.
140 wireMusicBrainz();
141}
142
143// ── MusicBrainz: ben jij dit? (shaer-mbz) ─────────────────────────
144//
145// De kandidaten komen van de server, want MusicBrainz eist een verzoek per
146// seconde per APPLICATIE en een User-Agent met contact -- allebei niet vanuit
147// een browser af te dwingen.
148//
149// WIJ KIEZEN NIET. Ook niet als er precies een treffer is: een verkeerd geraden
150// MBID zet jouw naam onder andermans werk. De knop staat er, de klik is van de
151// artiest. En de keuze gaat mee met de Opslaan-knop van de pagina -- een eigen
152// formulier kan hier niet, want deze pagina IS er een.
153function wireMusicBrainz() {
154 const knop = document.getElementById('mb-zoek-btn');
155 const veld = document.getElementById('mb-q');
156 const uit = document.getElementById('mb-uit');
157 const idVeld = document.getElementById('mb-id');
158 const naamVeld = document.getElementById('mb-naam');
159 const huidig = document.getElementById('mb-huidig');
160 if (!knop || !veld || !uit || !idVeld || knop.__wired) return;
161 knop.__wired = true;
162
163 const el = (tag, cls, tekst) => {
164 const e = document.createElement(tag);
165 if (cls) e.className = cls;
166 // textContent en nooit innerHTML: dit is tekst uit een vreemd register.
167 if (tekst != null) e.textContent = tekst;
168 return e;
169 };
170
171 function zet(mbid, naam) {
172 idVeld.value = mbid || '';
173 naamVeld.value = naam || '';
174 if (huidig) {
175 huidig.hidden = !mbid;
176 const n = document.getElementById('mb-huidig-naam');
177 const a = document.getElementById('mb-huidig-link');
178 if (n) n.textContent = naam || mbid || '';
179 if (a) a.href = mbid ? `https://musicbrainz.org/artist/${mbid}` : '#';
180 }
181 uit.hidden = true;
182 uit.replaceChildren();
183 }
184
185 const wis = document.getElementById('mb-wis');
186 if (wis) wis.addEventListener('click', () => zet('', ''));
187
188 async function zoek() {
189 const q = (veld.value || '').trim();
190 if (!q) return;
191 uit.hidden = false;
192 uit.replaceChildren(el('p', 'form-hint', T.asite_mb_busy || 'Zoeken…'));
193 knop.disabled = true;
194 try {
195 const r = await fetch(`api/musicbrainz?q=${encodeURIComponent(q)}`, { credentials: 'same-origin' });
196 const j = await r.json();
197 toon((j && j.kandidaten) || []);
198 } catch {
199 uit.replaceChildren(el('p', 'form-hint', T.asite_mb_fail || 'MusicBrainz is even niet bereikbaar.'));
200 } finally {
201 knop.disabled = false;
202 }
203 }
204
205 function toon(kandidaten) {
206 if (!kandidaten.length) {
207 uit.replaceChildren(el('p', 'form-hint', T.asite_mb_none || 'Niets gevonden.'));
208 return;
209 }
210 const lijst = el('ul', 'mb-lijst');
211 for (const k of kandidaten) {
212 const li = el('li', 'mb-kandidaat');
213 li.appendChild(el('strong', null, k.naam));
214 // De toelichting is het hele punt: er zijn drie bands die Nirvana heten,
215 // en zonder dit veld kiest iemand de verkeerde.
216 const bij = [k.toelichting, k.soort, k.land, k.jaren].filter(Boolean).join(' · ');
217 if (bij) li.appendChild(el('small', 'mb-bij', bij));
218 const open = el('a', 'mb-open', T.asite_mb_open || 'Bekijk op MusicBrainz');
219 open.href = k.url; open.target = '_blank'; open.rel = 'noopener';
220 li.appendChild(open);
221 const b = el('button', 'btn', T.asite_mb_pick || 'Dit ben ik');
222 b.type = 'button';
223 b.addEventListener('click', () => zet(k.mbid, k.naam));
224 li.appendChild(b);
225 lijst.appendChild(li);
226 }
227 uit.replaceChildren(lijst);
228 }
229
230 knop.addEventListener('click', zoek);
231 veld.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); zoek(); } });
232}
Note: See TracBrowser for help on using the repository browser.