Skip to content

Commit 004bf7b

Browse files
authored
Add files via upload
Initial release for GitHub pages.
1 parent 1ead341 commit 004bf7b

12 files changed

Lines changed: 3570 additions & 0 deletions

File tree

public/index.html

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

public/main.js

Lines changed: 610 additions & 0 deletions
Large diffs are not rendered by default.

public/modules/api.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import {
2+
clearError,
3+
handleError,
4+
showError,
5+
showLoading
6+
} from '../main.js'
7+
import {
8+
afterContentLoad,
9+
displayPassage,
10+
extractVerseText
11+
} from './passage.js'
12+
import {
13+
bookNameMapping,
14+
state
15+
} from './state.js'
16+
const API_BASE_URL = 'https://bible.helloao.org/api';
17+
const translationMap = {
18+
BSB: 'BSB',
19+
KJV: 'eng_kjv',
20+
NET: 'eng_net',
21+
ASV: 'eng_asv',
22+
GNV: 'eng_gnv'
23+
};
24+
export function apiTranslationCode(uiCode) {
25+
return translationMap[uiCode] ?? uiCode;
26+
}
27+
export function getApiBookCode(displayName) {
28+
const code = bookNameMapping[displayName];
29+
if (code) return code;
30+
console.warn('Missing book‑code mapping for:', displayName);
31+
showError(`Cannot load “${displayName}” – unknown book code.`);
32+
throw new Error('Unknown book code');
33+
}
34+
export async function fetchChapter(translation, book, chapter) {
35+
if (!navigator.onLine) {
36+
throw new Error('Offline mode: Cannot fetch new chapters. Using cached data if available.');
37+
}
38+
const trans = translation.trim();
39+
const bk = book.replace(/\s+/g, '').toUpperCase();
40+
const ch = Number(chapter);
41+
if (!trans || !bk || Number.isNaN(ch) || ch < 1) {
42+
throw new Error('Invalid parameters for Bible API request');
43+
}
44+
const url = `${API_BASE_URL}/${trans}/${bk}/${ch}.json`;
45+
try {
46+
const resp = await fetch(url, {
47+
method: 'GET',
48+
headers: { Accept: 'application/json' },
49+
cache: 'no-store'
50+
});
51+
if (!resp.ok) {
52+
const txt = await resp.text();
53+
throw new Error(`API error ${resp.status}: ${txt}`);
54+
}
55+
const ct = resp.headers.get('content-type') || '';
56+
if (!ct.includes('application/json')) {
57+
if (ct.startsWith('<')) {
58+
throw new Error('API returned HTML instead of JSON');
59+
}
60+
try {
61+
return JSON.parse(await resp.text());
62+
} catch (_) {
63+
throw new Error('Unable to parse API response as JSON');
64+
}
65+
}
66+
return resp.json();
67+
} catch (err) {
68+
handleError(err, 'fetchChapter');
69+
}
70+
}
71+
export async function loadPassageFromAPI(passageInfo) {
72+
try {
73+
showLoading(true);
74+
const { book, chapter, startVerse, endVerse, displayRef } = passageInfo;
75+
state.currentPassageReference = displayRef;
76+
const apiMap = apiTranslationCode(state.settings.bibleTranslation);
77+
const apiBook = getApiBookCode(book);
78+
const chapterData = await fetchChapter(apiMap, apiBook, chapter);
79+
if (!chapterData || !chapterData.chapter ||
80+
!Array.isArray(chapterData.chapter.content)) {
81+
throw new Error('Malformed API response – missing chapter.content');
82+
}
83+
const chapterFootnotes = chapterData.chapter.footnotes || [];
84+
const footnoteCounter = { value: 1 };
85+
const verses = chapterData.chapter.content
86+
.filter(v =>
87+
v.type === 'verse' &&
88+
v.number >= startVerse &&
89+
v.number <= endVerse
90+
)
91+
.map(v => {
92+
const verseData = extractVerseText(v.content, chapterFootnotes, footnoteCounter);
93+
return {
94+
number: v.number,
95+
text: verseData,
96+
reference: `${book} ${chapter}:${v.number}`,
97+
rawContent: v.content
98+
};
99+
});
100+
if (verses.length === 0) {
101+
throw new Error('No verses found in the requested range');
102+
}
103+
displayPassage(verses);
104+
afterContentLoad();
105+
clearError();
106+
if (chapterData.translation && chapterData.translation.name) {
107+
document.getElementById('bibleName').textContent =
108+
chapterData.translation.name;
109+
}
110+
} catch (err) {
111+
handleError(err, 'loadPassageFromAPI');
112+
} finally {
113+
showLoading(false);
114+
}
115+
}

public/modules/navigation.js

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import {
2+
apiTranslationCode,
3+
fetchChapter,
4+
getApiBookCode,
5+
loadPassageFromAPI
6+
} from './api.js'
7+
import {
8+
clearError,
9+
handleError,
10+
showError,
11+
showLoading
12+
} from '../main.js'
13+
import {
14+
displayPassage,
15+
extractVerseText,
16+
loadPassage
17+
} from './passage.js'
18+
import {
19+
BOOK_ORDER,
20+
CHAPTER_COUNTS,
21+
bookNameMapping,
22+
getActivePlan,
23+
saveToStorage,
24+
state
25+
} from './state.js'
26+
import { updateReferencePanel } from './ui.js'
27+
export function populateBookDropdown() {
28+
const bookSel = document.getElementById('bookSelect');
29+
bookSel.innerHTML = '';
30+
BOOK_ORDER.forEach(book => {
31+
const opt = document.createElement('option');
32+
opt.value = book;
33+
opt.textContent = book;
34+
bookSel.appendChild(opt);
35+
});
36+
}
37+
export function populateChapterDropdown(selectedBook) {
38+
const chapSel = document.getElementById('chapterSelect');
39+
chapSel.innerHTML = '';
40+
const max = CHAPTER_COUNTS[selectedBook];
41+
for (let i = 1; i <= max; i++) {
42+
const opt = document.createElement('option');
43+
opt.value = i;
44+
opt.textContent = i;
45+
chapSel.appendChild(opt);
46+
}
47+
}
48+
export async function loadSelectedChapter(book = null, chapter = null) {
49+
const selBook = book || document.getElementById('bookSelect').value;
50+
const selChapter = chapter || document.getElementById('chapterSelect').value;
51+
const apiBook = getApiBookCode(selBook);
52+
try {
53+
showLoading(true);
54+
const apiTranslation = apiTranslationCode(state.settings.bibleTranslation);
55+
const chapterData = await fetchChapter(
56+
apiTranslation,
57+
apiBook,
58+
selChapter
59+
);
60+
const chapterFootnotes = chapterData.chapter.footnotes || [];
61+
const footnoteCounter = { value: 1 };
62+
const verses = chapterData.chapter.content
63+
.filter(v => v.type === 'verse')
64+
.map(v => ({
65+
number: v.number,
66+
text: extractVerseText(v.content, chapterFootnotes, footnoteCounter),
67+
reference: `${selBook} ${selChapter}:${v.number}`
68+
}));
69+
document.getElementById('passageReference').textContent =
70+
`${selBook} ${selChapter}`;
71+
state.footnotes = {};
72+
displayPassage(verses, `${selBook} ${selChapter}`);
73+
clearError();
74+
document.getElementById('scriptureSection').scrollTop = 0;
75+
if (state.settings.readingMode === 'manual') {
76+
state.settings.manualBook = selBook;
77+
state.settings.manualChapter = Number(selChapter);
78+
saveToStorage();
79+
}
80+
if (state.settings.referencePanelOpen) {
81+
updateReferencePanel();
82+
}
83+
} catch (err) {
84+
handleError(err, 'loadSelectedChapter');
85+
showError(`Could not load ${selBook} ${selChapter}: ${err.message}`);
86+
} finally {
87+
showLoading(false);
88+
}
89+
}
90+
export function initBookChapterControls() {
91+
populateBookDropdown();
92+
document.getElementById('bookSelect').addEventListener('change', e => {
93+
const book = e.target.value;
94+
state.settings.readingMode = 'manual';
95+
populateChapterDropdown(book);
96+
state.settings.manualBook = book;
97+
state.settings.manualChapter = 1;
98+
const chapterSel = document.getElementById('chapterSelect');
99+
chapterSel.value = '1';
100+
loadSelectedChapter(book, 1);
101+
saveToStorage();
102+
});
103+
document.getElementById('chapterSelect').addEventListener('change', () => {
104+
const book = document.getElementById('bookSelect').value;
105+
const chap = Number(document.getElementById('chapterSelect').value);
106+
state.settings.readingMode = 'manual';
107+
state.settings.manualBook = book;
108+
state.settings.manualChapter = chap;
109+
loadSelectedChapter(book, chap);
110+
saveToStorage();
111+
});
112+
populateChapterDropdown(BOOK_ORDER[0]);
113+
}
114+
export function manualPrevChapter() {
115+
let bookIdx = BOOK_ORDER.indexOf(state.settings.manualBook);
116+
let chap = state.settings.manualChapter;
117+
if (chap > 1) {
118+
state.settings.manualChapter = chap - 1;
119+
} else {
120+
if (bookIdx > 0) {
121+
const prevBook = BOOK_ORDER[bookIdx - 1];
122+
const maxCh = CHAPTER_COUNTS[prevBook];
123+
state.settings.manualBook = prevBook;
124+
state.settings.manualChapter = maxCh;
125+
} else {
126+
return;
127+
}
128+
}
129+
state.settings.readingMode = 'manual';
130+
loadSelectedChapter(state.settings.manualBook, state.settings.manualChapter);
131+
syncBookChapterSelectors();
132+
saveToStorage();
133+
if (state.settings.referencePanelOpen) {
134+
updateReferencePanel();
135+
}
136+
}
137+
export function manualNextChapter() {
138+
let bookIdx = BOOK_ORDER.indexOf(state.settings.manualBook);
139+
let chap = state.settings.manualChapter;
140+
const maxCh = CHAPTER_COUNTS[state.settings.manualBook];
141+
if (chap < maxCh) {
142+
state.settings.manualChapter = chap + 1;
143+
} else {
144+
if (bookIdx < BOOK_ORDER.length - 1) {
145+
const nextBook = BOOK_ORDER[bookIdx + 1];
146+
state.settings.manualBook = nextBook;
147+
state.settings.manualChapter = 1;
148+
} else {
149+
return;
150+
}
151+
}
152+
state.settings.readingMode = 'manual';
153+
loadSelectedChapter(state.settings.manualBook, state.settings.manualChapter);
154+
syncBookChapterSelectors();
155+
saveToStorage();
156+
if (state.settings.referencePanelOpen) {
157+
updateReferencePanel();
158+
}
159+
}
160+
export function prevPassage() {
161+
if (state.settings.readingMode === 'readingPlan') {
162+
const len = getActivePlan().length;
163+
let newIndex = (state.settings.currentPassageIndex - 1 + len) % len;
164+
state.settings.currentPassageIndex = newIndex;
165+
loadPassage();
166+
} else {
167+
manualPrevChapter();
168+
}
169+
document.getElementById('scriptureSection').scrollTop = 0;
170+
}
171+
export function nextPassage() {
172+
if (state.settings.readingMode === 'readingPlan') {
173+
const len = getActivePlan().length;
174+
let newIndex = (state.settings.currentPassageIndex + 1) % len;
175+
if (newIndex < 0) newIndex = len - 1;
176+
state.settings.currentPassageIndex = newIndex;
177+
loadPassage();
178+
} else {
179+
manualNextChapter();
180+
}
181+
document.getElementById('scriptureSection').scrollTop = 0;
182+
}
183+
export async function randomPassage() {
184+
try {
185+
state.settings.readingMode = 'manual';
186+
const randomLoc = await getRandomBibleLocation();
187+
state.settings.manualBook = randomLoc.book;
188+
state.settings.manualChapter = randomLoc.chapter;
189+
saveToStorage();
190+
await loadPassageFromAPI(randomLoc);
191+
document.getElementById('passageReference').textContent = randomLoc.displayRef;
192+
state.currentPassageReference = randomLoc.displayRef;
193+
syncBookChapterSelectors();
194+
if (state.settings.referencePanelOpen) {
195+
updateReferencePanel();
196+
}
197+
} catch (err) {
198+
handleError(err, 'randomPassage');
199+
showError('Could not load a random passage – see console for details.');
200+
}
201+
}
202+
export function syncBookChapterSelectors() {
203+
const bookSel = document.getElementById('bookSelect');
204+
const chapterSel = document.getElementById('chapterSelect');
205+
if (bookSel.value !== state.settings.manualBook) {
206+
bookSel.value = state.settings.manualBook;
207+
populateChapterDropdown(state.settings.manualBook);
208+
}
209+
const curMax = CHAPTER_COUNTS[state.settings.manualBook];
210+
const curChap = state.settings.manualChapter;
211+
populateChapterDropdown(state.settings.manualBook);
212+
chapterSel.value = (curChap <= curMax) ? curChap : curMax;
213+
}
214+
export function syncSelectorsToReadingPlan() {
215+
if (state.settings.readingMode !== 'readingPlan') return;
216+
const plan = getActivePlan();
217+
const passage = plan[state.settings.currentPassageIndex];
218+
if (!passage || !passage.book) {
219+
console.error('Invalid passage object:', passage);
220+
return;
221+
}
222+
const bookSel = document.getElementById('bookSelect');
223+
const chapterSel = document.getElementById('chapterSelect');
224+
state.settings.manualBook = passage.book;
225+
state.settings.manualChapter = passage.chapter;
226+
if (bookSel) bookSel.value = passage.book;
227+
populateChapterDropdown(passage.book);
228+
if (chapterSel) chapterSel.value = passage.chapter;
229+
saveToStorage();
230+
}
231+
async function getRandomBibleLocation() {
232+
try {
233+
const randomBook = BOOK_ORDER[Math.floor(Math.random() * BOOK_ORDER.length)];
234+
const maxCh = CHAPTER_COUNTS[randomBook];
235+
const randomChapter = Math.floor(Math.random() * maxCh) + 1;
236+
const apiMap = apiTranslationCode(state.settings.bibleTranslation);
237+
const apiBook = bookNameMapping[randomBook] ||
238+
randomBook.replace(/\s+/g, '').toUpperCase();
239+
const chapterData = await fetchChapter(apiMap, apiBook, randomChapter);
240+
const verses = chapterData.chapter.content.filter(v => v.type === 'verse');
241+
const verseCount = verses.length || 1;
242+
return {
243+
book: randomBook,
244+
chapter: randomChapter,
245+
startVerse: 1,
246+
endVerse: verseCount,
247+
displayRef: `${randomBook} ${randomChapter}`
248+
};
249+
} catch (err) {
250+
handleError(err, 'getRandomBibleLocation');
251+
}
252+
}

0 commit comments

Comments
 (0)