Invite-only sign-up, present mode, series, handouts, templates, dictation, progress, sharing
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>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-06 10:26:43 -04:00
1 parent 88bc59bac0
commit 54cb1c217d
73 files changed
+2739 -134

No files matched your search

+11 -6
View File
@@ -102,7 +102,7 @@ function initialChapterIndex(project) {
return index >= 0 ? index : 0;
}
export function useChunkBuilder({ project, setProject, updateProject, allChunks, setCurrentPage }) {
export function useChunkBuilder({ project, setProject, updateProject, allChunks, setCurrentPage, userTemplates = [] }) {
const { toast } = useToast();
const [setup, setSetup] = useState(() => initialSetupForm(project));
const [titleEdited, setTitleEdited] = useState(false);
@@ -138,7 +138,10 @@ export function useChunkBuilder({ project, setProject, updateProject, allChunks,
const activeChapter = project?.chapters[activeChapterIndex] ?? null;
const handleSetupField = (field, value) => {
if (field === 'book') {
if (field === 'template') {
// a template of your own is copied into the study, so it keeps working wherever the study goes
setSetup((current) => ({ ...current, template: value, templateDef: userTemplates.find((t) => t.id === value) }));
} else if (field === 'book') {
const selected = bookOptions.find((book) => book.abbrev === value) ?? bookOptions[0];
setSetup((current) => ({
...current,
@@ -195,6 +198,8 @@ export function useChunkBuilder({ project, setProject, updateProject, allChunks,
title: setup.title,
translation: setup.translation,
templateId: setup.template ?? 'blank',
...(setup.templateDef ? { templateDef: setup.templateDef } : {}), // a copy of your own template
lastEdited: Date.now(),
selectedChunkId: null,
chapters: [
@@ -237,7 +242,7 @@ export function useChunkBuilder({ project, setProject, updateProject, allChunks,
&& (c.spilloverEndVerse ?? null) === (spilloverEndVerse ?? null),
);
if (overlap) return ch;
const newChunk = blankChunk(start, end, spilloverEndVerse, current.templateId);
const newChunk = blankChunk(start, end, spilloverEndVerse, current.templateDef ?? current.templateId);
return { ...ch, chunks: [...ch.chunks, newChunk] };
});
// Find the newly added chunk id
@@ -286,7 +291,7 @@ export function useChunkBuilder({ project, setProject, updateProject, allChunks,
skippedCount += 1;
return;
}
const newChunk = blankChunk(start, end, spilloverEndVerse, current.templateId);
const newChunk = blankChunk(start, end, spilloverEndVerse, current.templateDef ?? current.templateId);
existing.add(key);
newChunks.push(newChunk);
addedCount += 1;
@@ -577,12 +582,12 @@ export function useChunkBuilder({ project, setProject, updateProject, allChunks,
if (i >= 0) {
const c = chunks[i];
chunks[i] = { ...c, endVerse: n, spilloverEndVerse: null };
chunks.splice(i + 1, 0, blankChunk(n + 1, c.endVerse, c.spilloverEndVerse ?? null, current.templateId));
chunks.splice(i + 1, 0, blankChunk(n + 1, c.endVerse, c.spilloverEndVerse ?? null, current.templateDef ?? current.templateId));
return { ...ch, chunks };
}
if (chunks.some((c) => c.startVerse <= n && n <= c.endVerse)) return ch; // already a boundary
const start = Math.max(0, ...chunks.filter((c) => c.endVerse < n).map((c) => c.endVerse)) + 1;
const created = blankChunk(start, n, null, current.templateId);
const created = blankChunk(start, n, null, current.templateDef ?? current.templateId);
selected = created.id;
const at = chunks.findIndex((c) => c.startVerse > n);
chunks.splice(at < 0 ? chunks.length : at, 0, created);