Build and Push Docker Image / build-and-push (push) Successful in 6m47s
- 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>
33 lines
1.5 KiB
JavaScript
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) };
|
|
}
|