feat: user preferences sync — server-side storage, whiteboard defaults, dashboard widget visibility

- New table `user_preferences` (user_id PK, JSON blob, updated_at)
- GET/PATCH/DELETE /api/preferences with deep-merge UPSERT
- LS.prefs singleton in api.js: dot-notation get/set, debounced flush (1.5s), server sync
- classroom.html: load wb.color/width/lineStyle/theme from prefs on init; save on change
- dashboard.html: widget configurator panel (gear button) — toggle visibility per-user, persisted server-side

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-04-14 20:17:25 +03:00
parent ba20a76839
commit 89ba25cd20
7 changed files with 292 additions and 6 deletions
+35
View File
@@ -4072,6 +4072,36 @@
// Sync board theme selector to whiteboard default
{ const sel = document.getElementById('wb-theme-select'); if (sel) sel.value = 'chalkboard'; }
// Apply saved whiteboard defaults from user preferences
if (canEdit && LS.prefs) {
LS.prefs.init().then(() => {
const wb = LS.prefs.get('wb', {});
if (wb.color) {
_wb.setColor(wb.color);
document.querySelectorAll('.cr-color-btn').forEach(b => b.classList.remove('active'));
const match = document.querySelector(`.cr-color-btn[data-color="${wb.color}"]`);
if (match) match.classList.add('active');
}
if (wb.width) {
_wb.setWidth(wb.width);
document.querySelectorAll('.cr-width-btn').forEach(b => b.classList.remove('active'));
const wBtn = document.getElementById(`wb-w${wb.width}`);
if (wBtn) wBtn.classList.add('active');
}
if (wb.lineStyle) {
_wb.setLineStyle(wb.lineStyle);
document.querySelectorAll('.cr-linestyle-btn').forEach(b => b.classList.remove('active'));
const lBtn = document.getElementById(`wb-ls-${wb.lineStyle}`);
if (lBtn) lBtn.classList.add('active');
}
if (wb.theme) {
_wb.setBoardTheme(wb.theme);
const sel = document.getElementById('wb-theme-select');
if (sel) sel.value = wb.theme;
}
}).catch(() => {});
}
// show/hide toolbar, thumbs panel, and readonly label
document.getElementById('cr-toolbar').style.display = canEdit ? 'flex' : 'none';
document.getElementById('wb-thumbs-panel').style.display = isTeacher ? 'flex' : 'none';
@@ -4817,6 +4847,7 @@
if (activeTools.some(t => document.getElementById(`wb-tool-${t}`)?.classList.contains('active')))
wbSetTool('pencil');
else wbUpdateCursorStyle();
if (LS.prefs) LS.prefs.set('wb.color', color);
}
function wbSetWidth(px, btn) {
@@ -4825,6 +4856,7 @@
document.querySelectorAll('.cr-width-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
wbUpdateCursorStyle();
if (LS.prefs) LS.prefs.set('wb.width', px);
}
function wbSetLineStyle(style, btn) {
@@ -4832,6 +4864,7 @@
_wb.setLineStyle(style);
document.querySelectorAll('.cr-linestyle-btn').forEach(b => b.classList.remove('active'));
if (btn) btn.classList.add('active');
if (LS.prefs) LS.prefs.set('wb.lineStyle', style);
}
function wbSetOpacity(val) {
@@ -5478,6 +5511,7 @@
// Update selector if called programmatically
const sel = document.getElementById('wb-theme-select');
if (sel) sel.value = theme;
if (LS.prefs) LS.prefs.set('wb.theme', theme);
}
/* ── Page context menu ── */
@@ -6434,6 +6468,7 @@
setTimeout(() => { if (input) input.style.outline = ''; }, 300);
if (['eraser','laser'].some(t => document.getElementById(`wb-tool-${t}`)?.classList.contains('active')))
wbSetTool('pencil');
if (LS.prefs) LS.prefs.set('wb.color', input.value);
}
function wbToggleFullscreen() {