// 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 }; }