Invite-only sign-up, present mode, series, handouts, templates, dictation, progress, sharing
Build and Push Docker Image / build-and-push (push) Successful in 6m47s
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:
1 parent
88bc59bac0
commit
54cb1c217d
73 files changed
+2739
-134
No files matched your search
@@ -1,7 +1,8 @@
|
||||
// Observe / Interpret / Apply as an accordion (one open at a time), plus a "More"
|
||||
// area for background notes and tags so the page stays quiet.
|
||||
import { useEffect, useState } from 'react';
|
||||
import { templatePrompt } from '../../utils/studyTemplates.js';
|
||||
import DictateButton, { appendSpoken } from '../DictateButton.jsx';
|
||||
import { templateFromChunk, templatePrompt } from '../../utils/studyTemplates.js';
|
||||
|
||||
const SECTIONS = [
|
||||
{ field: 'observation', label: 'Observe', hint: 'What does the text say?',
|
||||
@@ -15,11 +16,13 @@ const SECTIONS = [
|
||||
const areaCls = 'w-full resize-y rounded-xl border border-[var(--r-line)] bg-[var(--r-bg)] px-3.5 py-3 font-serif text-[15px] leading-7 text-[var(--r-text)] placeholder:font-sans placeholder:text-[13px] placeholder:leading-5 placeholder:text-[var(--r-sub)] focus:outline-none focus:ring-2 focus:ring-[var(--r-line)]';
|
||||
const words = (s) => (s ?? '').trim().split(/\s+/).filter(Boolean).length;
|
||||
|
||||
export default function NotesPane({ chunk, updateChunk, allTags, templateId }) {
|
||||
export default function NotesPane({ chunk, updateChunk, allTags, templateId, saveTemplate, notify }) {
|
||||
const [open, setOpen] = useState(() => {
|
||||
try { return localStorage.getItem('studyNotesOpen') || 'observation'; } catch { return 'observation'; }
|
||||
});
|
||||
const [more, setMore] = useState(false);
|
||||
const [tplName, setTplName] = useState('');
|
||||
const [tplOpen, setTplOpen] = useState(false);
|
||||
const [tagInput, setTagInput] = useState('');
|
||||
useEffect(() => { setTagInput(''); }, [chunk.id]);
|
||||
useEffect(() => { try { localStorage.setItem('studyNotesOpen', open || ''); } catch { /* storage unavailable */ } }, [open]);
|
||||
@@ -53,6 +56,7 @@ export default function NotesPane({ chunk, updateChunk, allTags, templateId }) {
|
||||
<div className="px-4 pb-4">
|
||||
<textarea aria-label={label} value={chunk[field] ?? ''} rows={7} placeholder={templatePrompt(templateId, field) ?? placeholder}
|
||||
onChange={(e) => updateChunk(chunk.id, { [field]: e.target.value })} className={areaCls} />
|
||||
<div className="mt-1.5"><DictateButton label={`Dictate ${label.toLowerCase()}`} onText={(said) => updateChunk(chunk.id, { [field]: appendSpoken(chunk[field], said) })} /></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -71,7 +75,34 @@ export default function NotesPane({ chunk, updateChunk, allTags, templateId }) {
|
||||
Background / general notes
|
||||
<textarea value={chunk.generalNotes ?? ''} rows={5} onChange={(e) => updateChunk(chunk.id, { generalNotes: e.target.value })}
|
||||
placeholder="e.g. author, date, audience, historical context, key themes…" className={`mt-1 ${areaCls}`} />
|
||||
<span className="mt-1 block"><DictateButton label="Dictate background" onText={(said) => updateChunk(chunk.id, { generalNotes: appendSpoken(chunk.generalNotes, said) })} /></span>
|
||||
</label>
|
||||
<label className="block text-xs text-[var(--r-sub)]">
|
||||
Discussion questions <span className="opacity-70">(one per line; printed on the lesson handout)</span>
|
||||
<textarea value={chunk.discussionQuestions ?? ''} rows={4} onChange={(e) => updateChunk(chunk.id, { discussionQuestions: e.target.value })}
|
||||
placeholder={'What does this passage say about God?\nWhere do you see this in your own life?'} className={`mt-1 ${areaCls}`} />
|
||||
</label>
|
||||
{saveTemplate && (
|
||||
<div className="rounded-xl border border-dashed border-[var(--r-line)] p-3">
|
||||
{!tplOpen ? (
|
||||
<button type="button" onClick={() => setTplOpen(true)} className="text-xs text-[var(--r-accent)] hover:underline">Save this chunk’s notes as a template…</button>
|
||||
) : (
|
||||
<div>
|
||||
<p className="mb-2 text-xs text-[var(--r-sub)]">What’s written in this chunk’s note boxes becomes the starting text of each new chunk in studies made from the template, along with this study’s prompts.</p>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<input type="text" aria-label="Template name" value={tplName} onChange={(e) => setTplName(e.target.value)} maxLength={40} placeholder="Template name"
|
||||
className="min-w-0 flex-1 rounded-lg border border-[var(--r-line)] bg-[var(--r-bg)] px-2.5 py-1.5 text-sm" />
|
||||
<button type="button" onClick={() => {
|
||||
const made = templateFromChunk({ name: tplName, chunk, template: templateId });
|
||||
if (!made) { notify?.('Give it a name, and write something in a note box first.', 2600); return; }
|
||||
saveTemplate(made); setTplName(''); setTplOpen(false); notify?.(`Saved the template “${made.name}”. Pick it when you start a new study.`, 3200);
|
||||
}} className="rounded-lg border border-[var(--r-line)] px-3 py-1.5 text-xs hover:bg-black/5">Save template</button>
|
||||
<button type="button" onClick={() => setTplOpen(false)} className="text-xs text-[var(--r-sub)] hover:underline">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="mb-1.5 text-xs text-[var(--r-sub)]">Tags</p>
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
|
||||
Reference in new issue
Block a user