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>
50 lines
2.0 KiB
JavaScript
50 lines
2.0 KiB
JavaScript
// 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 };
|
|
}
|