// 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
?
: }
);
}