Add NLT, CSB and NASB via api.bible proxy; new app logo and icons
Build and Push Docker Image / build-and-push (push) Failing after 21s

Reader and Reading Plans get a translation picker. Licensed translations are
fetched through a server-side api.bible proxy so the key stays off the client.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-09-29 08:37:48 -04:00
1 parent 986d2e9331
commit 22c80d2bc3
13 files changed
+209 -20

No files matched your search

+48 -9
View File
@@ -2,14 +2,31 @@
// Audio playback stays in App because the study page's book player shares
// the same <audio> element.
import { useCallback, useEffect, useRef, useState } from 'react';
import { bookOptions, parseBibleChapter } from '../utils/bibleUtils.jsx';
import { bookOptions, CHAPTER_COUNTS, parseBibleChapter } from '../utils/bibleUtils.jsx';
import { BIBLE_IDB_TTL, getBibleIndexFromIDB, saveBibleIndexToIDB } from '../utils/bibleIndexCache.js';
export const READER_TRANSLATIONS = [
{ id: 'BSB', name: 'Berean Standard Bible' },
{ id: 'NLT', name: 'New Living Translation', licensed: true,
notice: 'Holy Bible, New Living Translation, Copyright © 2014, Tyndale House Publishers. All rights reserved. tyndale.com' },
{ id: 'CSB', name: 'Christian Standard Bible', licensed: true,
notice: 'Christian Standard Bible® and CSB® are federally registered trademarks of Holman Bible Publishers. All rights reserved. bhpublishinggroup.com' },
{ id: 'NASB', name: 'New American Standard Bible', licensed: true,
notice: 'New American Standard Bible®, Copyright © 1995, The Lockman Foundation. All rights reserved. lockman.org' },
];
const BOOKMARK_COLORS = ['#fef08a', '#bbf7d0', '#bfdbfe', '#fecaca', '#e9d5ff'];
export function useBibleReader(currentPage) {
const [readerBookAbbrev, setReaderBookAbbrev] = useState(bookOptions[0].abbrev);
const [readerChapter, setReaderChapter] = useState(1);
const [readerTranslation, setReaderTranslationState] = useState(() => {
try {
const saved = localStorage.getItem('reader-translation');
return READER_TRANSLATIONS.some((t) => t.id === saved) ? saved : 'BSB';
} catch { return 'BSB'; }
});
const [readerCopyright, setReaderCopyright] = useState('');
const [readerVerses, setReaderVerses] = useState([]);
const [readerTotalChapters, setReaderTotalChapters] = useState(1);
const [readerLoading, setReaderLoading] = useState(false);
@@ -89,19 +106,39 @@ export function useBibleReader(currentPage) {
}
};
const loadReaderChapter = async (bookAbbrev, chapterNumber) => {
const setReaderTranslation = (id) => {
setReaderTranslationState(id);
try { localStorage.setItem('reader-translation', id); } catch {}
};
// BSB comes straight from HelloAO; licensed translations (NLT/CSB/NASB) go
// through the server's api.bible proxy so the API key never reaches the browser.
const loadReaderChapter = async (bookAbbrev, chapterNumber, translation = readerTranslation) => {
setReaderLoading(true);
setReaderError('');
try {
const res = await fetch(`https://bible.helloao.org/api/BSB/${bookAbbrev}/${chapterNumber}.json`);
if (!res.ok) throw new Error('Unable to load chapter.');
const data = await res.json();
const verses = parseBibleChapter(data);
let verses;
let copyright = '';
let totalChapters = CHAPTER_COUNTS[bookAbbrev] ?? chapterNumber;
if (translation === 'BSB') {
const res = await fetch(`https://bible.helloao.org/api/BSB/${bookAbbrev}/${chapterNumber}.json`);
if (!res.ok) throw new Error('Unable to load chapter.');
const data = await res.json();
verses = parseBibleChapter(data);
totalChapters = data.book?.numberOfChapters ?? totalChapters;
} else {
const res = await fetch(`/api/bible/${translation}/${bookAbbrev}/${chapterNumber}`, { credentials: 'include' });
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || `Unable to load ${translation}.`);
verses = data.verses;
copyright = data.copyright ?? '';
}
if (!Array.isArray(verses) || verses.length === 0) {
throw new Error('Invalid Bible data returned.');
}
setReaderVerses(verses);
setReaderTotalChapters(data.book?.numberOfChapters ?? chapterNumber);
setReaderCopyright(copyright);
setReaderTotalChapters(totalChapters);
} catch (err) {
setReaderError(err.message || 'Failed to load chapter.');
setReaderVerses([]);
@@ -112,8 +149,8 @@ export function useBibleReader(currentPage) {
useEffect(() => {
if (currentPage !== 'reader') return;
loadReaderChapter(readerBookAbbrev, readerChapter);
}, [currentPage, readerBookAbbrev, readerChapter]);
loadReaderChapter(readerBookAbbrev, readerChapter, readerTranslation);
}, [currentPage, readerBookAbbrev, readerChapter, readerTranslation]);
// After navigating to a verse (from the bookmarks panel or a whole-Bible search
// result), scroll to it and briefly flash its background so it's easy to spot.
@@ -294,6 +331,8 @@ export function useBibleReader(currentPage) {
return {
readerBookAbbrev, setReaderBookAbbrev,
readerChapter, setReaderChapter,
readerTranslation, setReaderTranslation,
readerCopyright,
readerVerses,
readerTotalChapters,
readerLoading,