Reading plans: integrated into the Reader, redesigned, with per-plan translation
Build and Push Docker Image / build-and-push (push) Failing after 19s
Build and Push Docker Image / build-and-push (push) Failing after 19s
Reader: plan strip with a dot per chapter, plan sheet (day picker, chapter ticks, translation, streak, other plans), end-of-day card that steps through the day and rolls into the next, and chapter-level auto-marking. Each plan remembers its own translation (BSB, ESV, NLT, CSB, NASB, NET) and the Reader follows it while a plan is open. Plans home redesigned in the reader's themes with a Continue card, in-progress plans, a filterable catalog, and sheets for details and the custom-plan builder. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c11b6a60bf
commit
b1bc1c8ea4
17 files changed
+1205
-863
No files matched your search
@@ -0,0 +1,113 @@
|
||||
// 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 } from '../../utils/readingPlans.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;
|
||||
return (
|
||||
<>
|
||||
<div className="mb-3 flex items-center justify-between text-sm">
|
||||
<span>{progress.completedDays.length} of {plan.days} days · {pct}%</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="text-xs text-[var(--r-sub)]">Read in</span>
|
||||
<TranslationSelect value={progress.translation ?? actions.defaultTranslation} onChange={(t) => actions.setTranslation(progress.id, t)} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="mb-3 h-1.5 overflow-hidden rounded-full bg-[var(--r-line)]"><div className="h-full bg-[var(--r-accent)]" style={{ width: `${pct}%` }} /></div>
|
||||
{behind >= 2 && (
|
||||
<div className="mb-3 flex items-center justify-between rounded-xl border border-[var(--r-line)] px-3 py-2 text-xs">
|
||||
<span className="text-[var(--r-sub)]">{behind} days behind pace</span>
|
||||
<button type="button" onClick={() => actions.recalibrate(progress.id)} className="font-medium text-[var(--r-accent)] hover:underline">Catch me up</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="max-h-[38vh] overflow-y-auto rounded-xl border border-[var(--r-line)]">
|
||||
{schedule.map((refs, i) => {
|
||||
const done = progress.completedDays.includes(i);
|
||||
return (
|
||||
<div key={i} className="flex items-center gap-3 border-b border-[var(--r-line)] px-3 py-2 last:border-0">
|
||||
<input type="checkbox" checked={done} onChange={() => actions.toggleDay(progress.id, i, plan.days)} className="h-4 w-4 shrink-0 accent-[var(--r-accent)]" aria-label={`Day ${i + 1} read`} />
|
||||
<span className={`flex-1 text-sm ${done ? 'text-[var(--r-sub)] line-through' : ''}`}>Day {i + 1} · {formatDayReadings(refs)}</span>
|
||||
<button type="button" onClick={() => { onClose(); actions.read(progress.id, i); }} className="shrink-0 text-xs font-medium text-[var(--r-accent)] hover:underline">Read</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-4 flex items-center justify-between text-xs">
|
||||
<button type="button" onClick={() => { if (window.confirm('Restart this plan from day 1?')) { actions.restart(progress.id); onClose(); } }} className="text-[var(--r-sub)] hover:text-[var(--r-text)]">Restart</button>
|
||||
<button type="button" onClick={() => { if (window.confirm('Remove this plan?')) { actions.remove(progress.id); onClose(); } }} className="text-red-600 hover:underline">Remove</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 && <img src={plan.image} alt="" onError={(e) => { e.currentTarget.style.display = 'none'; }} className="mb-3 h-32 w-full rounded-xl object-cover" style={{ objectPosition: plan.imageFocus ?? 'center' }} />}
|
||||
<p className="text-sm text-[var(--r-sub)]">{plan.description || 'A custom reading plan.'}</p>
|
||||
<p className="mt-2 text-xs text-[var(--r-sub)]">{plan.days} days · {chapters} chapters · about {(chapters / plan.days).toFixed(1)} a day{completedCount > 0 ? ` · completed ${completedCount}×` : ''}</p>
|
||||
|
||||
<p className="mb-1 mt-4 text-[11px] uppercase tracking-wide text-[var(--r-sub)]">First days</p>
|
||||
<div className="rounded-xl border border-[var(--r-line)]">
|
||||
{schedule.slice(0, 5).map((refs, i) => (
|
||||
<div key={i} className="border-b border-[var(--r-line)] px-3 py-1.5 text-sm last:border-0">Day {i + 1} · {formatDayReadings(refs)}</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!active && (
|
||||
<div className="mt-4 flex items-center justify-between text-sm">
|
||||
<span>Read in</span>
|
||||
<TranslationSelect value={translation} onChange={setTranslation} />
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3">
|
||||
{active
|
||||
? <button type="button" className={primary} onClick={() => { onClose(); actions.read(active.id, actions.nextDay(plan, active)); }}>Continue reading</button>
|
||||
: <button type="button" className={primary} onClick={() => { actions.start(plan.id, translation); onClose(); }}>{completedCount > 0 ? 'Start again' : 'Start plan'}</button>}
|
||||
</div>
|
||||
|
||||
{plan.custom && (
|
||||
<div className="mt-4 flex flex-wrap items-center gap-3 border-t border-[var(--r-line)] pt-3 text-xs">
|
||||
<button type="button" onClick={() => { onClose(); actions.edit(plan.id); }} className="text-[var(--r-sub)] hover:text-[var(--r-text)]">Edit plan</button>
|
||||
<button type="button" onClick={() => fileRef.current?.click()} className="text-[var(--r-sub)] hover:text-[var(--r-text)]">{plan.image ? 'Change cover' : 'Add cover photo'}</button>
|
||||
{plan.image && <button type="button" onClick={() => actions.updateCover(plan.id, null)} className="text-[var(--r-sub)] hover:text-red-600">Remove cover</button>}
|
||||
<button type="button" onClick={() => { if (window.confirm('Delete this plan? Its progress history will be removed too.')) { actions.delete(plan.id); onClose(); } }} className="ml-auto text-red-600 hover:underline">Delete</button>
|
||||
<input ref={fileRef} type="file" accept="image/*" onChange={pickImage} className="hidden" />
|
||||
{imageError && <p className="w-full text-red-600">{imageError}</p>}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PlanDetailSheet({ plan, progress, active, completedCount, onClose, actions }) {
|
||||
if (!plan) return null;
|
||||
return (
|
||||
<Sheet open onClose={onClose} title={<span>{plan.icon ? `${plan.icon} ` : ''}{plan.title}</span>}>
|
||||
{progress
|
||||
? <ActiveBody plan={plan} progress={progress} onClose={onClose} actions={actions} />
|
||||
: <CatalogBody plan={plan} active={active} completedCount={completedCount} onClose={onClose} actions={actions} />}
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user