Voeg thematische kleurplaten en creatieve niveaus toe (v0.4.65-beta) #3

Open
bes-r wants to merge 207 commits from bes-r/kleurplaten-v0-4-65 into main AGit
6 changed files with 191 additions and 72 deletions
Showing only changes of commit e555876fa4 - Show all commits

View file

@ -1 +1 @@
0.4.14-beta
0.4.15-beta

View file

@ -135,7 +135,8 @@
#pupilView{display:none;}
body.pupil-mode #board,body.pupil-mode #fab,body.pupil-mode #fabBoardsBtn,
body.pupil-mode #boardStrip,body.pupil-mode #fabmenu,
body.pupil-mode #hud,body.pupil-mode #btnFolders,body.pupil-mode #btnSave{display:none !important;}
body.pupil-mode #hud,body.pupil-mode #btnFolders,body.pupil-mode #btnSave,
body.pupil-mode #btnCast{display:none !important;}
body.pupil-mode #pupilView{
display:flex; flex-wrap:wrap; gap:18px; align-content:flex-start;
position:absolute; inset:var(--tbar) 0 0 0; overflow:auto; padding:18px; box-sizing:border-box;
@ -160,7 +161,8 @@
#parentView{display:none;}
body.parent-mode #board,body.parent-mode #fab,body.parent-mode #fabBoardsBtn,
body.parent-mode #boardStrip,body.parent-mode #fabmenu,
body.parent-mode #hud,body.parent-mode #btnFolders,body.parent-mode #btnSave{display:none !important;}
body.parent-mode #hud,body.parent-mode #btnFolders,body.parent-mode #btnSave,
body.parent-mode #btnCast{display:none !important;}
body.parent-mode #parentView{
display:block; position:absolute; inset:var(--tbar) 0 0 0;
overflow:auto; padding:24px 18px; box-sizing:border-box;
@ -1316,12 +1318,16 @@
font-size:11.5px; font-weight:700; text-transform:uppercase; letter-spacing:.4px;
color:var(--muted); margin:14px 0 6px;
}
.am-person{cursor:pointer;}
.am-person,.am-target{cursor:pointer;}
.am-person-info{flex:1; display:flex; flex-direction:column; min-width:0; gap:1px;}
.am-person-info .am-name{font-size:14px;}
.am-person-info .am-role{font-size:11.5px; color:var(--muted); font-weight:600;}
.am-sheet-backdrop{position:fixed; inset:0; background:rgba(15,22,38,.4); z-index:60; display:none;}
.am-sheet-backdrop.open{display:block;}
/* de Cast-sheet opent vanaf het bord zelf (buiten Instellingen om), dus moet
boven de zwevende pen-HUD (#hud, z-index 5500) uitkomen - zelfde laag als
.modalwrap (6000), niet de 60 die volstaat zodra je al in Instellingen zit */
#castBackdrop{z-index:6000;}
.am-sheet{
position:absolute; left:0; right:0; bottom:0; max-width:520px; margin:0 auto;
background:var(--surface); border-radius:18px 18px 0 0; padding:16px 18px 24px;
@ -1456,6 +1462,7 @@
}
button.am-chip{cursor:pointer; color:var(--muted);}
button.am-chip.on{border-color:var(--accent); background:var(--accent-soft); color:var(--accent-ink);}
.am-chip.active{border-color:var(--green); background:color-mix(in srgb, var(--green) 15%, var(--surface)); color:var(--green);}
.am-chipx{border:none; background:transparent; color:var(--red); font-weight:900; cursor:pointer; padding:0; font-size:11px;}
.am-reset{
background:var(--surface-2); border:1px solid var(--line); border-radius:12px;

View file

@ -43,6 +43,7 @@
<script src="js/pupil.js?v=__V__" defer></script>
<script src="js/parent.js?v=__V__" defer></script>
<script src="js/admin.js?v=__V__" defer></script>
<script src="js/cast.js?v=__V__" defer></script>
<script src="js/image-catalog.js?v=__V__" defer></script>
<script src="js/app.js?v=__V__" defer></script>
</head>
@ -61,6 +62,7 @@
<button class="tbtn ghost" id="btnImages">🖼️</button>
<button class="tbtn ghost" id="btnFolders">📁</button>
<button class="tbtn ghost" id="btnSave">💾 <span data-i18n="saveBoard"></span></button>
<button class="tbtn ghost" id="btnCast" title="Cast">📡</button>
<button class="tbtn ghost" id="btnUser">⚙️ <span id="userLbl"></span></button>
<button class="tbtn ghost" id="btnFs" title="Fullscreen"></button>
</div>

View file

@ -621,19 +621,9 @@
return box;
}
/* toewijzingsrij: kies een van je eigen borden (uit BS, board.js) voor een klas of
een individuele leerling. Geen momentopname - de leerling-pagina zoekt het bord
live op, dus nieuwe woorden die je toevoegt komen vanzelf door zonder opnieuw
toe te wijzen. Een expliciete "Toewijzen"-knop i.p.v. direct bij het kiezen van
een bord opslaan, zodat een klas-brede wijziging niet per ongeluk gebeurt. */
/* widgets van een eigen bord (voor de één-widget-keuze in assignmentRow);
widgets zonder wid komen uit borden van vóór v0.3.13 en verschijnen pas
na de eerstvolgende save van dat bord */
function boardWidgets(boardId){
let found = null;
BS.folders.forEach(fo=>fo.boards.forEach(bo=>{ if(bo.id===boardId) found = bo; }));
return ((found && found.data && found.data.widgets) || []).filter(w=>w.wid);
}
/* toewijzingsrij: overzicht van de huidige toewijzing (aangemaakt via de
Cast-knop op het whiteboard, cast.js) + loskoppelen. Aanmaken/wijzigen
gebeurt niet meer hier - zie cast.js. */
function describeCurrent(current){
if(!current) return "";
let s = `${T("amAssignedAs")}${current.boardName || current.boardId}`;
@ -643,56 +633,23 @@
}
return s;
}
function assignmentRow(target, boardOptions, current){
const sel = h("select","am-sel");
sel.appendChild(new Option(T("amNoAssignment"), ""));
boardOptions.forEach(b=>sel.appendChild(new Option(b.label, b.id)));
sel.value = current ? current.boardId : "";
/* keuze: heel bord (default, bestaand gedrag) of één widget + modus */
const widgetSel = h("select","am-sel am-widget-sel");
const modeSel = h("select","am-sel am-mode-sel");
modeSel.appendChild(new Option(T("amModeWork"), "werken"));
modeSel.appendChild(new Option(T("amModeWatch"), "kijken"));
const syncWidgetSel = ()=>{
widgetSel.innerHTML = "";
widgetSel.appendChild(new Option(T("amWholeBoard"), ""));
boardWidgets(sel.value).forEach(w=>widgetSel.appendChild(new Option(T("wg_"+w.id), w.wid)));
widgetSel.value = (current && current.boardId===sel.value && current.widgetId) ? current.widgetId : "";
widgetSel.style.display = sel.value ? "" : "none";
syncModeSel();
};
const syncModeSel = ()=>{ modeSel.style.display = (sel.value && widgetSel.value) ? "" : "none"; };
modeSel.value = current && current.mode ? current.mode : "werken";
sel.addEventListener("change", syncWidgetSel);
widgetSel.addEventListener("change", syncModeSel);
const info = h("span","am-code", describeCurrent(current));
syncWidgetSel();
const apply = h("button","am-btn", T("amAssign"));
apply.type = "button";
apply.addEventListener("click", async ()=>{
try{
if(sel.value){
const body = { boardId: sel.value };
if(widgetSel.value){ body.widgetId = widgetSel.value; body.mode = modeSel.value; }
await api(`/assignments/${target.type}/${target.id}`, {method:"PUT", body});
}
else await api(`/assignments/${target.type}/${target.id}`, {method:"DELETE"});
if(target.type==="class"){ await reload(); }
else{
const a = (await api("/assignments/pupil/"+target.id)).assignment;
info.textContent = describeCurrent(a);
msg(T("amAssignSaved"));
}
}catch(e){ msg(e.message); }
});
/* kop toont compact de huidige stand; de keuzes klappen uit via Bewerken */
function assignmentRow(target, current){
const info = h("span","am-code", describeCurrent(current) || T("amNoAssignment"));
const tools = h("div","am-tools");
const inline = h("div","am-inline");
inline.appendChild(sel);
inline.appendChild(widgetSel);
inline.appendChild(modeSel);
inline.appendChild(apply);
tools.appendChild(inline);
if(current){
const del = dangerBtn(T("amUnlink"), null, async ()=>{
try{
await api(`/assignments/${target.type}/${target.id}`, {method:"DELETE"});
if(target.type==="class"){ await reload(); }
else{
info.textContent = T("amNoAssignment");
del.remove();
msg(T("amAssignSaved"));
}
}catch(e){ msg(e.message); }
});
tools.appendChild(del);
}
tools.appendChild(info);
return groupCard({
title: target.label,
@ -730,13 +687,10 @@
panel.appendChild(h("div","guestnote", T("amPickSchoolHint")));
return;
}
const boardOptions = [];
BS.folders.forEach(fo=>fo.boards.forEach(bo=>boardOptions.push({ id: bo.id, label: `${fo.name} / ${bo.name}` })));
panel.appendChild(h("div","am-group", T("amAssignClasses")));
const classList = h("div","am-list");
if(!CLASSES.length) classList.appendChild(h("div","guestnote", T("amNoClass")));
CLASSES.forEach(c=>classList.appendChild(assignmentRow({type:"class", id:c.id, label:c.name}, boardOptions, ASSIGNMENTS[c.id])));
CLASSES.forEach(c=>classList.appendChild(assignmentRow({type:"class", id:c.id, label:c.name}, ASSIGNMENTS[c.id])));
panel.appendChild(classList);
panel.appendChild(h("div","am-group", T("amAssignPupils")));
@ -752,7 +706,7 @@
const u = pupils.find(x=>String(x.id)===pupilSel.value);
let current = null;
try{ current = (await api("/assignments/pupil/"+u.id)).assignment; }catch(e){}
pupilRowHolder.appendChild(assignmentRow({type:"pupil", id:u.id, label:u.displayName}, boardOptions, current));
pupilRowHolder.appendChild(assignmentRow({type:"pupil", id:u.id, label:u.displayName}, current));
});
}

146
public/js/cast.js Normal file
View file

@ -0,0 +1,146 @@
/* teach - cast (📡): het geopende bord (of één widget erop) rechtstreeks
vanaf de toolbar uitzenden naar een klas of leerling, i.p.v. de omweg via
Instellingen > Toewijzingen. Hergebruikt dezelfde PUT/DELETE-routes als
admin.js's assignmentRow - geen serverwijziging, alleen een korter pad. */
"use strict";
const btnCast = document.getElementById("btnCast");
btnCast.title = T("cast");
document.addEventListener("langchange", ()=>{
btnCast.title = T("cast");
if(castOpen) renderCast();
});
function h(tag, cls, text){
const el = document.createElement(tag);
if(cls) el.className = cls;
if(text != null) el.textContent = text;
return el;
}
const castBackdrop = h("div","am-sheet-backdrop");
castBackdrop.id = "castBackdrop";
document.body.appendChild(castBackdrop);
castBackdrop.addEventListener("click", e=>{ if(e.target===castBackdrop) closeCast(); });
let castOpen = false;
let castLoading = false;
let castClasses = [], castPupils = [];
let castSelWidget = "", castSelMode = "werken";
/* toewijzingen die deze sessie al gelukt zijn ("type:id"), zodat je hetzelfde
bord naar meerdere klassen/leerlingen achter elkaar kunt casten zonder dat
de sheet na elke keer sluit */
let castActiveKeys = new Set();
let castError = "";
/* widgets van het live-open bord, zelfde bron als serializeBoard() (board.js) */
function currentBoardWidgets(){
return [...instances].map(inst=>({ wid: inst.wid, id: inst.def.id })).filter(w=>w.wid);
}
btnCast.addEventListener("click", async ()=>{
stashCurrent(); /* verse widget-state, zelfde als btnFolders doet */
castOpen = true;
castSelWidget = ""; castSelMode = "werken";
castActiveKeys = new Set(); castError = "";
castLoading = true;
renderCast();
try{
const [cRes, uRes] = await Promise.all([ api("/admin/classes"), api("/admin/users") ]);
castClasses = cRes.classes || [];
castPupils = (uRes.users || []).filter(u=>u.role==="pupil");
}catch(e){
castClasses = []; castPupils = []; castError = e.message;
}
castLoading = false;
renderCast();
});
function closeCast(){ castOpen = false; renderCast(); }
function castTargetRow(target){
const row = h("div","am-row am-target");
const info = h("div","am-person-info");
info.appendChild(h("span","am-name", target.label));
if(target.sub) info.appendChild(h("span","am-role", target.sub));
row.appendChild(info);
const key = target.type+":"+target.id;
if(castActiveKeys.has(key)) row.appendChild(h("span","am-chip active", T("castActive")));
row.addEventListener("click", async ()=>{
try{
const body = { boardId: curSlot().id };
if(castSelWidget){ body.widgetId = castSelWidget; body.mode = castSelMode; }
await api(`/assignments/${target.type}/${target.id}`, { method:"PUT", body });
castActiveKeys.add(key);
castError = "";
}catch(e){ castError = e.message; }
renderCast();
});
return row;
}
function renderCast(){
castBackdrop.classList.toggle("open", castOpen);
castBackdrop.innerHTML = "";
if(!castOpen) return;
const sheet = h("div","am-sheet");
castBackdrop.appendChild(sheet);
const head = h("div","am-kebab-row");
const titleWrap = h("div");
titleWrap.appendChild(h("h2","am-nav-title", T("castTitle")));
titleWrap.appendChild(h("div","am-role", `${BS.folders[BS.f].name} / ${curSlot().name}`));
head.appendChild(titleWrap);
const closeBtn = h("button","am-btn", T("close"));
closeBtn.type = "button";
closeBtn.addEventListener("click", closeCast);
head.appendChild(closeBtn);
sheet.appendChild(head);
sheet.appendChild(h("div","am-msg", castError));
sheet.appendChild(h("div","am-group", T("castWhat")));
const widgetSel = h("select","am-sel");
widgetSel.appendChild(new Option(T("amWholeBoard"), ""));
currentBoardWidgets().forEach(w=>widgetSel.appendChild(new Option(T("wg_"+w.id), w.wid)));
widgetSel.value = castSelWidget;
const modeSel = h("select","am-sel");
modeSel.appendChild(new Option(T("amModeWork"), "werken"));
modeSel.appendChild(new Option(T("amModeWatch"), "kijken"));
modeSel.value = castSelMode;
modeSel.style.display = castSelWidget ? "" : "none";
widgetSel.addEventListener("change", ()=>{
castSelWidget = widgetSel.value;
modeSel.style.display = castSelWidget ? "" : "none";
});
modeSel.addEventListener("change", ()=>{ castSelMode = modeSel.value; });
const whatRow = h("div","am-inline");
whatRow.appendChild(widgetSel);
whatRow.appendChild(modeSel);
sheet.appendChild(whatRow);
sheet.appendChild(h("div","am-group", T("castClasses")));
const classList = h("div","am-list");
if(castLoading) classList.appendChild(h("div","guestnote", "…"));
else if(!castClasses.length) classList.appendChild(h("div","guestnote", T("castNoClasses")));
else castClasses.forEach(c=>classList.appendChild(castTargetRow({type:"class", id:c.id, label:c.name})));
sheet.appendChild(classList);
sheet.appendChild(h("div","am-group", T("castPupils")));
const pupilList = h("div","am-list");
if(castLoading) pupilList.appendChild(h("div","guestnote", "…"));
else if(!castPupils.length) pupilList.appendChild(h("div","guestnote", T("castNoPupils")));
else{
const byClass = new Map();
castPupils.forEach(u=>{
const key = u.classId || "none";
if(!byClass.has(key)) byClass.set(key, []);
byClass.get(key).push(u);
});
castClasses.forEach(c=>(byClass.get(c.id) || []).forEach(u=>
pupilList.appendChild(castTargetRow({type:"pupil", id:u.id, label:u.displayName, sub:c.name}))));
(byClass.get("none") || []).forEach(u=>
pupilList.appendChild(castTargetRow({type:"pupil", id:u.id, label:u.displayName, sub:T("amUnassigned")})));
}
sheet.appendChild(pupilList);
}

View file

@ -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.4.14-beta";
const VERSION = "0.4.15-beta";
(function(){
const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION;
@ -315,7 +315,12 @@ const I18N = {
amAssignments:"Toewijzingen", amAssignClasses:"Per klas (standaard)", amAssignPupils:"Per leerling (uitzondering)",
amPickPupil:"— kies een leerling —", amNoAssignment:"— geen toewijzing —",
amAssignedAs:"Toegewezen:", amAssign:"Toewijzen", amAssignSaved:"Toewijzing opgeslagen.",
amUnlink:"Loskoppelen",
amWholeBoard:"— heel bord —", amModeWatch:"kijken", amModeWork:"werken",
cast:"Cast", castTitle:"Cast bord", castWhat:"Wat cast je?",
castClasses:"Klassen", castPupils:"Leerlingen",
castNoClasses:"Geen klassen in deze school.", castNoPupils:"Geen leerlingen in deze school.",
castActive:"✓ Actief",
amProgress:"Voortgang", amProgressEmpty:"Nog geen voortgang voor deze leerling.",
amConfirmDel:"Weet je zeker dat je deze gebruiker wilt verwijderen?",
amCopy:"kopieer", amCopyAll:"kopieer alles",
@ -617,7 +622,12 @@ const I18N = {
amAssignments:"Assignments", amAssignClasses:"Per class (default)", amAssignPupils:"Per pupil (override)",
amPickPupil:"— pick a pupil —", amNoAssignment:"— no assignment —",
amAssignedAs:"Assigned:", amAssign:"Assign", amAssignSaved:"Assignment saved.",
amUnlink:"Unlink",
amWholeBoard:"— whole board —", amModeWatch:"watch", amModeWork:"work",
cast:"Cast", castTitle:"Cast board", castWhat:"What are you casting?",
castClasses:"Classes", castPupils:"Pupils",
castNoClasses:"No classes in this school.", castNoPupils:"No pupils in this school.",
castActive:"✓ Active",
amProgress:"Progress", amProgressEmpty:"No progress for this pupil yet.",
amConfirmDel:"Are you sure you want to delete this user?",
amCopy:"copy", amCopyAll:"copy all",