// Detail for one plan. Two modes: an in-progress plan (days to tick or jump into, // its translation, catch-up / restart / remove) and a catalog plan (preview of the // first days, pick a translation, start it). import { useRef, useState } from 'react'; import Sheet from '../reader/Sheet.jsx'; import TranslationSelect from './TranslationSelect.jsx'; import { resizeImageToDataUrl } from './CustomPlanBuilder.jsx'; import { formatDayReadings, getPlanSchedule, daysBehindPace, nextUnreadDayIndex } from '../../utils/readingPlans.js'; import { useOfflineSave } from '../../hooks/useOfflineSave.js'; import { DownloadProgress } from '../OfflineSave.jsx'; import { canSaveOffline } from '../../utils/offline.js'; const btn = 'rounded-xl border border-[var(--r-line)] px-3 py-2 text-sm hover:bg-black/5'; const primary = 'w-full rounded-xl bg-[var(--r-accent)] py-2.5 text-sm font-medium text-[var(--r-on)] hover:opacity-90 disabled:opacity-50'; function ActiveBody({ plan, progress, onClose, actions }) { const schedule = getPlanSchedule(plan.id); const behind = daysBehindPace(plan, progress); const pct = plan.days > 0 ? Math.round((progress.completedDays.length / plan.days) * 100) : 0; const offline = useOfflineSave(); const translation = progress.translation ?? actions.defaultTranslation; // The next week of readings, so a trip or a bad connection doesn't interrupt the plan const upcoming = (() => { const from = nextUnreadDayIndex(plan, progress.completedDays); return from == null ? [] : schedule.slice(from, from + 7).flat(); })(); return ( <>
{progress.completedDays.length} of {plan.days} days · {pct}% Read in actions.setTranslation(progress.id, t)} />
{behind >= 2 && (
{behind} days behind pace
)}
{schedule.map((refs, i) => { const done = progress.completedDays.includes(i); return (
actions.toggleDay(progress.id, i, plan.days)} className="h-4 w-4 shrink-0 accent-[var(--r-accent)]" aria-label={`Day ${i + 1} read`} /> Day {i + 1} · {formatDayReadings(refs)}
); })}
{upcoming.length > 0 && canSaveOffline(translation) && (
{offline.job ? : ( )}
)}
); } function CatalogBody({ plan, active, completedCount, onClose, actions }) { const [translation, setTranslation] = useState(actions.defaultTranslation); const [imageError, setImageError] = useState(''); const fileRef = useRef(null); const schedule = getPlanSchedule(plan.id); const chapters = schedule.reduce((n, d) => n + d.length, 0); const pickImage = async (e) => { const file = e.target.files?.[0]; e.target.value = ''; if (!file) return; try { setImageError(''); actions.updateCover(plan.id, await resizeImageToDataUrl(file)); } catch { setImageError('Couldn’t use that image.'); } }; return ( <> {plan.image && { e.currentTarget.style.display = 'none'; }} className="mb-3 h-32 w-full rounded-xl object-cover" style={{ objectPosition: plan.imageFocus ?? 'center' }} />}

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

{plan.days} days · {chapters} chapters · about {(chapters / plan.days).toFixed(1)} a day{completedCount > 0 ? ` · completed ${completedCount}×` : ''}

First days

{schedule.slice(0, 5).map((refs, i) => (
Day {i + 1} · {formatDayReadings(refs)}
))}
{!active && (
Read in
)}
{active ? : }
{plan.custom && (
{plan.image && } {imageError &&

{imageError}

}
)} ); } export default function PlanDetailSheet({ plan, progress, active, completedCount, onClose, actions }) { if (!plan) return null; return ( {plan.icon ? `${plan.icon} ` : ''}{plan.title}}> {progress ? : } ); }