From eccc5e509d03da973c581eed796bf4fcf4ce58fb Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:51:49 -0400 Subject: [PATCH] Studying: daily five minutes, reading list, plan-day studies use your template - Home: "Your five minutes": a rotating Observe/Interpret/Apply prompt on the passage whose matching note is thinnest in your own words, a 5:00 clock and a box that appends to that note. Pasting is blocked; sessions show in Insights. - Study: a Reading list tab (R) for the study: books and articles to consult, with a note and a tick; merges across devices. - Starting a study from a reading-plan day now uses the notes template you last picked when starting a study by hand (logic moved to utils/planStudy.js). Co-Authored-By: Claude Sonnet 5.5 --- src/App.jsx | 25 ++-------- src/components/DailyFive.jsx | 66 +++++++++++++++++++++++++ src/components/DailyFive.test.jsx | 46 +++++++++++++++++ src/components/ShortcutsHelp.jsx | 2 +- src/components/study/ReadingListTab.jsx | 32 ++++++++++++ src/components/study/ResourcePane.jsx | 7 ++- src/hooks/useChunkBuilder.js | 4 +- src/hooks/useProjectLibrary.js | 12 +++-- src/pages/HomePage.jsx | 2 + src/pages/InsightsPage.jsx | 2 +- src/pages/StudyPage.jsx | 6 +-- src/test/App.home.test.jsx | 12 +++++ src/utils/dailyFive.js | 53 ++++++++++++++++++++ src/utils/dailyFive.test.js | 42 ++++++++++++++++ src/utils/insights.js | 1 + src/utils/insights.test.js | 5 ++ src/utils/mergeStudies.js | 3 +- src/utils/mergeStudies.test.js | 12 +++++ src/utils/planStudy.js | 33 +++++++++++++ src/utils/planStudy.test.js | 43 ++++++++++++++++ 20 files changed, 376 insertions(+), 32 deletions(-) create mode 100644 src/components/DailyFive.jsx create mode 100644 src/components/DailyFive.test.jsx create mode 100644 src/components/study/ReadingListTab.jsx create mode 100644 src/utils/dailyFive.js create mode 100644 src/utils/dailyFive.test.js create mode 100644 src/utils/planStudy.js create mode 100644 src/utils/planStudy.test.js diff --git a/src/App.jsx b/src/App.jsx index 10ffb59..14f634e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -11,6 +11,7 @@ import Tooltips from './components/Tooltips.jsx'; import { useAudio } from './hooks/useAudio.js'; import { useAuth } from './hooks/useAuth.js'; import { useBibleReader } from './hooks/useBibleReader.js'; +import { lastTemplateId, planStudyProject } from './utils/planStudy.js'; import { isAudienceWindow } from './utils/presentSync.js'; import { useProjectLibrary } from './hooks/useProjectLibrary.js'; import { useHistoryNav } from './hooks/useHistoryNav.js'; @@ -425,29 +426,13 @@ const AppInner = () => { const studyReadingPlanDay = async (dayRefs, title) => { setStudyBuildBusy(true); try { + const lastTemplate = lastTemplateId(); // the notes template you picked last time you started a study by hand + const templateDef = reader.userTemplates?.find((t) => t.id === lastTemplate); const chapters = await Promise.all(dayRefs.map(async ({ book, chapter }) => { const data = await fetchStudyChapter('BSB', book, chapter); - const verses = parseBibleChapter(data); - const bookName = bookOptions.find((b) => b.abbrev === book)?.name ?? book; - const chunk = { - id: makeId(), - startVerse: verses[0]?.number ?? 1, - endVerse: verses.at(-1)?.number ?? 1, - spilloverEndVerse: null, - observation: '', interpretation: '', application: '', - crossReferences: [], greekWords: [], generalNotes: '', - episodeNumber: '', episodeTitle: '', finalScript: '', tags: [], - }; - return { book: bookName, bookAbbrev: book, chapter: String(chapter), verses, chunks: [chunk] }; + return { bookAbbrev: book, bookName: bookOptions.find((b) => b.abbrev === book)?.name ?? book, chapter, verses: parseBibleChapter(data) }; })); - const newProject = { - id: makeId(), - title: title || 'Reading Plan Study', - translation: 'BSB', - lastEdited: Date.now(), - selectedChunkId: chapters[0]?.chunks[0]?.id ?? null, - chapters, - }; + const newProject = planStudyProject({ chapters, title, templateId: lastTemplate, templateDef, makeId }); setProject(newProject); setCurrentPage('study'); } catch (err) { diff --git a/src/components/DailyFive.jsx b/src/components/DailyFive.jsx new file mode 100644 index 0000000..70bf82a --- /dev/null +++ b/src/components/DailyFive.jsx @@ -0,0 +1,66 @@ +// Home card: your five minutes for today. A short prompt on a passage from your studies, a five-minute clock, and a +// box to write in. What you write is added to that passage's notes. Pasting is turned off here on purpose. +import { useEffect, useMemo, useRef, useState } from 'react'; +import { useApp } from '../context/AppContext.js'; +import { loadProjectById } from '../utils/projectStorage.js'; +import { FIELD_NAMES, MINUTES, dailyPatch, pickDaily } from '../utils/dailyFive.js'; +import { formatClock } from '../utils/present.js'; + +const box = 'mb-4 rounded-xl border border-[var(--r-line)] bg-[var(--r-bar)] px-4 py-3 text-sm'; + +export default function DailyFive() { + const { projectIndex, saveChunkPatch } = useApp(); + const [step, setStep] = useState('card'); // card -> write -> done + const [text, setText] = useState(''); + const [left, setLeft] = useState(MINUTES * 60); + const [pasteTried, setPasteTried] = useState(false); + const pick = useMemo(() => pickDaily(projectIndex.map((e) => loadProjectById(e.id)).filter(Boolean)), [projectIndex]); + const started = useRef(null); + + useEffect(() => { + if (step !== 'write') return undefined; + started.current = Date.now(); + const t = window.setInterval(() => setLeft(Math.max(0, MINUTES * 60 - Math.round((Date.now() - started.current) / 1000))), 1000); + return () => window.clearInterval(t); + }, [step]); + + if (step === 'done') return
Saved to your notes. Same time tomorrow.
; + if (!pick) return null; + const { prompt } = pick; + + const save = () => { + saveChunkPatch(pick.projectId, pick.chunk.id, dailyPatch(pick.chunk, prompt.field, text)); + setStep('done'); + }; + + if (step === 'card') { + return ( +
+ Your five minutes: {pick.reference} · {FIELD_NAMES[prompt.field]} · {pick.projectTitle} + +
+ ); + } + return ( +
+
+

{pick.reference} · {FIELD_NAMES[prompt.field]}

+ {left === 0 ? 'Time’s up, finish your thought' : formatClock(left)} +
+ {pick.verses.length > 0 && ( +
+ {pick.verses.map((v) =>

{v.number}{v.text}

)} +
+ )} +

{prompt.text}

+