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>
75 lines
3.7 KiB
JavaScript
75 lines
3.7 KiB
JavaScript
// 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 };
|
|
}
|