App quality: offline badge, per-note history, keyboard study mode, notes import
- Study cards say whether the Bible text works offline, with a button to save the missing book (notes are always kept on the device). - Each note keeps its last 12 versions with Restore (History under each note). - Keyboard-only study: 1/2/3 write in Observe/Interpret/Apply, C/W/X/P switch resource tabs, V compares, Esc leaves a note box; listed under "?". - Export > Import notes: bring Word/Markdown/text notes into the matching chunks without overwriting anything; can mark them as not your own. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
648c3206df
commit
67645cf1df
14 files changed
+421
-7
No files matched your search
@@ -0,0 +1,61 @@
|
||||
// Export → Import notes: bring notes from a Word or Markdown file into this study, matched to its chunks by passage.
|
||||
import { useState } from 'react';
|
||||
import { useApp } from '../context/AppContext.js';
|
||||
import { useToast } from './Toasts.jsx';
|
||||
import { applyImportedNotes, parseNotesText } from '../utils/importNotes.js';
|
||||
|
||||
export default function ImportNotes() {
|
||||
const { project, updateProject } = useApp();
|
||||
const { toast } = useToast();
|
||||
const [file, setFile] = useState('');
|
||||
const [sections, setSections] = useState(null); // null until a file is read
|
||||
const [own, setOwn] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const choose = async (e) => {
|
||||
const f = e.target.files?.[0];
|
||||
e.target.value = '';
|
||||
setError(''); setSections(null);
|
||||
if (!f) return;
|
||||
try {
|
||||
let text;
|
||||
if (/\.docx$/i.test(f.name)) {
|
||||
const { default: mammoth } = await import('mammoth');
|
||||
text = (await mammoth.extractRawText({ arrayBuffer: await f.arrayBuffer() })).value;
|
||||
} else text = await f.text();
|
||||
const found = parseNotesText(text);
|
||||
setFile(f.name);
|
||||
setSections(found);
|
||||
if (found.length === 0) setError('No passage headings with Observe / Interpret / Apply notes were found in that file. Start each section with a heading like “John 3:16–18”.');
|
||||
} catch { setError('That file couldn’t be read.'); }
|
||||
};
|
||||
|
||||
const preview = sections ? applyImportedNotes(project, sections, { own }) : null;
|
||||
const add = () => {
|
||||
updateProject((current) => applyImportedNotes(current, sections, { own }).project);
|
||||
toast(`Added notes to ${preview.matched.length} chunk${preview.matched.length === 1 ? '' : 's'}`);
|
||||
setSections(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-5" aria-label="Import notes">
|
||||
<h2 className="text-sm font-medium">Import notes</h2>
|
||||
<p className="mb-3 mt-1 text-xs text-[var(--r-sub)]">Bring in notes from a Word (.docx), Markdown or text file. Start each section with a passage heading (“John 3:16–18”), then label the notes “Observe:”, “Interpret:”, “Apply:”, “Background:” or “Questions:”. Notes you’ve already written are kept; imported text is added below them.</p>
|
||||
<label className="inline-block cursor-pointer rounded-xl border border-[var(--r-line)] px-4 py-2 text-sm hover:bg-black/5">Choose a file
|
||||
<input type="file" accept=".docx,.md,.markdown,.txt,text/markdown,text/plain" className="hidden" onChange={choose} />
|
||||
</label>
|
||||
{error && <p role="alert" className="mt-2 text-xs text-[var(--r-danger,#b42318)]">{error}</p>}
|
||||
{preview && sections.length > 0 && (
|
||||
<div className="mt-3 space-y-2 text-sm">
|
||||
<p className="text-xs text-[var(--r-sub)]">{file}: {sections.length} section{sections.length === 1 ? '' : 's'} found.</p>
|
||||
<ul className="space-y-1 text-xs">
|
||||
{preview.matched.map((m) => <li key={m.heading}>✓ {m.heading}: {m.fields.join(', ')}</li>)}
|
||||
{preview.unmatched.map((h) => <li key={h} className="text-[var(--r-sub)]">No chunk for {h}, skipped</li>)}
|
||||
</ul>
|
||||
<label className="flex items-center gap-2 text-xs text-[var(--r-sub)]"><input type="checkbox" checked={own} onChange={(e) => setOwn(e.target.checked)} className="accent-[var(--r-accent)]" /> These notes are my own writing (untick if they came from AI or another source)</label>
|
||||
<button type="button" onClick={add} disabled={preview.matched.length === 0} className="rounded-xl bg-[var(--r-text)] px-4 py-2 text-sm font-medium text-[var(--r-bg)] disabled:opacity-40">Add to this study</button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user