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:
@@ -0,0 +1,52 @@
|
||||
const db = require('../db/db');
|
||||
|
||||
// Recursive deep merge: values from `patch` override `base`, objects are merged
|
||||
function deepMerge(base, patch) {
|
||||
const result = Object.assign({}, base);
|
||||
for (const key of Object.keys(patch)) {
|
||||
if (
|
||||
patch[key] !== null &&
|
||||
typeof patch[key] === 'object' &&
|
||||
!Array.isArray(patch[key]) &&
|
||||
typeof result[key] === 'object' &&
|
||||
result[key] !== null &&
|
||||
!Array.isArray(result[key])
|
||||
) {
|
||||
result[key] = deepMerge(result[key], patch[key]);
|
||||
} else {
|
||||
result[key] = patch[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/* ── GET /api/preferences ────────────────────────────────────────────────── */
|
||||
function getPreferences(req, res) {
|
||||
const row = db.prepare('SELECT data FROM user_preferences WHERE user_id = ?').get(req.user.id);
|
||||
res.json(JSON.parse(row?.data || '{}'));
|
||||
}
|
||||
|
||||
/* ── PATCH /api/preferences ──────────────────────────────────────────────── */
|
||||
function patchPreferences(req, res) {
|
||||
if (!req.body || typeof req.body !== 'object' || Array.isArray(req.body)) {
|
||||
return res.status(400).json({ error: 'Body must be a JSON object' });
|
||||
}
|
||||
const current = JSON.parse(
|
||||
db.prepare('SELECT data FROM user_preferences WHERE user_id = ?').get(req.user.id)?.data || '{}'
|
||||
);
|
||||
const merged = deepMerge(current, req.body);
|
||||
db.prepare(`
|
||||
INSERT INTO user_preferences (user_id, data, updated_at)
|
||||
VALUES (?, ?, datetime('now'))
|
||||
ON CONFLICT(user_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at
|
||||
`).run(req.user.id, JSON.stringify(merged));
|
||||
res.json(merged);
|
||||
}
|
||||
|
||||
/* ── DELETE /api/preferences ─────────────────────────────────────────────── */
|
||||
function resetPreferences(req, res) {
|
||||
db.prepare('DELETE FROM user_preferences WHERE user_id = ?').run(req.user.id);
|
||||
res.json({});
|
||||
}
|
||||
|
||||
module.exports = { getPreferences, patchPreferences, resetPreferences };
|
||||
@@ -2898,4 +2898,13 @@ db.exec(`
|
||||
)
|
||||
`);
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_geo_subs_task ON geometry_submissions(task_id)');
|
||||
|
||||
// User preferences (server-synced: whiteboard defaults, dashboard visibility, etc.)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS user_preferences (
|
||||
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
data TEXT NOT NULL DEFAULT '{}',
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_geo_subs_student ON geometry_submissions(student_id)');
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const { getPreferences, patchPreferences, resetPreferences } = require('../controllers/preferencesController');
|
||||
|
||||
router.get('/', authMiddleware, getPreferences);
|
||||
router.patch('/', authMiddleware, patchPreferences);
|
||||
router.delete('/', authMiddleware, resetPreferences);
|
||||
|
||||
module.exports = router;
|
||||
@@ -143,6 +143,7 @@ app.use('/api/bookmarks', bookmarkRoutes);
|
||||
app.use('/api/search', searchRoutes);
|
||||
app.use('/api/flashcards', flashcardRoutes);
|
||||
app.use('/api/settings', settingsRoutes);
|
||||
app.use('/api/preferences', require('./routes/preferences'));
|
||||
app.use('/api/analytics', analyticsRoutes);
|
||||
app.use('/api/live', liveRoutes);
|
||||
app.use('/api/classroom/guest', guestClassroomRoutes); // public — MUST be before /api/classroom
|
||||
|
||||
Reference in New Issue
Block a user