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

+9 -5
View File
@@ -14,7 +14,7 @@ const primaryBtn = 'rounded-xl bg-[var(--r-accent)] px-5 py-2.5 text-sm font-med
// Band tints that alternate from chunk to chunk so neighbouring chunks are easy to tell apart.
const BANDS = ['color-mix(in srgb, var(--r-accent) 13%, transparent)', 'color-mix(in srgb, var(--r-sub) 17%, transparent)'];
function PassageForm({ setup, onField, onTitleChange, onLoad, loading, error, withTitle }) {
function PassageForm({ setup, onField, onTitleChange, onLoad, loading, error, withTitle, userTemplates = [], onDeleteTemplate }) {
return (
<div className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
@@ -35,8 +35,12 @@ function PassageForm({ setup, onField, onTitleChange, onLoad, loading, error, wi
<label className="block text-sm sm:col-span-2">Start from
<select value={setup.template ?? 'blank'} onChange={(e) => onField('template', e.target.value)} className={inputCls}>
{STUDY_TEMPLATES.map((t) => <option key={t.id} value={t.id}>{t.name}</option>)}
{userTemplates.length > 0 && <optgroup label="Your templates">{userTemplates.map((t) => <option key={t.id} value={t.id}>{t.name}</option>)}</optgroup>}
</select>
<span className="mt-1 block text-xs text-[var(--r-sub)]">{getTemplate(setup.template).description}</span>
<span className="mt-1 block text-xs text-[var(--r-sub)]">{getTemplate(setup.template, userTemplates).description}</span>
{setup.template?.startsWith('user-') && onDeleteTemplate && (
<button type="button" onClick={() => { onDeleteTemplate(setup.template); onField('template', 'blank'); }} className="mt-1 text-xs text-[var(--r-danger,#b42318)] hover:underline">Delete this template</button>
)}
</label>
)}
</div>
@@ -49,8 +53,8 @@ function PassageForm({ setup, onField, onTitleChange, onLoad, loading, error, wi
}
export default function SetupPage() {
const { project, setProject, allChunks, updateProject, setCurrentPage } = useApp();
const b = useChunkBuilder({ project, setProject, updateProject, allChunks, setCurrentPage });
const { project, setProject, allChunks, updateProject, setCurrentPage, userTemplates, deleteUserTemplate } = useApp();
const b = useChunkBuilder({ project, setProject, updateProject, allChunks, setCurrentPage, userTemplates });
const { activeChapter, activeChapterIndex } = b;
const chapters = project?.chapters ?? [];
@@ -73,7 +77,7 @@ export default function SetupPage() {
<h1 className="mb-1 text-xl font-medium">New study</h1>
<p className="mb-5 text-sm text-[var(--r-sub)]">Pick a chapter, then cut it into chunks. You’ll study one chunk at a time.</p>
<PassageForm setup={b.setup} onField={b.handleSetupField} onTitleChange={onTitleChange} onLoad={b.handleLoadChapter}
loading={b.loadingChapter} error={b.errorMessage} withTitle />
loading={b.loadingChapter} error={b.errorMessage} withTitle userTemplates={userTemplates} onDeleteTemplate={deleteUserTemplate} />
</section>
)}