feat: CMS-sectie Inhoud voor de systeemmanager (v0.3.84-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 56s
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 56s
- Nieuwe sectie "Inhoud" in Instellingen (groep Systeem, alleen systeemmanager, doorzoekbaar via het sectieregister) - Welkomstwoord (NL/EN) verschijnt op het inlogscherm, direct aanpasbaar zonder release - Melding voor alle gebruikers: aan/uit, niveau informatie/belangrijk, per taal; verschijnt als wegklikbare banner onder de menubalk en elke wijziging krijgt een nieuw id zodat een eerder weggeklikte melding opnieuw verschijnt - Widget-uitleg: per widget een eigen beschrijving die de standaardtekst in de widgetbibliotheek vervangt (leeg = standaard, placeholder toont de standaardtekst) - Snelkoppelingen naar de bestaande kanalen voor systeembrede inhoud: gedeelde bibliotheek (borden & ankerdiagrammen, map 🌐 Systeem) en de afbeeldingencatalogus - settings.js kreeg een generiek haakje (showSettingsDom) zodat modules met een eigen paneel zich in het sectieregister kunnen voegen - Geverifieerd in headless Chromium (15 checks): banner + dismiss + nieuw-id-gedrag, welkomstwoord, bewerken/opslaan, galerij-override, sectie onzichtbaar voor andere rollen Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
This commit is contained in:
parent
aa4c5faaf9
commit
526e4e5d52
7 changed files with 274 additions and 3 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.3.83-beta
|
||||
0.3.84-beta
|
||||
|
|
|
|||
|
|
@ -375,6 +375,30 @@
|
|||
.dtool-opts .do-wide{width:auto; min-width:48px; padding:0 9px; border-radius:12px;}
|
||||
/* slot-knop kleurt mee zodra het gereedschap vaststaat */
|
||||
.dtool-opts .do-lock.on{background:var(--accent-soft);}
|
||||
/* ---------- CMS: banner, welkomstwoord en Inhoud-sectie ---------- */
|
||||
.cms-banner{
|
||||
position:fixed; top:var(--tbar); left:0; right:0; z-index:5800;
|
||||
display:flex; gap:10px; align-items:center; justify-content:center;
|
||||
padding:9px 44px 9px 14px; font-weight:700; font-size:14px; text-align:center;
|
||||
background:var(--accent-soft); color:var(--accent-ink);
|
||||
box-shadow:0 3px 10px rgba(30,50,90,.12);
|
||||
}
|
||||
.cms-banner.warn{background:var(--orange); color:#fff;}
|
||||
.cms-banner-x{
|
||||
position:absolute; right:8px; top:50%; transform:translateY(-50%);
|
||||
width:28px; height:28px; border:none; border-radius:8px; cursor:pointer;
|
||||
background:rgba(255,255,255,.35); color:inherit; font-weight:900; line-height:1;
|
||||
}
|
||||
#cmsWelcome{
|
||||
margin:2px 0 12px; padding:10px 12px; border-radius:12px;
|
||||
background:var(--accent-soft); color:var(--accent-ink);
|
||||
font-size:13.5px; font-weight:600; line-height:1.45; white-space:pre-line;
|
||||
}
|
||||
.cms-block{display:flex; flex-direction:column; gap:8px; align-items:flex-start;}
|
||||
.cms-field{display:flex; flex-direction:column; gap:3px; width:100%;}
|
||||
.cms-ta{width:100%; min-height:64px; resize:vertical; font:inherit; padding:8px 10px;}
|
||||
.cms-row{display:flex; gap:12px; align-items:center; flex-wrap:wrap;}
|
||||
.cms-check{display:flex; align-items:center; gap:6px; font-weight:700; cursor:pointer;}
|
||||
/* live hoek-uitlezing tijdens het draaien van liniaal/geodriehoek */
|
||||
.dtool-deg{
|
||||
display:none; position:absolute; left:50%; top:50%; margin:-16px 0 0 -24px;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
<script src="js/core.js?v=__V__" defer></script>
|
||||
<script src="js/permissions.js?v=__V__" defer></script>
|
||||
<script src="js/settings.js?v=__V__" defer></script>
|
||||
<script src="js/cms.js?v=__V__" defer></script>
|
||||
<script src="js/data.js?v=__V__" defer></script>
|
||||
<script src="js/explorer.js?v=__V__" defer></script>
|
||||
<script src="js/picker.js?v=__V__" defer></script>
|
||||
|
|
|
|||
|
|
@ -1127,7 +1127,8 @@ function renderGallery(){
|
|||
defs.forEach(def=>{const c=document.createElement("div");c.className="gcard";c.tabIndex=0;
|
||||
const icon=document.createElement("div");icon.className="gi";icon.textContent=def.icon;
|
||||
const name=document.createElement("div");name.className="gn";name.textContent=T("wg_"+def.id);
|
||||
const desc=document.createElement("div");desc.className="gd";desc.textContent=T("wg_"+def.id+"_d");
|
||||
/* uitleg: eigen tekst van de systeemmanager (CMS) gaat vóór de standaard */
|
||||
const desc=document.createElement("div");desc.className="gd";desc.textContent=(typeof CMS!=="undefined"&&CMS.widgetInfo&&CMS.widgetInfo[def.id]&&(LANG==="en"?CMS.widgetInfo[def.id].en:CMS.widgetInfo[def.id].nl))||T("wg_"+def.id+"_d");
|
||||
const star=document.createElement("button");star.className="gstar";star.type="button";star.textContent=favorites.includes(def.id)?"★":"☆";star.title=T("galleryFavorite");
|
||||
star.onclick=e=>{e.stopPropagation();favorites=favorites.includes(def.id)?favorites.filter(x=>x!==def.id):[...favorites,def.id];localStorage.setItem("teachWidgetFavorites",JSON.stringify(favorites));renderGallery()};
|
||||
const add=()=>{galleryWrap.classList.remove("open");makeWidget(def)};c.onclick=add;c.onkeydown=e=>{if(e.key==="Enter"||e.key===" "){e.preventDefault();add()}};c.append(star,icon,name,desc);grid.appendChild(c)});gallery.appendChild(grid);
|
||||
|
|
|
|||
221
public/js/cms.js
Normal file
221
public/js/cms.js
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
/* 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(def.icon + " " + 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:32,
|
||||
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();
|
||||
})();
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"use strict";
|
||||
/* version — shown until /api/version resolves (or if the fetch fails, e.g. offline).
|
||||
Kept in sync by hand with the VERSION file at the repo root on every release. */
|
||||
const VERSION = "0.3.83-beta";
|
||||
const VERSION = "0.3.84-beta";
|
||||
(function(){
|
||||
const tag = document.getElementById("verTag");
|
||||
tag.textContent = "v"+VERSION;
|
||||
|
|
@ -211,6 +211,17 @@ const I18N = {
|
|||
stVoortgangSub:"Volg per leerling de resultaten in de tijd.",
|
||||
stScholenSub:"Scholen en systeemmanagers, over alle scholen heen.",
|
||||
stOpslag:"Opslag", stOpslagSub:"Afbeeldingencatalogus en opslagruimte per school.",
|
||||
stInhoud:"Inhoud", stInhoudSub:"Welkomstwoord, meldingen en widget-uitleg - direct aanpasbaar, zonder release.",
|
||||
cmsWelcomeH:"Welkomstwoord (inlogscherm)",
|
||||
cmsAnnounceH:"Melding voor alle gebruikers",
|
||||
cmsWidgetH:"Widget-uitleg (bibliotheek)",
|
||||
cmsActive:"Melding tonen", cmsLevelInfo:"informatie", cmsLevelWarn:"belangrijk",
|
||||
cmsSaved:"Opgeslagen ✓", cmsNl:"Nederlands", cmsEn:"English",
|
||||
cmsWidgetPick:"— kies een widget… —",
|
||||
cmsWidgetHint:"Leeg laten = de standaarduitleg.",
|
||||
cmsShortcutsH:"Systeembrede inhoud",
|
||||
cmsShortcutsHint:"Ankerdiagrammen en borden deel je via de gedeelde bibliotheek (map 🌐 Systeem in de verkenner); afbeeldingen via de catalogus. Iedereen kan ze daar kopiëren.",
|
||||
cmsOpenShared:"🌐 Borden & ankerdiagrammen", cmsOpenImages:"🖼 Afbeeldingencatalogus",
|
||||
stBackups:"Reservekopieën", backupsNote:"Automatisch bewaarde eerdere versies van je borden en woorden.",
|
||||
backupsEmpty:"Nog geen reservekopieën.", backupRestore:"herstel", backupRestoreConfirm:"zeker?",
|
||||
backupRestored:"Versie hersteld ✓",
|
||||
|
|
@ -489,6 +500,17 @@ const I18N = {
|
|||
stVoortgangSub:"Follow each pupil's results over time.",
|
||||
stScholenSub:"Schools and system managers, across all schools.",
|
||||
stOpslag:"Storage", stOpslagSub:"Image catalogue and storage per school.",
|
||||
stInhoud:"Content", stInhoudSub:"Welcome text, announcements and widget descriptions - editable without a release.",
|
||||
cmsWelcomeH:"Welcome text (login screen)",
|
||||
cmsAnnounceH:"Announcement for all users",
|
||||
cmsWidgetH:"Widget descriptions (library)",
|
||||
cmsActive:"Show announcement", cmsLevelInfo:"information", cmsLevelWarn:"important",
|
||||
cmsSaved:"Saved ✓", cmsNl:"Nederlands", cmsEn:"English",
|
||||
cmsWidgetPick:"— pick a widget… —",
|
||||
cmsWidgetHint:"Leave empty for the default description.",
|
||||
cmsShortcutsH:"System-wide content",
|
||||
cmsShortcutsHint:"Share anchor diagrams and boards through the shared library (🌐 System folder in the explorer); images through the catalogue. Everyone can copy them from there.",
|
||||
cmsOpenShared:"🌐 Boards & anchor diagrams", cmsOpenImages:"🖼 Image catalogue",
|
||||
stBackups:"Backups", backupsNote:"Automatically kept earlier versions of your boards and words.",
|
||||
backupsEmpty:"No backups yet.", backupRestore:"restore", backupRestoreConfirm:"sure?",
|
||||
backupRestored:"Version restored ✓",
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ function registerSettingsSection(sec){ ST_SECTIONS.push(sec); }
|
|||
});
|
||||
}
|
||||
window.showAdminHost = ()=>showDomSection("admin");
|
||||
/* modules met een eigen paneel (bv. cms.js) tonen hun sectie-div hiermee */
|
||||
window.showSettingsDom = showDomSection;
|
||||
|
||||
const GROUPS = [["personal","stGroupPersonal"], ["school","stGroupSchool"], ["system","stGroupSystem"]];
|
||||
function buildNav(){
|
||||
|
|
|
|||
Loading…
Reference in a new issue