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:
1 parent
f1026dc194
commit
600b3d3ab9
21 files changed
+1166
-159
No files matched your search
+29
-15
@@ -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
|
||||
|
||||
Reference in new issue
Block a user