Connect reading plans to study/audio, cache offline audio, add journal export
Build and Push Docker Image / build-and-push (push) Successful in 4m33s

Reading plan days now offer "Study this" (seeds a new chunk-builder project
with the day's full passage and jumps straight into the Study editor) and
"Listen" (starts book-mode audio at the right chapter via a new
handlePlayBookAudioFrom, avoiding a stale-closure bug the reader-state
approach would hit). Fixed the "Listen to BSB Audio" player only rendering
when a study project existed, leaving Listen with nowhere to show up.

The service worker now caches audio.bible.helloao.org (the actual host
serving narrator audio, not just the text/cross-ref API) and no longer
tries to cache 206 Partial Content responses, which the Cache API rejects
and was throwing unhandled rejections for every range-requested audio file.

Added a reading journal export (Settings > My reading journal): gathers
bookmarks, text highlights, and completed plans, fetches their verse text,
and offers a .docx download or a print-to-PDF page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5 committed 2026-09-23 13:41:40 -04:00
1 parent 1dd6aef6a5
commit 787edcbe73
8 files changed
+604 -105

No files matched your search

+68 -17
View File
@@ -36,6 +36,9 @@ export default function ReadingPlansPage() {
removeCustomPlan,
updateCustomPlan,
readReadingPlanDay,
listenReadingPlanDay,
studyReadingPlanDay,
studyBuildBusy,
} = useApp();
const [builderMode, setBuilderMode] = useState(null); // null | 'new' | a custom plan id to edit
@@ -78,7 +81,14 @@ export default function ReadingPlansPage() {
<main className="mx-auto max-w-5xl space-y-8 px-4 py-8 sm:px-6 lg:px-8">
{activeProgress.length > 0 && (
<TodayPanel activeProgress={activeProgress} onRead={readReadingPlanDay} onToggleDay={toggleDay} />
<TodayPanel
activeProgress={activeProgress}
onRead={readReadingPlanDay}
onListen={listenReadingPlanDay}
onStudy={studyReadingPlanDay}
studyBuildBusy={studyBuildBusy}
onToggleDay={toggleDay}
/>
)}
{activeProgress.length > 0 && (
@@ -90,6 +100,9 @@ export default function ReadingPlansPage() {
key={progress.id}
progress={progress}
onRead={readReadingPlanDay}
onListen={listenReadingPlanDay}
onStudy={studyReadingPlanDay}
studyBuildBusy={studyBuildBusy}
onToggleDay={toggleDay}
onRestart={restartPlan}
onRemove={removePlanProgress}
@@ -220,7 +233,7 @@ export default function ReadingPlansPage() {
// A single combined "what's left to read today, across every active plan" list —
// the fast-glance view; ActivePlanCard below still has full detail/expand/restart.
function TodayPanel({ activeProgress, onRead, onToggleDay }) {
function TodayPanel({ activeProgress, onRead, onListen, onStudy, studyBuildBusy, onToggleDay }) {
const rows = activeProgress
.map((progress) => {
const plan = getPlan(progress.planId);
@@ -252,13 +265,32 @@ function TodayPanel({ activeProgress, onRead, onToggleDay }) {
<p className="truncate text-sm font-semibold text-slate-900">{plan.title}</p>
<p className="truncate text-xs text-slate-500">{formatDayReadings(refs)}</p>
</div>
<button
type="button"
onClick={() => onRead(ref.book, ref.chapter)}
className="shrink-0 rounded-xl bg-indigo-600 px-3 py-1.5 text-sm font-semibold text-white hover:bg-indigo-500"
>
Read
</button>
<div className="flex shrink-0 items-center gap-1.5">
<button
type="button"
onClick={() => onListen(ref.book, ref.chapter)}
title="Listen to today's reading"
className="rounded-xl border border-indigo-200 px-2.5 py-1.5 text-sm text-indigo-700 hover:bg-indigo-100"
>
🎧
</button>
<button
type="button"
disabled={studyBuildBusy}
onClick={() => onStudy(refs, `${plan.title} — ${formatDayReadings(refs)}`)}
title="Start a study project from this passage"
className="rounded-xl border border-indigo-200 px-2.5 py-1.5 text-sm text-indigo-700 hover:bg-indigo-100 disabled:cursor-not-allowed disabled:opacity-50"
>
✎
</button>
<button
type="button"
onClick={() => onRead(ref.book, ref.chapter)}
className="rounded-xl bg-indigo-600 px-3 py-1.5 text-sm font-semibold text-white hover:bg-indigo-500"
>
Read
</button>
</div>
</div>
))}
</div>
@@ -286,7 +318,7 @@ function PlanCover({ plan }) {
);
}
function ActivePlanCard({ progress, onRead, onToggleDay, onRestart, onRemove, onRecalibrate }) {
function ActivePlanCard({ progress, onRead, onListen, onStudy, studyBuildBusy, onToggleDay, onRestart, onRemove, onRecalibrate }) {
const [expanded, setExpanded] = useState(false);
const plan = getPlan(progress.planId);
if (!plan) return null;
@@ -314,13 +346,32 @@ function ActivePlanCard({ progress, onRead, onToggleDay, onRestart, onRemove, on
</div>
<div className="flex flex-wrap items-center gap-2">
{nextFirstRef && (
<button
type="button"
onClick={() => onRead(nextFirstRef.book, nextFirstRef.chapter)}
className="rounded-xl bg-emerald-600 px-3 py-1.5 text-sm font-semibold text-white hover:bg-emerald-500"
>
Read
</button>
<>
<button
type="button"
onClick={() => onListen(nextFirstRef.book, nextFirstRef.chapter)}
title="Listen to today's reading"
className="rounded-xl border border-emerald-300 px-2.5 py-1.5 text-sm text-emerald-700 hover:bg-emerald-100"
>
🎧
</button>
<button
type="button"
disabled={studyBuildBusy}
onClick={() => onStudy(nextRefs, `${plan.title} — ${formatDayReadings(nextRefs)}`)}
title="Start a study project from this passage"
className="rounded-xl border border-emerald-300 px-2.5 py-1.5 text-sm text-emerald-700 hover:bg-emerald-100 disabled:cursor-not-allowed disabled:opacity-50"
>
✎
</button>
<button
type="button"
onClick={() => onRead(nextFirstRef.book, nextFirstRef.chapter)}
className="rounded-xl bg-emerald-600 px-3 py-1.5 text-sm font-semibold text-white hover:bg-emerald-500"
>
Read
</button>
</>
)}
<button
type="button"