Split Home: local UI state, useProjectLibrary, useReadingPlan
- Search, tag filter, full-text note search, and rename state live in HomePage. The sort choice is remembered in localStorage. - useProjectLibrary owns opening/renaming/deleting projects and the server reconcile (auto-restore + newer-on-server list). - useReadingPlan owns the reading plan shared by Home and Reader. - Tests: note search, rename, delete, sort memory, reading plan. Test setup now resets the storage namespace between tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8bb3b104c8
commit
a7e00a76b5
5 files changed
+291
-178
No files matched your search
+54
-8
@@ -1,6 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useApp } from '../context/AppContext.js';
|
||||
import { bookOptions, CHAPTER_COUNTS, formatRelativeDate } from '../utils/bibleUtils.jsx';
|
||||
import {
|
||||
bookOptions,
|
||||
CHAPTER_COUNTS,
|
||||
formatChunkReference,
|
||||
formatRelativeDate,
|
||||
loadProjectIndex,
|
||||
} from '../utils/bibleUtils.jsx';
|
||||
import { loadProjectById } from '../utils/projectStorage.js';
|
||||
|
||||
export default function HomePage() {
|
||||
const {
|
||||
@@ -8,18 +15,12 @@ export default function HomePage() {
|
||||
autoRestoredCount,
|
||||
staleLocalProjects,
|
||||
projectIndex,
|
||||
homeSearch, setHomeSearch,
|
||||
homeSort, setHomeSort,
|
||||
homeTagFilter, setHomeTagFilter,
|
||||
homeFullTextResults,
|
||||
readingPlan,
|
||||
createReadingPlan,
|
||||
clearReadingPlan,
|
||||
audioBook, setAudioBook,
|
||||
audioNarrator, setAudioNarrator,
|
||||
audioState,
|
||||
renamingId, setRenamingId,
|
||||
renameValue, setRenameValue,
|
||||
openBibleReader,
|
||||
openImportProject,
|
||||
openNewProject,
|
||||
@@ -31,6 +32,51 @@ export default function HomePage() {
|
||||
handleStopBookAudio,
|
||||
handleToggleBookAudioPause,
|
||||
} = useApp();
|
||||
const [homeSearch, setHomeSearch] = useState('');
|
||||
const [homeSort, setHomeSort] = useState(() => {
|
||||
try { return localStorage.getItem('homeSort') || 'recent'; } catch { return 'recent'; }
|
||||
}); // 'recent' | 'title' | 'passage'
|
||||
const [homeTagFilter, setHomeTagFilter] = useState('');
|
||||
const [homeFullTextResults, setHomeFullTextResults] = useState(null); // null=idle, []=no match, [...]= results
|
||||
const fullTextTimerRef = useRef(null);
|
||||
const [renamingId, setRenamingId] = useState(null);
|
||||
const [renameValue, setRenameValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
try { localStorage.setItem('homeSort', homeSort); } catch {}
|
||||
}, [homeSort]);
|
||||
|
||||
// Full-text search across all project notes (debounced, synchronous localStorage read)
|
||||
useEffect(() => {
|
||||
clearTimeout(fullTextTimerRef.current);
|
||||
const q = homeSearch.trim().toLowerCase();
|
||||
if (q.length < 3) { setHomeFullTextResults(null); return; }
|
||||
fullTextTimerRef.current = window.setTimeout(() => {
|
||||
const index = loadProjectIndex();
|
||||
const results = [];
|
||||
for (const entry of index) {
|
||||
const proj = loadProjectById(entry.id);
|
||||
if (!proj) continue;
|
||||
const matches = [];
|
||||
for (const [chIdx, ch] of (proj.chapters ?? []).entries()) {
|
||||
for (const chunk of (ch.chunks ?? [])) {
|
||||
for (const field of ['observation', 'interpretation', 'application', 'generalNotes', 'finalScript']) {
|
||||
const text = (chunk[field] ?? '').toLowerCase();
|
||||
if (!text.includes(q)) continue;
|
||||
const idx = text.indexOf(q);
|
||||
const raw = chunk[field] ?? '';
|
||||
const start = Math.max(0, idx - 40);
|
||||
const snippet = (start > 0 ? '…' : '') + raw.slice(start, idx + q.length + 60) + (idx + q.length + 60 < raw.length ? '…' : '');
|
||||
matches.push({ chunkId: chunk.id, ref: formatChunkReference(proj, chIdx, chunk, '–'), field, snippet });
|
||||
break; // one match per chunk
|
||||
}
|
||||
}
|
||||
}
|
||||
if (matches.length > 0) results.push({ projectId: entry.id, projectTitle: entry.title, matches });
|
||||
}
|
||||
setHomeFullTextResults(results);
|
||||
}, 300);
|
||||
}, [homeSearch]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 text-slate-900">
|
||||
|
||||
Reference in new issue
Block a user