Version history, notes on highlights and bookmarks, "you've studied this", prayer journal
Build and Push Docker Image / build-and-push (push) Successful in 5m13s

- Earlier versions of a study: the server keeps compressed copies (about one per ten minutes of
  editing, the latest 30); Export shows what differs and puts one back (the replaced copy is kept)
- Notes on highlights and bookmarks: synced, merged without loss, in exports and backups
- Reader verse panel lists your studies and chunks that cover the verse, and opens them
- Prayer journal: requests, who for, linked verses, days prayed, answers; synced, in backups
  and the account export; daily prayer reminder through ntfy
- TODO.md lists the ideas not built yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-06 10:45:32 -04:00
1 parent 54cb1c217d
commit 207bfcb919
45 files changed
+1669 -75

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
// The prayer journal on this device, kept in step with your account. Every change is saved here at once and
// sent to the account a moment later; signing in merges the two (the newer version of each prayer wins), so a prayer
// added on your phone appears on your computer. Signing out clears this device's copy.
import { useCallback, useEffect, useRef, useState } from 'react';
import { getRemotePrayers, saveRemotePrayers } from '../syncService.js';
import { markAnswered, markDeleted, mergePrayers, newPrayer, prayersEqual, reopen, touch, togglePrayedToday } from '../utils/prayers.js';
import { localDateString } from '../utils/readingPlans.js';
const KEY = 'prayers';
const PUSH_DELAY_MS = 1500;
const load = () => { try { const v = JSON.parse(localStorage.getItem(KEY) || '[]'); return Array.isArray(v) ? v : []; } catch { return []; } };
const save = (list) => { try { localStorage.setItem(KEY, JSON.stringify(list)); } catch { /* storage unavailable */ } };
export function usePrayers() {
const [prayers, setPrayersState] = useState(load);
const ref = useRef(prayers);
ref.current = prayers;
const enabledRef = useRef(false);
const timerRef = useRef(null);
const push = useCallback(async () => {
window.clearTimeout(timerRef.current);
if (!enabledRef.current) return;
const result = await saveRemotePrayers(ref.current);
if (result.ok) {
const merged = mergePrayers(ref.current, result.data?.prayers);
if (!prayersEqual(merged, ref.current)) { ref.current = merged; save(merged); setPrayersState(merged); }
}
}, []);
const set = useCallback((next) => {
ref.current = next;
save(next);
setPrayersState(next);
if (enabledRef.current) { window.clearTimeout(timerRef.current); timerRef.current = window.setTimeout(push, PUSH_DELAY_MS); }
}, [push]);
const replace = (id, fn) => set(ref.current.map((p) => (p.id === id ? fn(p) : p)));
const addPrayer = (fields) => { const p = newPrayer(fields); set([p, ...ref.current]); return p; };
const editPrayer = (id, patch) => replace(id, (p) => touch(p, patch));
const togglePrayed = (id) => replace(id, (p) => togglePrayedToday(p, localDateString()));
const answerPrayer = (id, note) => replace(id, (p) => markAnswered(p, note));
const reopenPrayer = (id) => replace(id, (p) => reopen(p));
// Deleting leaves a marker so the other devices drop it too; the returned function puts it back
const deletePrayer = (id) => {
const before = ref.current.find((p) => p.id === id);
if (!before) return () => {};
set(ref.current.map((p) => (p.id === id ? markDeleted(p) : p)));
return () => set(ref.current.map((p) => (p.id === id ? touch(before, {}) : p)));
};
// Prayers from a backup file are added; a prayer already here keeps whichever copy is newer
const importPrayers = (list) => set(mergePrayers(ref.current, list));
const pull = useCallback(async () => {
enabledRef.current = true;
const result = await getRemotePrayers();
if (!result.ok) return;
const merged = mergePrayers(ref.current, result.data?.prayers);
if (!prayersEqual(merged, ref.current)) { ref.current = merged; save(merged); setPrayersState(merged); }
if (!prayersEqual(merged, result.data?.prayers ?? [])) await push();
}, [push]);
const stop = useCallback(() => {
enabledRef.current = false;
window.clearTimeout(timerRef.current);
try { localStorage.removeItem(KEY); } catch { /* storage unavailable */ }
ref.current = [];
setPrayersState([]);
}, []);
useEffect(() => () => window.clearTimeout(timerRef.current), []);
return { prayers, addPrayer, editPrayer, togglePrayed, answerPrayer, reopenPrayer, deletePrayer, importPrayers, pullPrayers: pull, stopPrayers: stop };
}