Add reading plans: catalog, progress tracking, and custom plans
Replaces the old single-book reading goal with a full Reading Plans page: 9 built-in plans (Whole Bible, OT/NT, topical) with cover photos and color-themed cards, day-by-day checklists synced to the server per user, inline "mark today read" in the Bible reader, and user-created plans with their own book picker and an optional uploaded cover photo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f1026dc194
commit
600b3d3ab9
21 files changed
+1166
-159
No files matched your search
@@ -81,6 +81,16 @@ export function initDb() {
|
||||
);
|
||||
`);
|
||||
|
||||
// Reading plan progress — one row per user, holding the whole list of
|
||||
// in-progress/completed plans as JSON (small; no need to normalize).
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS reading_progress (
|
||||
user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
data TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
console.log(`SQLite database ready at ${DB_PATH}`);
|
||||
}
|
||||
|
||||
@@ -182,6 +192,30 @@ export function setReaderInkPage(userId, bookAbbrev, chapter, strokes) {
|
||||
`).run(userId, bookAbbrev, Number(chapter), JSON.stringify(strokes), Date.now());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Reading plan progress — cross-device sync for started/completed reading plans
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Returns the user's reading-plan data ({ progress, customPlans }, parsed JSON), or an empty shell. */
|
||||
export function getReadingProgress(userId) {
|
||||
const row = db.prepare('SELECT data FROM reading_progress WHERE user_id = ?').get(userId);
|
||||
if (!row) return { progress: [], customPlans: [] };
|
||||
try {
|
||||
return JSON.parse(row.data);
|
||||
} catch {
|
||||
return { progress: [], customPlans: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/** Overwrites the user's whole reading-plan progress list. */
|
||||
export function setReadingProgress(userId, progress) {
|
||||
db.prepare(`
|
||||
INSERT INTO reading_progress (user_id, data, updated_at)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at
|
||||
`).run(userId, JSON.stringify(progress), Date.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns any pre-existing, unowned projects (from before multi-user support)
|
||||
* to the given user. Intended to run once, right after the first account is created.
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
adminGetAllUsers, adminDeleteUser, adminGetAllProjects, adminGetProject, adminDeleteProject,
|
||||
setUserPassword, destroyAllSessionsForUser,
|
||||
getAllReaderInk, setReaderInkPage,
|
||||
getReadingProgress, setReadingProgress,
|
||||
} from './db.js';
|
||||
import { SqliteSessionStore } from './sessionStore.js';
|
||||
import {
|
||||
@@ -394,6 +395,35 @@ app.put('/api/reader/ink/:book/:chapter', requireAuth, (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Reading plan progress — cross-device sync for started/completed reading plans
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// GET /api/reading-plans — the current user's whole progress list
|
||||
app.get('/api/reading-plans', requireAuth, (req, res) => {
|
||||
try {
|
||||
res.json(getReadingProgress(req.session.userId));
|
||||
} catch (err) {
|
||||
console.error('GET /api/reading-plans error:', err);
|
||||
res.status(500).json({ error: 'Failed to load reading plan progress.' });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/reading-plans — overwrite the current user's whole progress list
|
||||
// (an object: { progress: [...], customPlans: [...] })
|
||||
app.put('/api/reading-plans', requireAuth, (req, res) => {
|
||||
try {
|
||||
if (!req.body || typeof req.body !== 'object' || Array.isArray(req.body)) {
|
||||
return res.status(400).json({ error: 'Body must be an object.' });
|
||||
}
|
||||
setReadingProgress(req.session.userId, req.body);
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
console.error('PUT /api/reading-plans error:', err);
|
||||
res.status(500).json({ error: 'Failed to save reading plan progress.' });
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Read-only share links
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user