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,67 @@
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user