import { useRef, useState } from 'react'; import { useApp } from '../context/AppContext.js'; import AuthStatus from '../components/AuthStatus.jsx'; import { bookOptions, CHAPTER_COUNTS, formatRelativeDate } from '../utils/bibleUtils.jsx'; import { READING_PLANS, getPlan, getPlanSchedule, formatDayReadings, firstRefOfDay, nextUnreadDayIndex, themeForCategory, ALL_BOOKS, OT_BOOKS, NT_BOOKS, GOSPELS, PENTATEUCH, WISDOM_BOOKS, } from '../utils/readingPlans.js'; export default function ReadingPlansPage() { const { goHome, planProgress, customPlans, startPlan, toggleDay, restartPlan, removePlanProgress, addCustomPlan, removeCustomPlan, updateCustomPlan, readReadingPlanDay, } = useApp(); const [showBuilder, setShowBuilder] = useState(false); const activeProgress = planProgress.filter((p) => !p.completedAt); const completedProgress = planProgress .filter((p) => p.completedAt) .sort((a, b) => b.completedAt - a.completedAt); const categoryOrder = ['Whole Bible', 'Old Testament', 'New Testament', 'Topical']; const builtinCategories = categoryOrder.filter((c) => READING_PLANS.some((p) => p.category === c)); return (

Bible Study Project

Reading Plans

{activeProgress.length > 0 && (

Continue Reading

{activeProgress.map((progress) => ( ))}
)}

My Plans

{!showBuilder && ( )}
{showBuilder && (
{ addCustomPlan(input); setShowBuilder(false); }} onCancel={() => setShowBuilder(false)} />
)} {customPlans.length > 0 ? (
{customPlans.map((plan) => ( p.planId === plan.id)} onStart={startPlan} onDelete={(id) => { if (window.confirm('Delete this plan? Its progress history will be removed too.')) removeCustomPlan(id); }} onImageChange={(id, image) => updateCustomPlan(id, { image })} /> ))}
) : !showBuilder && (

You haven't created any plans yet โ€” try "+ New Plan".

)}
{builtinCategories.map((category) => (

{category}

{READING_PLANS.filter((p) => p.category === category).map((plan) => ( p.planId === plan.id)} onStart={startPlan} /> ))}
))} {completedProgress.length > 0 && (

Completed

{completedProgress.map((progress) => { const plan = getPlan(progress.planId); if (!plan) return null; return (

{plan.icon ?? 'โœ“'} {plan.title}

Completed {formatRelativeDate(progress.completedAt)}

); })}
)}
); } function PlanCover({ plan }) { const [imgFailed, setImgFailed] = useState(false); const theme = themeForCategory(plan.category); const showImage = !!plan.image && !imgFailed; return (
{showImage && ( setImgFailed(true)} style={{ objectPosition: plan.imageFocus ?? 'center' }} className="absolute inset-0 h-full w-full object-cover" /> )} {!showImage && {plan.icon ?? '๐Ÿ“–'}}
); } function ActivePlanCard({ progress, onRead, onToggleDay, onRestart, onRemove }) { const [expanded, setExpanded] = useState(false); const plan = getPlan(progress.planId); if (!plan) return null; const theme = themeForCategory(plan.category); const schedule = getPlanSchedule(progress.planId); const pct = plan.days > 0 ? Math.round((progress.completedDays.length / plan.days) * 100) : 0; const nextDay = nextUnreadDayIndex(plan, progress.completedDays); const nextRefs = nextDay != null ? schedule[nextDay] : null; const nextFirstRef = nextRefs ? firstRefOfDay(nextRefs) : null; return (

{plan.category}

{plan.icon ?? '๐Ÿ“–'} {plan.title}

{progress.completedDays.length} / {plan.days} days {nextRefs && <> ยท Next: Day {nextDay + 1} โ€” {formatDayReadings(nextRefs)}}

{nextFirstRef && ( )}

{pct}%

{expanded && (
{schedule.map((dayRefs, i) => { const done = progress.completedDays.includes(i); const ref = firstRefOfDay(dayRefs); return (
onToggleDay(progress.id, i, plan.days)} className="h-4 w-4 shrink-0 rounded border-slate-300 text-emerald-600 focus:ring-emerald-500" /> Day {i + 1} โ€” {formatDayReadings(dayRefs)}
); })}
)}
); } function PlanCatalogCard({ plan, progressForPlan, onStart, onDelete, onImageChange }) { const active = progressForPlan.find((p) => !p.completedAt); const completedCount = progressForPlan.filter((p) => p.completedAt).length; const theme = themeForCategory(plan.category); const fileInputRef = useRef(null); const [imageError, setImageError] = useState(''); const pickImage = async (e) => { const file = e.target.files?.[0]; e.target.value = ''; // lets the same file be re-picked later if (!file) return; try { setImageError(''); onImageChange(plan.id, await resizeImageToDataUrl(file)); } catch { setImageError("Couldn't use that image."); } }; return (

{plan.title}

{onDelete && ( )}

{plan.description || 'A custom reading plan.'}

{plan.days} days {completedCount > 0 && โœ“ Completed {completedCount}ร—}
{onImageChange && (
{plan.image && ( )}
)} {imageError &&

{imageError}

}
); } // Downscales a picked image file to a reasonable cover-photo size and returns // a data URL โ€” no server upload endpoint needed, it's stored right on the plan. function resizeImageToDataUrl(file, maxWidth = 960, quality = 0.82) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onerror = () => reject(new Error('Could not read file.')); reader.onload = () => { const img = new Image(); img.onerror = () => reject(new Error('Could not decode image.')); img.onload = () => { const scale = Math.min(1, maxWidth / img.width); const canvas = document.createElement('canvas'); canvas.width = Math.round(img.width * scale); canvas.height = Math.round(img.height * scale); canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height); resolve(canvas.toDataURL('image/jpeg', quality)); }; img.src = reader.result; }; reader.readAsDataURL(file); }); } const BOOK_PRESETS = [ ['All', ALL_BOOKS], ['None', []], ['Old Testament', OT_BOOKS], ['New Testament', NT_BOOKS], ['Gospels', GOSPELS], ['Pentateuch', PENTATEUCH], ['Psalms', ['PSA']], ['Proverbs', ['PRO']], ['Wisdom', WISDOM_BOOKS], ]; function CustomPlanBuilder({ onCreate, onCancel }) { const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); const [days, setDays] = useState(30); const [books, setBooks] = useState([]); const [image, setImage] = useState(null); const [imageError, setImageError] = useState(''); const fileInputRef = useRef(null); const toggleBook = (abbrev) => { setBooks((prev) => (prev.includes(abbrev) ? prev.filter((b) => b !== abbrev) : [...prev, abbrev])); }; const pickImage = async (e) => { const file = e.target.files?.[0]; e.target.value = ''; if (!file) return; try { setImageError(''); setImage(await resizeImageToDataUrl(file)); } catch { setImageError("Couldn't use that image."); } }; const totalChapters = books.reduce((sum, b) => sum + (CHAPTER_COUNTS[b] ?? 0), 0); const canCreate = title.trim().length > 0 && books.length > 0 && days >= 1; return (

New reading plan