// Reading plans home, styled to match the reader (paper / sepia / dark). One big // "Continue reading" card, your plans in progress, and a browsable catalog. Details, // the custom-plan builder and everything else open as sheets. import { useState } from 'react'; import { useApp } from '../context/AppContext.js'; import { themeVars, READER_THEMES } from '../components/reader/readerThemes.js'; import { ChevronLeft } from '../components/reader/icons.jsx'; import Sheet from '../components/reader/Sheet.jsx'; import PlanDetailSheet from '../components/plans/PlanDetailSheet.jsx'; import CustomPlanBuilder from '../components/plans/CustomPlanBuilder.jsx'; import { formatRelativeDate } from '../utils/bibleUtils.jsx'; import { READING_PLANS, getPlan, getPlanSchedule, formatDayReadings, nextUnreadDayIndex, computeStreak, daysBehindPace, } from '../utils/readingPlans.js'; const CATEGORY_ORDER = ['Whole Bible', 'Old Testament', 'New Testament', 'Topical']; const THEME_ORDER = ['paper', 'sepia', 'dark']; const pill = 'rounded-full border border-[var(--r-line)] px-2 py-0.5 text-[10px] text-[var(--r-sub)]'; const sectionTitle = 'mb-2 mt-7 text-[11px] uppercase tracking-wide text-[var(--r-sub)]'; function ProgressBar({ pct }) { return
; } // The next unread day of an in-progress plan, with its readings. function nextOf(progress) { const plan = getPlan(progress.planId); if (!plan) return null; const dayIndex = nextUnreadDayIndex(plan, progress.completedDays); if (dayIndex == null) return null; return { plan, dayIndex, refs: getPlanSchedule(progress.planId)[dayIndex] ?? [] }; } export default function ReadingPlansPage() { const { goHome, planProgress, customPlans, readingActivity, startPlan, toggleDay, restartPlan, removePlanProgress, recalibratePace, setPlanTranslation, addCustomPlan, removeCustomPlan, updateCustomPlan, openPlanReading, studyReadingPlanDay, studyBuildBusy, readingPlanSyncStatus, readingPlanSyncError, retryReadingPlanSync, readerTheme, setReaderTheme, readerTranslation, planTranslationPref, } = useApp(); const [detail, setDetail] = useState(null); // { planId, progressId? } const [builderMode, setBuilderMode] = useState(null); // null | 'new' | a custom plan id const [category, setCategory] = useState('All'); const active = planProgress.filter((p) => !p.completedAt && getPlan(p.planId)).sort((a, b) => (b.lastActivityAt ?? 0) - (a.lastActivityAt ?? 0)); const completed = planProgress.filter((p) => p.completedAt && getPlan(p.planId)).sort((a, b) => b.completedAt - a.completedAt); const streak = computeStreak(readingActivity); const editingPlan = builderMode && builderMode !== 'new' ? customPlans.find((p) => p.id === builderMode) : null; const hero = active[0]; const heroNext = hero ? nextOf(hero) : null; const catalog = [...customPlans, ...READING_PLANS]; const categories = ['All', ...CATEGORY_ORDER.filter((c) => READING_PLANS.some((p) => p.category === c)), ...(customPlans.length ? ['My Plans'] : [])]; const shown = catalog.filter((p) => category === 'All' || p.category === category); const cycleTheme = () => setReaderTheme(THEME_ORDER[(THEME_ORDER.indexOf(readerTheme) + 1) % THEME_ORDER.length]); const actions = { defaultTranslation: planTranslationPref && planTranslationPref !== 'same' ? planTranslationPref : readerTranslation, setTranslation: setPlanTranslation, toggleDay, recalibrate: recalibratePace, restart: restartPlan, remove: removePlanProgress, read: openPlanReading, start: startPlan, nextDay: (plan, progress) => nextUnreadDayIndex(plan, progress.completedDays) ?? 0, edit: setBuilderMode, delete: removeCustomPlan, updateCover: (id, image) => updateCustomPlan(id, { image }), }; const detailPlan = detail ? getPlan(detail.planId) : null; const detailProgress = detail?.progressId ? planProgress.find((p) => p.id === detail.progressId) : null; const detailActive = detail && !detail.progressId ? active.find((p) => p.planId === detail.planId) : null; const submitBuilder = (values) => { if (editingPlan) { const booksChanged = JSON.stringify(values.books) !== JSON.stringify(editingPlan.books); const daysChanged = values.days !== editingPlan.days; const rangesChanged = JSON.stringify(values.chapterRanges ?? {}) !== JSON.stringify(editingPlan.chapterRanges ?? {}); updateCustomPlan(editingPlan.id, { title: values.title.trim(), description: values.description.trim(), days: Math.max(1, Math.round(values.days)), books: values.books, chapterRanges: values.chapterRanges && Object.keys(values.chapterRanges).length > 0 ? values.chapterRanges : undefined, image: values.image || undefined, }, { resetProgress: booksChanged || daysChanged || rangesChanged }); } else { addCustomPlan(values); } setBuilderMode(null); }; return (

Reading plans

{streak.current > 0 && {streak.current} day streak}
{readingPlanSyncStatus === 'error' && (
Couldn’t sync your reading plans to the server. Changes are only saved on this device until this is fixed. {readingPlanSyncError && {readingPlanSyncError}}
)} {hero && heroNext && (

Today · {heroNext.plan.title} · Day {heroNext.dayIndex + 1}

{formatDayReadings(heroNext.refs)}

{hero.translation ?? readerTranslation}
)} {active.length > 0 && ( <>

In progress

{active.map((p) => { const next = nextOf(p); const plan = getPlan(p.planId); const pct = Math.round((p.completedDays.length / plan.days) * 100); const behind = daysBehindPace(plan, p); return (
{next && (
{pct}%
)}
); })}
)}
Browse plans
{categories.map((c) => ( ))}
{shown.map((plan) => { const mine = planProgress.filter((p) => p.planId === plan.id); const running = mine.find((p) => !p.completedAt); const done = mine.filter((p) => p.completedAt).length; return ( ); })}
{completed.length > 0 && ( <>

Completed

{completed.map((p) => { const plan = getPlan(p.planId); return (

{plan.icon ?? '✓'} {plan.title}

Completed {formatRelativeDate(p.completedAt)}{p.translation ? ` · ${p.translation}` : ''}

); })}
)}
{detailPlan && ( p.planId === detailPlan.id && p.completedAt).length} onClose={() => setDetail(null)} actions={actions} /> )} setBuilderMode(null)} title={editingPlan ? 'Edit reading plan' : 'New reading plan'}> {builderMode !== null && ( setBuilderMode(null)} /> )}
); }