From 90fd5c4e9ee2b388b53b35aa3abccf4c3e1b9e7b Mon Sep 17 00:00:00 2001 From: nmemmert Date: Thu, 13 Aug 2026 16:08:25 -0400 Subject: [PATCH] Add fixed bottom chunk nav and IndexedDB Bible search cache - Replace non-functional sticky nav with a fixed bottom bar (position: fixed) that stays visible while scrolling long study notes; hides in draw mode - Add pb-20 to main so the bar never covers the last study section - Cache whole-Bible search index (BSB complete.json, ~7MB) in IndexedDB with a 7-day TTL so repeated searches skip the network fetch entirely Co-Authored-By: Claude Sonnet 4.6 --- SUGGESTIONS.md | 2 -- src/App.jsx | 60 +++++++++++++++++++++++++++++++++++++++++ src/pages/StudyPage.jsx | 51 +++++++++++++++++++---------------- 3 files changed, 88 insertions(+), 25 deletions(-) diff --git a/SUGGESTIONS.md b/SUGGESTIONS.md index 25bf332..8f7f285 100644 --- a/SUGGESTIONS.md +++ b/SUGGESTIONS.md @@ -22,10 +22,8 @@ _Refreshed 2026-07-06 (multiple passes) — items already shipped have been remo ## UX / UI ### Study Page -- **Sticky bottom nav** — top Prev/Next chunk nav shipped (commit `103e20c`); a matching sticky bottom bar for long chunks would avoid scroll-back ### Reader -- **Whole-Bible search index isn't persisted** — the ~7MB `complete.json` fetch is cached in-memory only for the session; a page reload re-downloads it. Worth persisting to IndexedDB (not localStorage — too small) if this gets used often - **Bookmark color picker is still an emoji button** (🎨) — the bookmark/copy icons became proper SVGs, but color-cycling didn't get the same treatment ### Home Page diff --git a/src/App.jsx b/src/App.jsx index f4f3434..f6ab340 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1072,6 +1072,56 @@ export function CrossRefChip({ label, onRemove, loadVerseText }) { ); } +// --------------------------------------------------------------------------- +// IndexedDB helpers for whole-Bible search cache +// --------------------------------------------------------------------------- + +const BIBLE_IDB_NAME = 'study-app-bible-index'; +const BIBLE_IDB_STORE = 'bible-index'; +const BIBLE_IDB_TTL = 7 * 24 * 60 * 60 * 1000; // 7 days + +function openBibleIndexDB() { + return new Promise((resolve, reject) => { + const req = indexedDB.open(BIBLE_IDB_NAME, 1); + req.onupgradeneeded = (e) => { + const db = e.target.result; + if (!db.objectStoreNames.contains(BIBLE_IDB_STORE)) { + db.createObjectStore(BIBLE_IDB_STORE, { keyPath: 'id' }); + } + }; + req.onsuccess = (e) => resolve(e.target.result); + req.onerror = () => reject(req.error); + }); +} + +async function getBibleIndexFromIDB(translation) { + try { + const db = await openBibleIndexDB(); + return new Promise((resolve, reject) => { + const tx = db.transaction(BIBLE_IDB_STORE, 'readonly'); + const req = tx.objectStore(BIBLE_IDB_STORE).get(translation); + req.onsuccess = () => resolve(req.result ?? null); + req.onerror = () => reject(req.error); + }); + } catch { + return null; + } +} + +async function saveBibleIndexToIDB(translation, flat) { + try { + const db = await openBibleIndexDB(); + await new Promise((resolve, reject) => { + const tx = db.transaction(BIBLE_IDB_STORE, 'readwrite'); + const req = tx.objectStore(BIBLE_IDB_STORE).put({ id: translation, flat, cachedAt: Date.now() }); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + }); + } catch { + // Non-fatal; in-memory cache still works for this session + } +} + // --------------------------------------------------------------------------- // App component // --------------------------------------------------------------------------- @@ -1388,6 +1438,14 @@ const App = () => { } setBibleIndexStatus('loading'); try { + // Check IndexedDB cache before hitting the network (~7 MB fetch) + const cached = await getBibleIndexFromIDB('BSB'); + if (cached?.flat && Date.now() - cached.cachedAt < BIBLE_IDB_TTL) { + _bibleIndexCacheRef.current.BSB = cached.flat; + setBibleIndexStatus('ready'); + return; + } + const res = await fetch('https://bible.helloao.org/api/BSB/complete.json'); if (!res.ok) throw new Error('Unable to load the full Bible for search.'); const data = await res.json(); @@ -1404,6 +1462,8 @@ const App = () => { } _bibleIndexCacheRef.current.BSB = flat; setBibleIndexStatus('ready'); + // Persist for future visits — non-blocking + saveBibleIndexToIDB('BSB', flat); } catch { setBibleIndexStatus('error'); } diff --git a/src/pages/StudyPage.jsx b/src/pages/StudyPage.jsx index 3b65308..9ec326c 100644 --- a/src/pages/StudyPage.jsx +++ b/src/pages/StudyPage.jsx @@ -76,7 +76,7 @@ export default function StudyPage() { -
+
{/* Sidebar */}
+ {/* Prev / Next — fixed bottom bar, visible while scrolling long chunks */} + {selectedChunk && studyLayout !== 'annotate' && ( +
+
+ + + {selectedChunkGlobalIndex + 1} / {allChunks.length} + + +
+
+ )} + {/* Greek word picker modal */} {suggestModal && createPortal(