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
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:
1 parent
54cb1c217d
commit
207bfcb919
45 files changed
+1669
-75
No files matched your search
@@ -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 };
|
||||
}
|
||||
Reference in new issue
Block a user