Files
study-bible-project/server/readerHistory.js
T
nmemmertandClaude Sonnet 5.5 54cb1c217d
Build and Push Docker Image / build-and-push (push) Successful in 6m47s
Invite-only sign-up, present mode, series, handouts, templates, dictation, progress, sharing
- Admin: sign-up Open / Invite only / Closed with single-use invite links
- Present mode for scripts (auto-scroll) and outlines (step through, pace vs planned minutes)
- Series: group studies on the dashboard, filter by series, calendar file for a study's sessions
- Lesson handout: printable page per session (passage, outline, questions, writing lines)
- Save a chunk's notes as your own study template (synced)
- Voice dictation for notes and scripts (browser speech recognition)
- Reading progress: map of the 66 books and a calendar of days read, synced and merged across devices
- Export highlights and bookmarks as Markdown or Word, grouped by named colour
- Verse of the day push to the ntfy topic (needs IQBIBLE_API_KEY)
- Sharing: co-teacher editing via single-use invite links; read-only links can expire and take a passcode

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 10:26:43 -04:00

33 lines
1.5 KiB
JavaScript

// What a person has read in the Reader: which chapters (how many times, and when last) and on which days.
// Merging two copies never loses anything, so two devices can both add to it: each chapter keeps the larger
// count and the later date, and the days are combined. The data comes from the browser, so it is rebuilt field by field.
const KEY = /^[1-3A-Z]{3}\.\d{1,3}$/;
const DATE = /^\d{4}-\d{2}-\d{2}$/;
const MAX_DAYS = 5000;
export const emptyHistory = () => ({ chapters: {}, days: [] });
export function cleanHistory(raw) {
const chapters = {};
if (raw?.chapters && typeof raw.chapters === 'object' && !Array.isArray(raw.chapters)) {
for (const [key, v] of Object.entries(raw.chapters).slice(0, 1300)) {
if (!KEY.test(key) || !v || typeof v !== 'object') continue;
const n = Number.isInteger(v.n) ? Math.min(9999, Math.max(1, v.n)) : 1;
chapters[key] = { n, last: DATE.test(v.last ?? '') ? v.last : '' };
}
}
const days = Array.isArray(raw?.days) ? [...new Set(raw.days.filter((d) => typeof d === 'string' && DATE.test(d)))].sort().slice(-MAX_DAYS) : [];
return { chapters, days };
}
export function mergeHistory(a, b) {
const x = cleanHistory(a);
const y = cleanHistory(b);
const chapters = { ...x.chapters };
for (const [key, v] of Object.entries(y.chapters)) {
const mine = chapters[key];
chapters[key] = mine ? { n: Math.max(mine.n, v.n), last: mine.last > v.last ? mine.last : v.last } : v;
}
return { chapters, days: [...new Set([...x.days, ...y.days])].sort().slice(-MAX_DAYS) };
}