Add reading plans: catalog, progress tracking, and custom plans

Replaces the old single-book reading goal with a full Reading Plans page:
9 built-in plans (Whole Bible, OT/NT, topical) with cover photos and
color-themed cards, day-by-day checklists synced to the server per user,
inline "mark today read" in the Bible reader, and user-created plans with
their own book picker and an optional uploaded cover photo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5 committed 2026-09-23 12:51:28 -04:00
1 parent f1026dc194
commit 600b3d3ab9
21 files changed
+1166 -159

No files matched your search

+29 -15
View File
@@ -4,6 +4,7 @@ import { formatCrossRef, speakOriginalWord } from '../utils/wordStudy.js';
import AuthStatus from '../components/AuthStatus.jsx';
import AudioPlayer from '../components/AudioPlayer.jsx';
import { bookOptions, BookmarkIcon, CopyIcon } from '../utils/bibleUtils.jsx';
import { getPlan, getPlanSchedule, findDayIndexForRef } from '../utils/readingPlans.js';
import DrawCanvas from './DrawCanvas.jsx';
const NO_STROKES = []; // stable fallback so DrawCanvas doesn't redraw every render
@@ -57,11 +58,26 @@ export default function ReaderPage() {
readerTextHighlights,
addTextHighlight,
removeTextHighlight,
readingPlan,
markChapterRead,
planProgress,
toggleDay,
} = useApp();
const readerBook = bookOptions.find((b) => b.abbrev === readerBookAbbrev);
// Active reading plans whose schedule includes the chapter currently on screen.
const planMatches = useMemo(() => {
return planProgress
.filter((p) => !p.completedAt)
.map((p) => {
const plan = getPlan(p.planId);
if (!plan) return null;
const dayIndex = findDayIndexForRef(getPlanSchedule(p.planId), readerBookAbbrev, readerChapter);
if (dayIndex === -1) return null;
return { progressId: p.id, plan, dayIndex, done: p.completedDays.includes(dayIndex) };
})
.filter(Boolean);
}, [planProgress, readerBookAbbrev, readerChapter]);
const [readerDrawMode, setReaderDrawMode] = useState(false);
const [readerWideLayout, setReaderWideLayout] = useState(() => localStorage.getItem('reader-wide') === '1');
const readerPageInkStrokes = readerInkByPage?.[`${readerBookAbbrev}_${readerChapter}`] ?? NO_STROKES;
@@ -316,19 +332,17 @@ export default function ReaderPage() {
</select>
</label>
<div className="ml-auto flex items-center gap-2">
{readingPlan && readingPlan.bookAbbrev === readerBookAbbrev && (() => {
const done = readingPlan.chaptersRead.includes(readerChapter);
return (
<button
type="button"
onClick={() => markChapterRead(readerChapter)}
className={`rounded-xl px-3 py-1.5 text-sm font-semibold transition ${done ? 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200' : 'border border-emerald-300 bg-white text-emerald-700 hover:bg-emerald-50'}`}
title={done ? 'Click to unmark' : 'Mark this chapter as read in your reading plan'}
>
{done ? '✓ Read' : 'Mark as read'}
</button>
);
})()}
{planMatches.map(({ progressId, plan, dayIndex, done }) => (
<button
key={progressId}
type="button"
onClick={() => toggleDay(progressId, dayIndex, plan.days)}
className={`rounded-xl px-3 py-1.5 text-sm font-semibold transition ${done ? 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200' : 'border border-emerald-300 bg-white text-emerald-700 hover:bg-emerald-50'}`}
title={done ? `Click to unmark Day ${dayIndex + 1} of "${plan.title}"` : `Mark Day ${dayIndex + 1} of "${plan.title}" as read`}
>
{done ? `✓ ${plan.title}` : `Mark read — ${plan.title}`}
</button>
))}
<button type="button" onClick={readerGoToPreviousChapter} disabled={readerChapter <= 1}
className="rounded-xl border border-slate-300 bg-white px-4 py-1.5 text-sm font-semibold text-slate-700 transition hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-40">
‹ Prev