teach/public/js/cms.js
Ramon ebe622588f
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 45s
style: tekstknoppen en consistente secties in het beheer - emoji-sweep afgerond (v0.3.87-beta)
- Alle resterende emoji uit het beheer: kopieerknoppen zijn tekstknoppen ("kopieer"), wachtwoord-genereerknop heet "genereer", "alles kopiëren" zonder klembord-icoon, schoolrijen zonder 🏫, contextbalk-label is het woord "School", bevestiging is "zeker?" (de rode armed-stijl is het signaal), kopieer-feedback een net vinkje
- Scholen-tabblad: "Bewerken"-tekstknoppen i.p.v. het ✏-potlood; zoek-placeholders zonder vergrootglas-emoji
- CMS-widgetkeuze zonder emoji-prefix in de opties
- Playwright-emoji-scan over alle vijf beheersecties (knoppen/labels/groepen schoon, ▸▾✕✓+ toegestaan) + volledige master-detail-regressie (19 checks) groen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-17 07:08:17 +02:00

221 lines
8.7 KiB
JavaScript

/* teach - CMS van de systeemmanager: site-inhoud die zonder release aanpasbaar
is. Drie sleutels (server: site_content, routes in api.js):
welcome - welkomstwoord op het inlogscherm, per taal;
announcement - melding/banner voor alle gebruikers (aan/uit, info of
belangrijk, per taal; elke wijziging krijgt een nieuw id
zodat weggeklikte meldingen opnieuw verschijnen);
widgetInfo - per widget een eigen uitleg die de standaardtekst in de
widgetgalerij vervangt (per taal, leeg = standaard).
Ankerdiagrammen/borden deelt de systeemmanager via de bestaande gedeelde
bibliotheek (map 🌐 Systeem in de verkenner) en afbeeldingen via de
catalogus - de Inhoud-sectie linkt daar rechtstreeks naartoe. */
"use strict";
let CMS = {};
(function(){
const DISMISS_KEY = "teach.dismissedAnnouncement";
const cmsEl = (tag, cls, text)=>{
const e = document.createElement(tag);
if(cls) e.className = cls;
if(text != null) e.textContent = text;
return e;
};
const inLang = v => v ? ((LANG==="en" ? v.en : v.nl) || v.nl || v.en || "") : "";
async function loadContent(){
try{ CMS = await api("/content") || {}; }catch(e){ CMS = {}; }
renderAnnouncement();
renderWelcome();
document.dispatchEvent(new CustomEvent("cmschange"));
}
/* ---- banner (update-melding) voor iedereen -------------------------------- */
let banner = null;
function renderAnnouncement(){
if(banner){ banner.remove(); banner = null; }
const a = CMS.announcement;
const text = a && a.active ? inLang(a) : "";
if(!text) return;
let dismissed = null;
try{ dismissed = localStorage.getItem(DISMISS_KEY); }catch(e){}
if(dismissed === String(a.id)) return;
banner = cmsEl("div", "cms-banner" + (a.level==="warn" ? " warn" : ""));
banner.appendChild(cmsEl("span", null, text));
const x = cmsEl("button", "cms-banner-x", "✕");
x.type = "button";
x.title = T("close");
x.addEventListener("click", ()=>{
try{ localStorage.setItem(DISMISS_KEY, String(a.id)); }catch(e){}
banner.remove(); banner = null;
});
banner.appendChild(x);
document.body.appendChild(banner);
}
/* ---- welkomstwoord op het inlogscherm -------------------------------------- */
function renderWelcome(){
let box = document.getElementById("cmsWelcome");
const text = inLang(CMS.welcome);
if(!text){ if(box) box.remove(); return; }
if(!box){
box = cmsEl("div");
box.id = "cmsWelcome";
document.querySelector("#loginModal .login-logo").after(box);
}
box.textContent = text;
}
document.addEventListener("langchange", ()=>{ renderAnnouncement(); renderWelcome(); });
/* ---- Inhoud-sectie in Instellingen (alleen systeemmanager) ----------------- */
const host = cmsEl("div", "st-section");
host.dataset.section = "inhoud";
host.style.display = "none";
document.getElementById("stContent").appendChild(host);
async function save(key, value, msgEl){
try{
await api("/admin/content/"+key, { method:"PUT", body:value });
CMS[key] = value;
msgEl.textContent = T("cmsSaved");
msgEl.classList.add("ok");
renderAnnouncement();
renderWelcome();
document.dispatchEvent(new CustomEvent("cmschange"));
}catch(e){
msgEl.textContent = e.message;
msgEl.classList.remove("ok");
}
}
const langArea = (labelKey, value)=>{
const wrap = cmsEl("label", "cms-field");
wrap.appendChild(cmsEl("span", "am-field-lbl", T(labelKey)));
const ta = document.createElement("textarea");
ta.className = "am-inp cms-ta";
ta.maxLength = 2000;
ta.value = value || "";
ta.addEventListener("keydown", ev=>ev.stopPropagation());
wrap.appendChild(ta);
wrap._ta = ta;
return wrap;
};
function renderPanel(){
host.innerHTML = "";
/* welkomstwoord */
host.appendChild(cmsEl("div", "am-h3", T("cmsWelcomeH")));
const wMsg = cmsEl("div", "formMsg");
const wNl = langArea("cmsNl", CMS.welcome && CMS.welcome.nl);
const wEn = langArea("cmsEn", CMS.welcome && CMS.welcome.en);
const wSave = cmsEl("button", "am-btn", T("save"));
wSave.type = "button";
wSave.addEventListener("click", ()=>save("welcome", { nl: wNl._ta.value.trim(), en: wEn._ta.value.trim() }, wMsg));
const wBlock = cmsEl("div", "am-reset cms-block");
[wNl, wEn, wSave, wMsg].forEach(n=>wBlock.appendChild(n));
host.appendChild(wBlock);
/* update-melding */
host.appendChild(cmsEl("div", "am-h3", T("cmsAnnounceH")));
const a = CMS.announcement || {};
const aMsg = cmsEl("div", "formMsg");
const activeLbl = cmsEl("label", "cms-check");
const active = document.createElement("input");
active.type = "checkbox";
active.checked = !!a.active;
activeLbl.appendChild(active);
activeLbl.appendChild(cmsEl("span", null, " " + T("cmsActive")));
const level = document.createElement("select");
level.className = "am-sel";
level.appendChild(new Option(T("cmsLevelInfo"), "info"));
level.appendChild(new Option(T("cmsLevelWarn"), "warn"));
level.value = a.level === "warn" ? "warn" : "info";
const aNl = langArea("cmsNl", a.nl);
const aEn = langArea("cmsEn", a.en);
const aSave = cmsEl("button", "am-btn", T("save"));
aSave.type = "button";
aSave.addEventListener("click", ()=>{
/* nieuw id per wijziging: weggeklikte meldingen verschijnen dan opnieuw */
save("announcement", { active: active.checked, level: level.value,
nl: aNl._ta.value.trim(), en: aEn._ta.value.trim(), id: Date.now() }, aMsg);
});
const aBlock = cmsEl("div", "am-reset cms-block");
const aRow = cmsEl("div", "cms-row");
aRow.appendChild(activeLbl);
aRow.appendChild(level);
[aRow, aNl, aEn, aSave, aMsg].forEach(n=>aBlock.appendChild(n));
host.appendChild(aBlock);
/* widget-uitleg */
host.appendChild(cmsEl("div", "am-h3", T("cmsWidgetH")));
const gBlock = cmsEl("div", "am-reset cms-block");
const gMsg = cmsEl("div", "formMsg");
const pick = document.createElement("select");
pick.className = "am-sel";
pick.appendChild(new Option(T("cmsWidgetPick"), ""));
REGISTRY.forEach(def=>pick.appendChild(new Option(T("wg_"+def.id), def.id)));
const editor = cmsEl("div");
editor.style.display = "none";
pick.addEventListener("change", ()=>{
editor.innerHTML = "";
gMsg.textContent = "";
if(!pick.value){ editor.style.display = "none"; return; }
const id = pick.value;
const cur = (CMS.widgetInfo || {})[id] || {};
const gNl = langArea("cmsNl", cur.nl);
const gEn = langArea("cmsEn", cur.en);
gNl._ta.placeholder = I18N.nl["wg_"+id+"_d"] || "";
gEn._ta.placeholder = I18N.en["wg_"+id+"_d"] || "";
const hint = cmsEl("div", "guestnote", T("cmsWidgetHint"));
const gSave = cmsEl("button", "am-btn", T("save"));
gSave.type = "button";
gSave.addEventListener("click", ()=>{
const info = { ...(CMS.widgetInfo || {}) };
const nl = gNl._ta.value.trim(), en = gEn._ta.value.trim();
if(nl || en) info[id] = { nl, en };
else delete info[id];
save("widgetInfo", info, gMsg);
});
[gNl, gEn, hint, gSave].forEach(n=>editor.appendChild(n));
editor.style.display = "";
});
gBlock.appendChild(pick);
gBlock.appendChild(editor);
gBlock.appendChild(gMsg);
host.appendChild(gBlock);
/* systeembrede inhoud: ankerdiagrammen/borden en afbeeldingen */
host.appendChild(cmsEl("div", "am-h3", T("cmsShortcutsH")));
const sBlock = cmsEl("div", "am-reset cms-block");
sBlock.appendChild(cmsEl("div", "guestnote", T("cmsShortcutsHint")));
const shared = cmsEl("button", "tbtn ghost", T("cmsOpenShared"));
shared.type = "button";
shared.addEventListener("click", ()=>{
document.getElementById("settingsWrap").classList.remove("open");
document.getElementById("btnFolders").click();
});
const imgs = cmsEl("button", "tbtn ghost", T("cmsOpenImages"));
imgs.type = "button";
imgs.addEventListener("click", ()=>{
document.getElementById("settingsWrap").classList.remove("open");
openImageCatalog();
});
const sRow = cmsEl("div", "cms-row");
sRow.appendChild(shared);
sRow.appendChild(imgs);
sBlock.appendChild(sRow);
host.appendChild(sBlock);
}
registerSettingsSection({
id:"inhoud", icon:"📢", group:"system", order:31,
label:()=>T("stInhoud"), sub:()=>T("stInhoudSub"),
visible:()=>!!currentUser && can("content.manage"),
activate(){
renderPanel();
showSettingsDom("inhoud");
},
keywords:()=>[T("cmsWelcomeH"), T("cmsAnnounceH"), T("cmsWidgetH"),
"welkom","welcome","melding","announcement","banner","uitleg","cms","tekst","text"],
});
loadContent();
})();