Reader: whole-Bible search in every translation
Build and Push Docker Image / build-and-push (push) Failing after 20s
Build and Push Docker Image / build-and-push (push) Failing after 20s
BSB and NET are indexed in the browser; ESV, NLT, CSB and NASB are searched through a server proxy (keys stay server-side). Results page in 25 at a time and jump to the verse. Also fixes a stray trailing comma in CSB chapter text. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3279dddae5
commit
c11b6a60bf
8 files changed
+262
-55
No files matched your search
@@ -0,0 +1,49 @@
|
||||
// Whole-Bible search for translations that can't be downloaded and indexed in the
|
||||
// browser (ESV, NLT, CSB, NASB): queries go through the server's search proxy.
|
||||
// Debounced, cancels stale requests, and pages in 25 verses at a time.
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
const DEBOUNCE_MS = 450;
|
||||
const EMPTY = { results: [], total: 0, page: 0, hasMore: false, loading: false, error: '' };
|
||||
|
||||
export function useBibleSearch(translation, query, enabled) {
|
||||
const [state, setState] = useState(EMPTY);
|
||||
const controllerRef = useRef(null);
|
||||
const q = query.trim();
|
||||
|
||||
const run = useCallback(async (page) => {
|
||||
controllerRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
controllerRef.current = controller;
|
||||
setState((s) => ({ ...s, loading: true, error: '' }));
|
||||
try {
|
||||
const res = await fetch(`/api/bible/${translation}/search?q=${encodeURIComponent(q)}&page=${page}`, {
|
||||
credentials: 'include', signal: controller.signal,
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || 'Search failed.');
|
||||
setState((s) => ({
|
||||
results: page === 1 ? data.results : [...s.results, ...data.results],
|
||||
total: data.total, page: data.page, hasMore: data.hasMore, loading: false, error: '',
|
||||
}));
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') return;
|
||||
setState((s) => ({ ...s, loading: false, error: err.message || 'Search failed.' }));
|
||||
}
|
||||
}, [translation, q]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || q.length < 2) {
|
||||
controllerRef.current?.abort();
|
||||
setState(EMPTY);
|
||||
return undefined;
|
||||
}
|
||||
const timer = window.setTimeout(() => run(1), DEBOUNCE_MS);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [enabled, q, run]);
|
||||
|
||||
useEffect(() => () => controllerRef.current?.abort(), []);
|
||||
|
||||
const loadMore = () => { if (!state.loading && state.hasMore) run(state.page + 1); };
|
||||
return { ...state, loadMore, tooShort: q.length < 2 };
|
||||
}
|
||||
Reference in new issue
Block a user