Files
study-bible-project/src/components/settings/DataSection.jsx
T

120 lines
6.4 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.
// Settings → "Your data": download everything as one file, restore from one, and save Bibles for offline.
import { useRef, useState } from 'react';
import { useApp } from '../../context/AppContext.js';
import { useToast } from '../Toasts.jsx';
import { DownloadProgress } from '../OfflineSave.jsx';
import { useOfflineSave } from '../../hooks/useOfflineSave.js';
import { backupFileName, buildBackup, markBackedUp, restoreProjects, validateBackup } from '../../utils/backup.js';
import { saveRemoteProject } from '../../syncService.js';
const btn = 'rounded-xl border border-[var(--r-line)] px-4 py-2 text-sm hover:bg-black/5 disabled:opacity-50';
const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
export default function DataSection() {
const {
authUser, readerBookmarks, readerBookmarkNotes, readerTextHighlights, readerColorLabels, readerInkByPage, readingPlansState,
importReaderMarks, importReadingPlans, refreshProjectIndex, prayers, importPrayers,
} = useApp();
const { toast } = useToast();
const offline = useOfflineSave();
const fileRef = useRef(null);
const [pending, setPending] = useState(null); // { name, backup, summary } waiting for "Restore"
const [error, setError] = useState('');
const [busy, setBusy] = useState(false);
const download = () => {
const backup = buildBackup({
email: authUser?.email ?? null,
reader: { bookmarks: readerBookmarks, bookmarkNotes: readerBookmarkNotes, highlights: readerTextHighlights, colorLabels: readerColorLabels, ink: readerInkByPage },
readingPlans: readingPlansState,
prayers,
});
const url = URL.createObjectURL(new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' }));
const link = document.createElement('a');
link.href = url;
link.download = backupFileName();
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
markBackedUp();
toast(`Backup downloaded: ${plural(backup.projects.length, 'study', 'studies')} and your reading marks`);
};
const choose = async (e) => {
const file = e.target.files?.[0];
e.target.value = '';
setError('');
setPending(null);
if (!file) return;
let data;
try { data = JSON.parse(await file.text()); } catch { setError('That file couldn’t be read as a backup.'); return; }
const checked = validateBackup(data);
if (!checked.ok) { setError(checked.error); return; }
setPending({ name: file.name, ...checked });
};
const restore = async () => {
setBusy(true);
const { backup } = pending;
const written = restoreProjects(backup.projects);
importReaderMarks(backup.reader);
importReadingPlans(backup.readingPlans);
importPrayers?.(backup.prayers);
refreshProjectIndex();
// Studies restored here also go to the account, so they follow you to other devices
if (authUser) await Promise.all(written.map((p) => saveRemoteProject(p)));
setBusy(false);
setPending(null);
toast(`Restored ${plural(written.length, 'study', 'studies')}, plus your bookmarks, highlights and plans`);
};
const s = pending?.summary;
return (
<section id="backup" aria-label="Your data" className="scroll-mt-16 space-y-5 rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-6">
<div>
<h2 className="text-base font-medium">Your data</h2>
<p className="mt-1 text-sm text-[var(--r-sub)]">Everything you’ve made — studies and notes, bookmarks, highlights, ink and reading plans — in one file you keep.{authUser ? ' “Download backup” is what’s on this device; “Download from my account” is everything the server holds. The server also keeps a daily snapshot for the administrator.' : ''}</p>
</div>
<div className="flex flex-wrap items-center gap-3">
<button type="button" onClick={download} className={btn}>Download backup</button>
{authUser && (
<a href="/api/account/export" download onClick={() => markBackedUp()} className={`${btn} inline-block`} title="Everything saved to your account on the server, including studies from other devices">Download from my account</a>
)}
<button type="button" onClick={() => fileRef.current?.click()} className={btn}>Restore from a backup…</button>
<input ref={fileRef} type="file" accept="application/json,.json" onChange={choose} className="hidden" aria-label="Backup file" />
</div>
{error && <p role="alert" className="text-sm text-[var(--r-danger,#b42318)]">{error}</p>}
{pending && (
<div className="rounded-xl border border-[var(--r-line)] bg-[var(--r-bg)] p-4 text-sm">
<p className="font-medium">{pending.name}</p>
<p className="mt-1 text-[var(--r-sub)]">
Contains {plural(s.projects, 'study', 'studies')}, {plural(s.bookmarks, 'bookmark')}, {plural(s.highlights, 'highlight')}, {plural(s.inkPages, 'ink page')}, {plural(s.plans, 'reading plan')} and {plural(s.prayers ?? 0, 'prayer')}.
Restoring adds these to what you have — nothing is deleted, and a study on this device is only replaced if the backup’s copy is newer.
</p>
<div className="mt-3 flex gap-2">
<button type="button" onClick={restore} disabled={busy} className="rounded-xl bg-[var(--r-accent)] px-4 py-2 text-sm font-medium text-[var(--r-on)] hover:opacity-90 disabled:opacity-50">{busy ? 'Restoring…' : 'Restore'}</button>
<button type="button" onClick={() => setPending(null)} className={btn}>Cancel</button>
</div>
</div>
)}
<div className="border-t border-[var(--r-line)] pt-4">
<h3 className="text-sm font-medium">Read offline</h3>
<p className="mb-3 mt-1 text-sm text-[var(--r-sub)]">Chapters you read are kept automatically. To read anywhere, save a whole Bible while you’re connected (about 4 MB). The ESV, NLT, CSB and NASB can’t be saved: their licences don’t allow it.</p>
{offline.job ? <DownloadProgress job={offline.job} onCancel={offline.cancel} /> : (
<div className="flex flex-wrap gap-3">
{['BSB', 'NET', 'KJV', 'ASV', 'LSV'].map((t) => (
<button key={t} type="button" onClick={() => offline.saveBible(t)} className={btn}>
{(offline.saved[t] ?? []).length >= 66 ? `✓ The whole ${t} is saved` : `Save the whole ${t}`}
</button>
))}
</div>
)}
</div>
</section>
);
}