// Free study data from HelloAO: public-domain commentaries and the Theographic // people/places/events dataset. Both are keyed by book + chapter, so results are // cached per chapter for the rest of the visit. import { useEffect, useState } from 'react'; import { renderVerseContent } from '../utils/bibleUtils.jsx'; const API = 'https://bible.helloao.org/api'; export const COMMENTARIES = [ { id: 'matthew-henry', name: 'Matthew Henry', credit: 'Matthew Henry Bible Commentary (public domain).' }, { id: 'john-gill', name: 'John Gill', credit: 'John Gill Bible Commentary (public domain).' }, { id: 'jamieson-fausset-brown', name: 'Jamieson-Fausset-Brown', credit: 'Jamieson-Fausset-Brown Bible Commentary (public domain).' }, { id: 'tyndale', name: 'Tyndale Study Notes', credit: 'Tyndale Open Study Notes © Tyndale House Foundation, licensed CC BY-SA 4.0.' }, { id: 'adam-clarke', name: 'Adam Clarke', credit: 'Adam Clarke Bible Commentary (public domain).' }, { id: 'john-calvin', name: 'John Calvin', credit: "John Calvin's Commentaries (public domain)." }, { id: 'keil-delitzsch', name: 'Keil & Delitzsch (OT)', credit: 'Keil & Delitzsch Old Testament Commentary (public domain).' }, ]; const commentaryCache = new Map(); // "id/BOOK/ch" -> Promise const theographicCache = new Map(); // "BOOK/ch" -> Promise<{people, places, events} | null> const personCache = new Map(); // id -> Promise const getJson = (url) => fetch(url).then((r) => (r.ok ? r.json() : null)).catch(() => null); // Resolves to [{ number, text }] sorted by verse, or null when the commentary // doesn't cover that book/chapter. export function loadCommentary(id, book, chapter) { const key = `${id}/${book}/${chapter}`; if (!commentaryCache.has(key)) { commentaryCache.set(key, getJson(`${API}/c/${id}/${book}/${chapter}.json`).then((data) => { const items = data?.chapter?.content; if (!Array.isArray(items)) return null; return items .filter((it) => it?.type === 'verse') .map((it) => ({ number: it.number, text: renderVerseContent(it.content).replace(/[ \t]+\n/g, '\n').trim() })) .filter((it) => it.text) .sort((a, b) => a.number - b.number); })); } return commentaryCache.get(key); } export function loadTheographic(book, chapter) { const key = `${book}/${chapter}`; if (!theographicCache.has(key)) { theographicCache.set(key, getJson(`${API}/d/theographic/${book}/${chapter}.json`).then((data) => ( data?.chapter ? { people: data.chapter.people ?? [], places: data.chapter.places ?? [], events: data.chapter.events ?? [] } : null ))); } return theographicCache.get(key); } export function loadPerson(id) { if (!personCache.has(id)) { personCache.set(id, getJson(`${API}/d/theographic/people/${id}.json`).then((d) => d?.person ?? null)); } return personCache.get(id); } // For verse N, the commentary entry that starts at N, or otherwise the closest // earlier one (several commentaries write on whole passages, not each verse). export function pickCommentaryEntry(entries, verse) { if (!entries?.length) return null; let best = null; for (const e of entries) { if (e.number <= verse) best = e; else break; } return best; } // Small state wrapper: { status: 'loading' | 'ready' | 'empty', data }. export function useAsync(loader, deps) { const [state, setState] = useState({ status: 'loading', data: null }); useEffect(() => { let active = true; setState({ status: 'loading', data: null }); loader().then((data) => { if (active) setState({ status: data ? 'ready' : 'empty', data }); }); return () => { active = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, deps); return state; }