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
@@ -0,0 +1,115 @@
|
||||
// The signed-in user's saved projects: opening, renaming, deleting, and keeping
|
||||
// this device's copies in step with the server.
|
||||
import { useEffect, useState } from 'react';
|
||||
import { loadProjectIndex } from '../utils/bibleUtils.jsx';
|
||||
import {
|
||||
saveProjectToStorage,
|
||||
deleteProjectFromStorage,
|
||||
loadProjectById,
|
||||
} from '../utils/projectStorage.js';
|
||||
import {
|
||||
saveRemoteProject,
|
||||
deleteRemoteProject,
|
||||
listRemoteProjects,
|
||||
loadRemoteProject,
|
||||
} from '../syncService.js';
|
||||
|
||||
export function useProjectLibrary({
|
||||
authUser,
|
||||
project,
|
||||
setProject,
|
||||
setProjectIndex,
|
||||
setCurrentPage,
|
||||
setActiveChapterIndex,
|
||||
}) {
|
||||
const [staleLocalProjects, setStaleLocalProjects] = useState([]); // projects where server is newer
|
||||
const [autoRestoredCount, setAutoRestoredCount] = useState(null); // brief "synced from another device" toast
|
||||
|
||||
// Once we know who's signed in, reconcile the local project index against the server.
|
||||
// Runs on every authUser change (including logout -> different login) so a previous
|
||||
// account's stale suggestions never linger after switching users. Projects that exist
|
||||
// on the server but not on this device (e.g. a brand new browser) are pulled down
|
||||
// automatically instead of requiring a manual "Restore" click per project.
|
||||
useEffect(() => {
|
||||
if (!authUser) {
|
||||
setStaleLocalProjects([]);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
const localIndex = loadProjectIndex();
|
||||
listRemoteProjects().then(async (result) => {
|
||||
if (!result.ok || cancelled) return;
|
||||
const localMap = new Map(localIndex.map((e) => [e.id, e]));
|
||||
|
||||
const missing = result.data.filter((e) => !localMap.has(e.id));
|
||||
if (missing.length > 0) {
|
||||
const fetched = await Promise.all(missing.map((e) => loadRemoteProject(e.id)));
|
||||
fetched.filter((r) => r.ok).forEach((r) => saveProjectToStorage(r.data));
|
||||
if (cancelled) return;
|
||||
const restoredCount = fetched.filter((r) => r.ok).length;
|
||||
if (restoredCount > 0) {
|
||||
setProjectIndex(loadProjectIndex());
|
||||
setAutoRestoredCount(restoredCount);
|
||||
window.setTimeout(() => setAutoRestoredCount(null), 4000);
|
||||
}
|
||||
}
|
||||
|
||||
const stale = result.data.filter((e) => {
|
||||
const local = localMap.get(e.id);
|
||||
return local && (e.lastEdited ?? 0) > (local.lastEdited ?? 0);
|
||||
});
|
||||
if (!cancelled) setStaleLocalProjects(stale);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [authUser]);
|
||||
|
||||
const resumeProject = (id) => {
|
||||
const loaded = loadProjectById(id);
|
||||
if (!loaded) return;
|
||||
setProject(loaded);
|
||||
setActiveChapterIndex(0);
|
||||
const loadedChunks = loaded.chapters.flatMap((ch) => ch.chunks);
|
||||
const hasSelectedChunk = loaded.selectedChunkId
|
||||
&& loadedChunks.some((c) => c.id === loaded.selectedChunkId);
|
||||
setCurrentPage(hasSelectedChunk ? 'study' : 'setup');
|
||||
};
|
||||
|
||||
const renameProjectInStorage = (id, title) => {
|
||||
const loaded = loadProjectById(id);
|
||||
if (!loaded) return;
|
||||
const updated = { ...loaded, title, lastEdited: Date.now() };
|
||||
saveProjectToStorage(updated);
|
||||
setProjectIndex(loadProjectIndex());
|
||||
saveRemoteProject(updated);
|
||||
if (project?.id === id) {
|
||||
setProject((current) => (current ? { ...current, title } : current));
|
||||
}
|
||||
};
|
||||
|
||||
const deleteProject = (id) => {
|
||||
if (!window.confirm('Delete this project? This cannot be undone.')) return;
|
||||
deleteProjectFromStorage(id);
|
||||
setProjectIndex(loadProjectIndex());
|
||||
deleteRemoteProject(id); // fire-and-forget
|
||||
};
|
||||
|
||||
const pullLatestFromServer = async (id) => {
|
||||
const result = await loadRemoteProject(id);
|
||||
if (!result.ok) {
|
||||
alert('Could not pull latest version from server.');
|
||||
return;
|
||||
}
|
||||
saveProjectToStorage(result.data);
|
||||
setProjectIndex(loadProjectIndex());
|
||||
setStaleLocalProjects((prev) => prev.filter((e) => e.id !== id));
|
||||
};
|
||||
|
||||
return {
|
||||
staleLocalProjects,
|
||||
autoRestoredCount,
|
||||
resumeProject,
|
||||
renameProjectInStorage,
|
||||
deleteProject,
|
||||
pullLatestFromServer,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// A simple "read this book in N weeks" plan, stored per browser.
|
||||
import { useState } from 'react';
|
||||
import { CHAPTER_COUNTS } from '../utils/bibleUtils.jsx';
|
||||
|
||||
export function useReadingPlan() {
|
||||
const [readingPlan, setReadingPlan] = useState(() => {
|
||||
try { return JSON.parse(localStorage.getItem('readingPlan') || 'null'); } catch { return null; }
|
||||
});
|
||||
|
||||
const createReadingPlan = (bookAbbrev, bookName, weeks) => {
|
||||
const totalChapters = CHAPTER_COUNTS[bookAbbrev] ?? 1;
|
||||
const plan = { bookAbbrev, bookName, totalChapters, startDate: Date.now(), targetDate: Date.now() + weeks * 7 * 24 * 60 * 60 * 1000, chaptersRead: [] };
|
||||
setReadingPlan(plan);
|
||||
try { localStorage.setItem('readingPlan', JSON.stringify(plan)); } catch {}
|
||||
};
|
||||
|
||||
const markChapterRead = (chapter) => {
|
||||
setReadingPlan((prev) => {
|
||||
if (!prev) return prev;
|
||||
const chaptersRead = prev.chaptersRead.includes(chapter)
|
||||
? prev.chaptersRead.filter((c) => c !== chapter)
|
||||
: [...prev.chaptersRead, chapter].sort((a, b) => a - b);
|
||||
const next = { ...prev, chaptersRead };
|
||||
try { localStorage.setItem('readingPlan', JSON.stringify(next)); } catch {}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const clearReadingPlan = () => {
|
||||
setReadingPlan(null);
|
||||
try { localStorage.removeItem('readingPlan'); } catch {}
|
||||
};
|
||||
|
||||
return { readingPlan, createReadingPlan, markChapterRead, clearReadingPlan };
|
||||
}
|
||||
Reference in new issue
Block a user