Files
study-bible-project/src/components/VersionHistory.jsx
T
nmemmertandClaude Sonnet 5.5 207bfcb919
Build and Push Docker Image / build-and-push (push) Successful in 5m13s
Version history, notes on highlights and bookmarks, "you've studied this", prayer journal
- 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>
2026-10-06 10:45:32 -04:00

68 lines
4.0 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Export → Earlier versions: the copies the server kept of this study as you worked. Look at what is different
// from now, and put one back. The version you replace is kept, so putting one back can be undone too.
import { useCallback, useEffect, useState } from 'react';
import { getStudyVersion, listStudyVersions } from '../syncService.js';
import { describeDiff, diffProjects } from '../utils/projectDiff.js';
const btn = 'rounded-lg border border-[var(--r-line)] px-3 py-1.5 text-xs hover:bg-black/5 disabled:opacity-50';
export default function VersionHistory({ project, onRestore }) {
const [state, setState] = useState({ status: 'loading', versions: [] });
const [open, setOpen] = useState(null); // { id, lines } the version being looked at
const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
const load = useCallback(async () => {
if (!project?.id) return;
const r = await listStudyVersions(project.id);
setState(r.ok ? { status: 'ready', versions: r.data.versions } : { status: 'unavailable', versions: [] });
}, [project?.id]);
useEffect(() => { load(); }, [load]);
const preview = async (v) => {
if (open?.id === v.id) { setOpen(null); return; }
setError('');
const r = await getStudyVersion(project.id, v.id);
if (!r.ok) { setError(r.error ?? 'Could not open that version.'); return; }
setOpen({ id: v.id, lines: describeDiff(diffProjects(project, r.data)) });
};
const restore = async (v) => {
if (!window.confirm(`Put back the version from ${new Date(v.savedAt).toLocaleString()}? What you have now is kept in this list, so you can undo it.`)) return;
setBusy(true); setError('');
const ok = await onRestore(v.id);
setBusy(false);
if (ok) { setOpen(null); load(); } else setError('Could not restore that version. Check your connection and try again.');
};
return (
<section className="rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-5" aria-label="Earlier versions">
<h2 className="text-sm font-medium">Earlier versions</h2>
<p className="mb-3 mt-1 text-xs text-[var(--r-sub)]">As you work, your account keeps earlier copies of this study (about one for every ten minutes of editing, the latest 30). Use one to undo a bad merge or a deletion.</p>
{state.status === 'loading' && <p className="text-xs text-[var(--r-sub)]">Loading…</p>}
{state.status === 'unavailable' && <p className="text-xs text-[var(--r-sub)]">Earlier versions need your account and a connection, and the study has to have synced once.</p>}
{state.status === 'ready' && state.versions.length === 0 && <p className="text-xs text-[var(--r-sub)]">Nothing kept yet. The first earlier version appears after you’ve changed the study a couple of times.</p>}
{state.versions.length > 0 && (
<ul className="divide-y divide-[var(--r-line)] text-sm">
{state.versions.map((v) => (
<li key={v.id} className="py-2">
<div className="flex flex-wrap items-center justify-between gap-2">
<span><span className="font-medium">{new Date(v.savedAt).toLocaleString()}</span> <span className="text-xs text-[var(--r-sub)]">{v.chunks} chunk{v.chunks === 1 ? '' : 's'} · {v.words} words</span></span>
<span className="flex gap-2">
<button type="button" onClick={() => preview(v)} aria-expanded={open?.id === v.id} className={btn}>{open?.id === v.id ? 'Hide' : 'What’s different'}</button>
<button type="button" onClick={() => restore(v)} disabled={busy} className={btn}>Put this back</button>
</span>
</div>
{open?.id === v.id && (
<ul className="mt-2 list-disc space-y-0.5 rounded-lg bg-[var(--r-bg)] py-2 pl-7 pr-3 text-xs text-[var(--r-sub)]">
{open.lines.map((line) => <li key={line}>{line}</li>)}
</ul>
)}
</li>
))}
</ul>
)}
{error && <p role="alert" className="mt-2 text-xs text-[var(--r-danger,#b42318)]">{error}</p>}
</section>
);
}