All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 33s
- Voortgang: tijdlijn toont 30 regels per keer met een "meer tonen (n)"-knop i.p.v. 200 regels ineens - Inhoud: de vier blokken (welkomstwoord, melding, widget-uitleg, systeembrede inhoud) zijn inklapbare groepskaarten - alleen het welkomstwoord staat standaard open; open-status overleeft her-renders - Account: de drie wachtwoordvelden verschijnen pas na een klik op de kop "Wachtwoord" - Daarmee volgt elke sectie hetzelfde principe: compacte koppen met samenvatting, detail pas op verzoek, verwante items in één kader - Geverifieerd in headless Chromium (9 nieuwe checks) + volledige regressie van de vier bestaande suites (56 checks) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
243 lines
9.6 KiB
JavaScript
243 lines
9.6 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(); });
|
|
|
|
/* welke Inhoud-blokken openstaan (welkom standaard); overleeft re-renders */
|
|
const cmsOpen = new Set(["welcome"]);
|
|
function cmsCard(key, titleKey, build){
|
|
const card = cmsEl("div", "am-card");
|
|
const head = cmsEl("div", "am-card-head am-card-toggle");
|
|
const title = cmsEl("span", "am-card-title", T(titleKey));
|
|
const arrow = cmsEl("span", "am-card-sum");
|
|
head.appendChild(title);
|
|
head.appendChild(arrow);
|
|
const body = cmsEl("div", "cms-card-body");
|
|
build(body);
|
|
const sync = ()=>{
|
|
const open = cmsOpen.has(key);
|
|
arrow.textContent = open ? "▾" : "▸";
|
|
body.style.display = open ? "" : "none";
|
|
};
|
|
head.addEventListener("click", ()=>{
|
|
if(cmsOpen.has(key)) cmsOpen.delete(key); else cmsOpen.add(key);
|
|
sync();
|
|
});
|
|
sync();
|
|
card.appendChild(head);
|
|
card.appendChild(body);
|
|
return card;
|
|
}
|
|
|
|
/* ---- 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 */
|
|
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(cmsCard("welcome", "cmsWelcomeH", b=>b.appendChild(wBlock)));
|
|
|
|
/* update-melding */
|
|
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(cmsCard("announcement", "cmsAnnounceH", b=>b.appendChild(aBlock)));
|
|
|
|
/* widget-uitleg */
|
|
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(cmsCard("widgets", "cmsWidgetH", b=>b.appendChild(gBlock)));
|
|
|
|
/* systeembrede inhoud: ankerdiagrammen/borden en afbeeldingen */
|
|
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(cmsCard("shortcuts", "cmsShortcutsH", b=>b.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();
|
|
})();
|