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
@@ -7,7 +7,7 @@ import { READER_TRANSLATIONS } from '../../hooks/useBibleReader.js';
|
||||
const NT_START = bookOptions.findIndex((b) => b.abbrev === 'MAT');
|
||||
const chip = 'rounded-lg border px-2 py-2 text-center text-sm transition';
|
||||
|
||||
export default function NavigatorSheet({ open, onClose, bookAbbrev, chapter, translation, onTranslation, onGo }) {
|
||||
export default function NavigatorSheet({ open, onClose, bookAbbrev, chapter, translation, onTranslation, translationNote, onGo }) {
|
||||
const [pickedBook, setPickedBook] = useState(null); // book abbrev whose chapters are showing
|
||||
const close = () => { setPickedBook(null); onClose(); };
|
||||
const book = pickedBook ? bookOptions.find((b) => b.abbrev === pickedBook) : null;
|
||||
@@ -33,6 +33,7 @@ export default function NavigatorSheet({ open, onClose, bookAbbrev, chapter, tra
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{translationNote && <p className="-mt-1 mb-3 text-[11px] text-[var(--r-sub)]">{translationNote}</p>}
|
||||
<p className="mb-1.5 text-[11px] uppercase tracking-wide text-[var(--r-sub)]">Old Testament</p>
|
||||
<div className="mb-4 grid grid-cols-3 gap-1.5 sm:grid-cols-4">{bookOptions.slice(0, NT_START).map(bookButton)}</div>
|
||||
<p className="mb-1.5 text-[11px] uppercase tracking-wide text-[var(--r-sub)]">New Testament</p>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Card at the end of the chapter while reading a plan: step to the next chapter of
|
||||
// today's reading, or finish the day and roll into the next one.
|
||||
export default function PlanFooter({ info, label, onNext, onNextDay, onFinish }) {
|
||||
if (!info || info.position === 0) return null;
|
||||
const last = info.position === info.total;
|
||||
const nextRef = !last ? info.refs[info.position] : null;
|
||||
const btn = 'rounded-full bg-[var(--r-accent)] px-5 py-2 text-sm font-medium text-[var(--r-on)]';
|
||||
return (
|
||||
<div className="mx-auto mt-10 max-w-md rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-4 text-center font-sans">
|
||||
<p className="text-xs text-[var(--r-sub)]">{info.plan.title} · Day {info.dayIndex + 1}</p>
|
||||
{!last && (
|
||||
<>
|
||||
<p className="mb-3 mt-1 text-sm font-medium text-[var(--r-text)]">{info.total - info.position} more chapter{info.total - info.position === 1 ? '' : 's'} today</p>
|
||||
<button type="button" onClick={onNext} className={btn}>Next: {label(nextRef)}</button>
|
||||
</>
|
||||
)}
|
||||
{last && info.nextDayRefs?.length > 0 && (
|
||||
<>
|
||||
<p className="mb-3 mt-1 text-sm font-medium text-[var(--r-text)]">That’s Day {info.dayIndex + 1}</p>
|
||||
<button type="button" onClick={onNextDay} className={btn}>Mark read · Day {info.dayIndex + 2}: {label(info.nextDayRefs[0])}</button>
|
||||
<div><button type="button" onClick={onFinish} className="mt-2 text-xs text-[var(--r-sub)] underline">Mark read and stop for today</button></div>
|
||||
</>
|
||||
)}
|
||||
{last && !info.nextDayRefs?.length && (
|
||||
<>
|
||||
<p className="mb-3 mt-1 text-sm font-medium text-[var(--r-text)]">Last day of the plan</p>
|
||||
<button type="button" onClick={onFinish} className={btn}>{info.done ? 'Plan complete' : 'Finish plan'}</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// The plan's control room, one tap from the reader: pick the day, tick chapters,
|
||||
// choose the Bible this plan is read in, and jump to another active plan.
|
||||
import Sheet from './Sheet.jsx';
|
||||
import { ChevronLeft, ChevronRight, CheckIcon } from './icons.jsx';
|
||||
import { READER_TRANSLATIONS } from '../../hooks/useBibleReader.js';
|
||||
import { formatDayReadings } from '../../utils/readingPlans.js';
|
||||
|
||||
export default function PlanSheet({
|
||||
open, onClose, info, translation, currentKey, streak, otherPlans, studyBusy,
|
||||
onDay, onToggleChapter, onGoChapter, onTranslation, onToggleDay, onSwitchPlan, onListen, onStudy, onLeave,
|
||||
}) {
|
||||
if (!info) return null;
|
||||
const label = (r) => formatDayReadings([r]);
|
||||
return (
|
||||
<Sheet open={open} onClose={onClose} title={info.plan.title}>
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<button type="button" onClick={() => onDay(-1)} disabled={info.dayIndex <= 0} aria-label="Previous day"
|
||||
className="rounded-lg p-1.5 text-[var(--r-sub)] hover:bg-black/5 disabled:opacity-30"><ChevronLeft /></button>
|
||||
<span className="text-sm font-medium">Day {info.dayIndex + 1} of {info.plan.days}{info.done ? ' · read' : ''}</span>
|
||||
<button type="button" onClick={() => onDay(1)} disabled={info.dayIndex >= info.plan.days - 1} aria-label="Next day"
|
||||
className="rounded-lg p-1.5 text-[var(--r-sub)] hover:bg-black/5 disabled:opacity-30"><ChevronRight /></button>
|
||||
</div>
|
||||
|
||||
<p className="mb-1 mt-3 text-[11px] uppercase tracking-wide text-[var(--r-sub)]">Reading</p>
|
||||
{info.refs.map((r) => {
|
||||
const key = `${r.book}.${r.chapter}`;
|
||||
const read = info.readSet.has(key);
|
||||
return (
|
||||
<div key={key} className="flex items-center gap-3 border-b border-[var(--r-line)] py-2">
|
||||
<button type="button" onClick={() => onToggleChapter(key)} aria-label={read ? 'Mark unread' : 'Mark read'}
|
||||
className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-[1.5px] ${read ? 'border-[var(--r-accent)] bg-[var(--r-accent)] text-[var(--r-on)]' : 'border-[var(--r-sub)] text-transparent'}`}>
|
||||
<span className="scale-75"><CheckIcon /></span>
|
||||
</button>
|
||||
<button type="button" onClick={() => onGoChapter(r)} className={`flex-1 text-left text-sm ${key === currentKey ? 'font-medium' : ''}`}>{label(r)}</button>
|
||||
{key === currentKey && <span className="rounded-full border border-[var(--r-line)] px-2 py-0.5 text-[10px] text-[var(--r-sub)]">reading</span>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button type="button" onClick={onToggleDay}
|
||||
className="mt-3 w-full rounded-xl border border-[var(--r-line)] py-2 text-sm hover:bg-black/5">
|
||||
{info.done ? 'Mark this day unread' : 'Mark the whole day read'}
|
||||
</button>
|
||||
|
||||
<p className="mb-1 mt-4 text-[11px] uppercase tracking-wide text-[var(--r-sub)]">This plan</p>
|
||||
<div className="flex items-center justify-between py-1.5 text-sm">
|
||||
<span>Read in</span>
|
||||
<select value={translation} onChange={(e) => onTranslation(e.target.value)}
|
||||
className="rounded-lg border border-[var(--r-line)] bg-[var(--r-bg)] px-2 py-1 text-sm text-[var(--r-text)]">
|
||||
{READER_TRANSLATIONS.map((t) => <option key={t.id} value={t.id}>{t.id} · {t.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-1.5 text-sm">
|
||||
<span>Streak</span><span className="text-[var(--r-sub)]">{streak.current} day{streak.current === 1 ? '' : 's'}</span>
|
||||
</div>
|
||||
<div className="mt-2 flex gap-2">
|
||||
<button type="button" onClick={onListen} className="flex-1 rounded-xl border border-[var(--r-line)] py-2 text-sm hover:bg-black/5">Listen</button>
|
||||
<button type="button" onClick={onStudy} disabled={studyBusy} className="flex-1 rounded-xl border border-[var(--r-line)] py-2 text-sm hover:bg-black/5 disabled:opacity-50">{studyBusy ? 'Building…' : 'Study this day'}</button>
|
||||
</div>
|
||||
|
||||
{otherPlans.length > 0 && (
|
||||
<>
|
||||
<p className="mb-1 mt-4 text-[11px] uppercase tracking-wide text-[var(--r-sub)]">Other plans</p>
|
||||
{otherPlans.map((o) => (
|
||||
<button key={o.progress.id} type="button" onClick={() => onSwitchPlan(o)}
|
||||
className="flex w-full items-center justify-between rounded-lg px-1 py-2 text-left text-sm hover:bg-black/5">
|
||||
<span className="truncate">{o.plan.title}</span>
|
||||
<span className="ml-3 shrink-0 text-xs text-[var(--r-sub)]">Day {o.dayIndex + 1}</span>
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
<button type="button" onClick={onLeave} className="mt-4 w-full text-center text-xs text-[var(--r-sub)] underline">Leave plan (keep reading on my own)</button>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,39 @@
|
||||
// Thin strip under the top bar while the open chapter belongs to an active
|
||||
// reading plan. Shows which day you're on and lets you mark it read.
|
||||
export default function PlanStrip({ matches, notice, onToggle }) {
|
||||
if (!matches.length && !notice) return null;
|
||||
// Thin strip under the top bar while reading a plan: which plan and day, one dot
|
||||
// per chapter of today's reading (filled = read, ringed = the chapter on screen),
|
||||
// and the translation. Tapping it opens the plan sheet. When you've wandered off
|
||||
// today's chapters (e.g. following a cross-reference) it offers a way back.
|
||||
export default function PlanStrip({ info, translation, inPlan, notice, onOpen, onJoin, onDot, onBackToDay }) {
|
||||
if (!info && !notice) return null;
|
||||
return (
|
||||
<div className="mx-auto flex max-w-2xl flex-col gap-1 px-4 pt-2 font-sans">
|
||||
<div className="mx-auto flex max-w-2xl flex-col gap-1 px-3 pt-2 font-sans">
|
||||
{notice && <p className="rounded-lg bg-[var(--r-bar)] px-3 py-1.5 text-xs text-[var(--r-accent)]">{notice}</p>}
|
||||
{matches.map((m) => (
|
||||
<button key={m.progressId} type="button" onClick={() => onToggle(m)}
|
||||
className="flex items-center justify-between gap-3 rounded-lg bg-[var(--r-bar)] px-3 py-1.5 text-left text-xs text-[var(--r-sub)] hover:bg-black/5"
|
||||
title={m.done ? 'Tap to unmark this day' : 'Tap to mark this day read'}>
|
||||
<span className="truncate"><span className="text-[var(--r-accent)]">{m.done ? '✓' : '●'}</span> {m.plan.title} · Day {m.dayIndex + 1} of {m.plan.days}</span>
|
||||
<span className="shrink-0">{m.total > 1 ? `${m.position} of ${m.total}` : m.done ? 'Read' : 'Mark read'}</span>
|
||||
</button>
|
||||
))}
|
||||
{info && (
|
||||
<div className="rounded-xl bg-[var(--r-bar)] px-3 py-2 text-xs text-[var(--r-sub)]">
|
||||
<button type="button" onClick={inPlan ? onOpen : onJoin} className="flex w-full items-center justify-between gap-3 text-left">
|
||||
<span className="truncate font-medium text-[var(--r-text)]">{info.plan.title} · Day {info.dayIndex + 1}</span>
|
||||
<span className="shrink-0">{inPlan ? translation : 'Open plan'}</span>
|
||||
</button>
|
||||
{inPlan && info.position === 0 && (
|
||||
<button type="button" onClick={onBackToDay} className="mt-1.5 rounded-full border border-[var(--r-line)] px-3 py-1 text-[11px] text-[var(--r-accent)] hover:bg-black/5">
|
||||
Back to today’s reading
|
||||
</button>
|
||||
)}
|
||||
{inPlan && info.position > 0 && (
|
||||
<div className="mt-1.5 flex items-center gap-1.5">
|
||||
{info.refs.map((r, i) => {
|
||||
const key = `${r.book}.${r.chapter}`;
|
||||
const read = info.readSet.has(key);
|
||||
const current = i + 1 === info.position;
|
||||
return (
|
||||
<button key={key} type="button" onClick={() => onDot(r)} aria-label={`Go to chapter ${r.chapter}`}
|
||||
className={`h-2.5 w-2.5 rounded-full border-[1.5px] ${read ? 'border-[var(--r-accent)] bg-[var(--r-accent)]' : 'border-[var(--r-sub)]'} ${current ? 'ring-2 ring-[var(--r-line)]' : ''}`} />
|
||||
);
|
||||
})}
|
||||
<span className="ml-auto">{info.done ? 'Day read' : `${info.position} of ${info.total}`}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { formatCrossRef } from '../../utils/wordStudy.js';
|
||||
export default function ReaderText({
|
||||
title, translation, notice, verses, loading, error, layout, fontSize, wide,
|
||||
bookmarks, bookKey, chapter, selectedVerse, onSelectVerse, renderVerseText, searchLower,
|
||||
crossRefs, showCrossRefs, onSelectionEnd,
|
||||
crossRefs, showCrossRefs, onSelectionEnd, footer,
|
||||
}) {
|
||||
const handleTap = (n) => {
|
||||
const sel = window.getSelection();
|
||||
@@ -73,6 +73,8 @@ export default function ReaderText({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && footer}
|
||||
|
||||
{notice && !loading && <p className="mt-10 border-t border-[var(--r-line)] pt-3 font-sans text-[11px] leading-snug text-[var(--r-sub)]">{notice}</p>}
|
||||
</article>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user