Fix plan days with no chapters, add a way out of ink mode, harden ntfy reminders
Build and Push Docker Image / build-and-push (push) Failing after 19s

Plans can no longer have more days than chapters (existing ones heal), and
opening a plan never strands the Reader on an empty day. The Reader shows
Done drawing while inking (Escape also exits). Reminders use the user's own
timezone, send within a 3 hour grace window, and Settings has a Send test
notification button.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-09-29 10:34:46 -04:00
1 parent ecb1c701fa
commit 297f2c7e84
14 files changed
+335 -58

No files matched your search

+10 -5
View File
@@ -3,7 +3,7 @@
import { useState, useEffect, useRef, useMemo, useDeferredValue } from 'react';
import { useApp } from '../context/AppContext.js';
import { bookOptions } from '../utils/bibleUtils.jsx';
import { getPlan, getPlanSchedule, findDayIndexForRef, formatDayReadings, nextUnreadDayIndex, computeStreak } from '../utils/readingPlans.js';
import { getPlan, getPlanSchedule, findDayIndexForRef, formatDayReadings, nextUnreadDayIndex, computeStreak, nearestReadingDay } from '../utils/readingPlans.js';
import DrawCanvas from './DrawCanvas.jsx';
import { READER_TRANSLATIONS } from '../hooks/useBibleReader.js';
import { useBibleSearch } from '../hooks/useBibleSearch.js';
@@ -162,11 +162,15 @@ export default function ReaderPage() {
};
const goToRef = (r) => goTo(r.book, r.chapter);
// Opens a day of the context plan on its first unread chapter.
// Opens a day of the context plan on its first unread chapter (or the nearest day that has one).
const goDay = (progressId, dayIndex) => {
const info = planInfo(planProgress.find((p) => p.id === progressId), dayIndex);
const progress = planProgress.find((p) => p.id === progressId);
if (!progress) return;
const schedule = getPlanSchedule(progress.planId);
const day = schedule[dayIndex]?.length ? dayIndex : nearestReadingDay(schedule, dayIndex);
const info = day >= 0 ? planInfo(progress, day) : null;
if (!info) return;
setReaderPlan({ progressId, dayIndex });
setReaderPlan({ progressId, dayIndex: day });
const target = info.refs.find((r) => !info.readSet.has(`${r.book}.${r.chapter}`)) ?? info.refs[0];
if (target) goToRef(target);
};
@@ -232,7 +236,7 @@ export default function ReaderPage() {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
if (e.key === 'ArrowLeft') readerGoToPreviousChapter();
else if (e.key === 'ArrowRight') readerGoToNextChapter();
else if (e.key === 'Escape') { if (sheet) closeSheet(); else setReaderSelectedVerse(null); }
else if (e.key === 'Escape') { if (sheet) closeSheet(); else if (readerDrawMode) setReaderDrawMode(false); else setReaderSelectedVerse(null); }
};
window.addEventListener('keydown', handleKey);
return () => window.removeEventListener('keydown', handleKey);
@@ -336,6 +340,7 @@ export default function ReaderPage() {
onBookmarks={() => openSheet('bookmarks')}
bookmarkCount={bookmarkEntries.length}
onSettings={() => openSheet('settings')}
onDoneDrawing={readerDrawMode ? () => setReaderDrawMode(false) : undefined}
/>
)}
{!chromeVisible && (
+31 -8
View File
@@ -8,6 +8,7 @@ import {
changePassword,
updateProfile,
updateReminderSettings,
sendReminderTest,
} from '../syncService.js';
import { exportJournalDocx, printJournalPdf } from '../utils/journalExport.js';
@@ -120,11 +121,20 @@ export default function SettingsPage() {
window.setTimeout(() => setPodcastNameSaved(false), 2500);
};
const [reminderTest, setReminderTest] = useState({ busy: false, ok: false, message: '' });
const testReminder = async () => {
setReminderTest({ busy: true, ok: false, message: '' });
const result = await sendReminderTest({ ntfyTopic: reminderForm.ntfyTopic, ntfyUrl: reminderForm.ntfyUrl });
setReminderTest(result.ok
? { busy: false, ok: true, message: 'Sent. Check your ntfy app or the topic page in a browser.' }
: { busy: false, ok: false, message: result.error ?? 'Could not send the test.' });
};
const submitReminderSettings = async (e) => {
e.preventDefault();
setReminderError('');
setReminderBusy(true);
const result = await updateReminderSettings(reminderForm);
const result = await updateReminderSettings({ ...reminderForm, reminderTz: Intl.DateTimeFormat().resolvedOptions().timeZone });
setReminderBusy(false);
if (!result.ok) {
setReminderError(result.error ?? 'Failed to save reminder settings.');
@@ -334,13 +344,26 @@ export default function SettingsPage() {
</label>
{reminderError && <p className="text-sm text-[var(--r-danger,#b42318)]">{reminderError}</p>}
{reminderSaved && <p className="text-sm text-[var(--r-accent)]">Saved.</p>}
<button
type="submit"
disabled={reminderBusy}
className="rounded-full bg-[var(--r-accent)] px-5 py-2 text-sm font-medium text-[var(--r-on)] transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
>
{reminderBusy ? 'Saving…' : 'Save'}
</button>
{reminderTest.message && (
<p role="status" className={`text-sm ${reminderTest.ok ? 'text-[var(--r-accent)]' : 'text-[var(--r-danger,#b42318)]'}`}>{reminderTest.message}</p>
)}
<div className="flex flex-wrap items-center gap-3">
<button
type="submit"
disabled={reminderBusy}
className="rounded-full bg-[var(--r-accent)] px-5 py-2 text-sm font-medium text-[var(--r-on)] transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
>
{reminderBusy ? 'Saving…' : 'Save'}
</button>
<button
type="button"
onClick={testReminder}
disabled={reminderTest.busy || !reminderForm.ntfyTopic.trim()}
className="rounded-full border border-[var(--r-line)] px-5 py-2 text-sm font-medium transition hover:bg-black/5 disabled:cursor-not-allowed disabled:opacity-50"
>
{reminderTest.busy ? 'Sending…' : 'Send test notification'}
</button>
</div>
</form>
</section>