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:
nmemmertandClaude Sonnet 5 committed 2026-09-23 12:51:28 -04:00
1 parent f1026dc194
commit 600b3d3ab9
21 files changed
+1166 -159

No files matched your search

+34
View File
@@ -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.
+30
View File
@@ -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
// ---------------------------------------------------------------------------