diff --git a/server/routes/bible-passage.js b/server/routes/bible-passage.js index 745d873..5a4d592 100644 --- a/server/routes/bible-passage.js +++ b/server/routes/bible-passage.js @@ -1,21 +1,19 @@ +import { BIBLE_BOOKS } from '../../shared/bible-books.js' + const passageCache = new Map() -const BOLLS_BOOK_MAP = { - genesis: 1, exodus: 2, leviticus: 3, numbers: 4, deuteronomy: 5, - joshua: 6, judges: 7, ruth: 8, '1samuel': 9, '2samuel': 10, - '1kings': 11, '2kings': 12, '1chronicles': 13, '2chronicles': 14, - ezra: 15, nehemiah: 16, esther: 17, job: 18, psalms: 19, proverbs: 20, - ecclesiastes: 21, songofsolomon: 22, isaiah: 23, jeremiah: 24, - lamentations: 25, ezekiel: 26, daniel: 27, hosea: 28, joel: 29, - amos: 30, obadiah: 31, jonah: 32, micah: 33, nahum: 34, habakkuk: 35, - zephaniah: 36, haggai: 37, zechariah: 38, malachi: 39, - matthew: 40, mark: 41, luke: 42, john: 43, acts: 44, - romans: 45, '1corinthians': 46, '2corinthians': 47, galatians: 48, - ephesians: 49, philippians: 50, colossians: 51, '1thessalonians': 52, - '2thessalonians': 53, '1timothy': 54, '2timothy': 55, titus: 56, - philemon: 57, hebrews: 58, james: 59, '1peter': 60, '2peter': 61, - '1john': 62, '2john': 63, '3john': 64, jude: 65, revelation: 66, -} +// Keyed by lowercase, whitespace-stripped book name/alias (matching the format +// the admin's "Bible Book" field and the /api/bible-passage `book` query param +// both use), mapped to bolls.life's numeric book id (canonical order, 1-66). +// Derived from the shared BIBLE_BOOKS list so it can't drift from the client's +// Scripture Index book map (src/App.tsx's SI_BOOK_MAP). +const BOLLS_BOOK_MAP = {} +BIBLE_BOOKS.forEach((b, index) => { + const bookNum = index + 1 + for (const key of [b.name.toLowerCase(), ...(b.aliases ?? [])]) { + BOLLS_BOOK_MAP[key.replace(/\s+/g, '')] = bookNum + } +}) function parseReference(reference) { const stripped = reference.replace(/^[a-z]+/i, '') diff --git a/shared/bible-books.js b/shared/bible-books.js new file mode 100644 index 0000000..052d9fc --- /dev/null +++ b/shared/bible-books.js @@ -0,0 +1,87 @@ +// Canonical Protestant Bible book list, in canonical (Genesis → Revelation) order. +// +// This is the single source of truth for book names/aliases, shared between the +// client (src/App.tsx, for the Scripture Index page) and the server +// (server/routes/bible-passage.js, for the BSB/bolls.life passage-fetch API). +// Previously these lived as three independent, hand-maintained lists with +// different key formats (spaced names + 3-letter ids in the client, unspaced +// names + numeric ids on the server) that could silently drift out of sync. +// +// - `id` is the 3-letter USFM-style book id used by the bible.helloao.org API. +// - bolls.life's book numbering is exactly this array's 1-based canonical order, +// so no separate numeric-id table is needed — see bookNumberFor() below. +// - `aliases` are additional short forms admins/users might type (abbreviations). +export const BIBLE_BOOKS = [ + { id: 'GEN', name: 'Genesis', aliases: ['gen'] }, + { id: 'EXO', name: 'Exodus', aliases: ['exo'] }, + { id: 'LEV', name: 'Leviticus', aliases: ['lev'] }, + { id: 'NUM', name: 'Numbers', aliases: ['num'] }, + { id: 'DEU', name: 'Deuteronomy', aliases: ['deut'] }, + { id: 'JOS', name: 'Joshua', aliases: ['josh'] }, + { id: 'JDG', name: 'Judges', aliases: ['judg'] }, + { id: 'RUT', name: 'Ruth' }, + { id: 'SA1', name: '1 Samuel', aliases: ['1 sam'] }, + { id: 'SA2', name: '2 Samuel', aliases: ['2 sam'] }, + { id: 'KI1', name: '1 Kings', aliases: ['1 kgs'] }, + { id: 'KI2', name: '2 Kings', aliases: ['2 kgs'] }, + { id: 'CH1', name: '1 Chronicles', aliases: ['1 chr'] }, + { id: 'CH2', name: '2 Chronicles', aliases: ['2 chr'] }, + { id: 'EZR', name: 'Ezra' }, + { id: 'NEH', name: 'Nehemiah', aliases: ['neh'] }, + { id: 'EST', name: 'Esther', aliases: ['esth'] }, + { id: 'JOB', name: 'Job' }, + { id: 'PSA', name: 'Psalms', aliases: ['ps', 'psa', 'psalm'] }, + { id: 'PRO', name: 'Proverbs', aliases: ['prov'] }, + { id: 'ECC', name: 'Ecclesiastes', aliases: ['eccl'] }, + { id: 'SNG', name: 'Song of Solomon', aliases: ['song'] }, + { id: 'ISA', name: 'Isaiah', aliases: ['isa'] }, + { id: 'JER', name: 'Jeremiah', aliases: ['jer'] }, + { id: 'LAM', name: 'Lamentations', aliases: ['lam'] }, + { id: 'EZK', name: 'Ezekiel', aliases: ['ezek'] }, + { id: 'DAN', name: 'Daniel', aliases: ['dan'] }, + { id: 'HOS', name: 'Hosea', aliases: ['hos'] }, + { id: 'JOL', name: 'Joel' }, + { id: 'AMO', name: 'Amos' }, + { id: 'OBA', name: 'Obadiah', aliases: ['obad'] }, + { id: 'JNA', name: 'Jonah' }, + { id: 'MIC', name: 'Micah', aliases: ['mic'] }, + { id: 'NAH', name: 'Nahum', aliases: ['nah'] }, + { id: 'HAB', name: 'Habakkuk', aliases: ['hab'] }, + { id: 'ZEP', name: 'Zephaniah', aliases: ['zeph'] }, + { id: 'HAG', name: 'Haggai', aliases: ['hag'] }, + { id: 'ZEC', name: 'Zechariah', aliases: ['zech'] }, + { id: 'MAL', name: 'Malachi', aliases: ['mal'] }, + { id: 'MAT', name: 'Matthew', aliases: ['matt'] }, + { id: 'MRK', name: 'Mark' }, + { id: 'LUK', name: 'Luke' }, + { id: 'JHN', name: 'John' }, + { id: 'ACT', name: 'Acts' }, + { id: 'ROM', name: 'Romans', aliases: ['rom'] }, + { id: 'CO1', name: '1 Corinthians', aliases: ['1 cor'] }, + { id: 'CO2', name: '2 Corinthians', aliases: ['2 cor'] }, + { id: 'GAL', name: 'Galatians', aliases: ['gal'] }, + { id: 'EPH', name: 'Ephesians', aliases: ['eph'] }, + { id: 'PHP', name: 'Philippians', aliases: ['phil'] }, + { id: 'COL', name: 'Colossians', aliases: ['col'] }, + { id: 'TH1', name: '1 Thessalonians', aliases: ['1 thess'] }, + { id: 'TH2', name: '2 Thessalonians', aliases: ['2 thess'] }, + { id: 'TI1', name: '1 Timothy', aliases: ['1 tim'] }, + { id: 'TI2', name: '2 Timothy', aliases: ['2 tim'] }, + { id: 'TIT', name: 'Titus' }, + { id: 'PHM', name: 'Philemon', aliases: ['philem'] }, + { id: 'HEB', name: 'Hebrews', aliases: ['heb'] }, + { id: 'JAM', name: 'James', aliases: ['jas'] }, + { id: 'PE1', name: '1 Peter', aliases: ['1 pet'] }, + { id: 'PE2', name: '2 Peter', aliases: ['2 pet'] }, + { id: 'JO1', name: '1 John' }, + { id: 'JO2', name: '2 John' }, + { id: 'JO3', name: '3 John' }, + { id: 'JDE', name: 'Jude', aliases: ['jude'] }, + { id: 'REV', name: 'Revelation', aliases: ['rev'] }, +] + +/** bolls.life's numeric book id: just this list's 1-based canonical position. */ +export function bookNumberFor(usfmId) { + const index = BIBLE_BOOKS.findIndex(b => b.id === usfmId) + return index === -1 ? null : index + 1 +} diff --git a/src/App.tsx b/src/App.tsx index 090027d..83d142f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,6 +12,7 @@ import { usePageMeta } from './hooks/usePageMeta' import { useGlobalSearch } from './hooks/useGlobalSearch' import { GlobalSearch } from './components/GlobalSearch' import { EpisodeAudioPlayer } from './components/EpisodeAudioPlayer' +import { BIBLE_BOOKS } from '../shared/bible-books.js' import './App.css' import { sendEvent, useScrollDepthTracking, useTimeOnPage, useUTMCapture, useLinkClickTracking } from './analytics' @@ -1983,75 +1984,9 @@ function ResourcesPage({ content }: { content: SiteContent }) { ) } -const BIBLE_BOOKS: Array<{ id: string; name: string; aliases?: string[] }> = [ - { id: 'GEN', name: 'Genesis', aliases: ['gen'] }, - { id: 'EXO', name: 'Exodus', aliases: ['exo'] }, - { id: 'LEV', name: 'Leviticus', aliases: ['lev'] }, - { id: 'NUM', name: 'Numbers', aliases: ['num'] }, - { id: 'DEU', name: 'Deuteronomy', aliases: ['deut'] }, - { id: 'JOS', name: 'Joshua', aliases: ['josh'] }, - { id: 'JDG', name: 'Judges', aliases: ['judg'] }, - { id: 'RUT', name: 'Ruth' }, - { id: 'SA1', name: '1 Samuel', aliases: ['1 sam'] }, - { id: 'SA2', name: '2 Samuel', aliases: ['2 sam'] }, - { id: 'KI1', name: '1 Kings', aliases: ['1 kgs'] }, - { id: 'KI2', name: '2 Kings', aliases: ['2 kgs'] }, - { id: 'CH1', name: '1 Chronicles', aliases: ['1 chr'] }, - { id: 'CH2', name: '2 Chronicles', aliases: ['2 chr'] }, - { id: 'EZR', name: 'Ezra' }, - { id: 'NEH', name: 'Nehemiah', aliases: ['neh'] }, - { id: 'EST', name: 'Esther', aliases: ['esth'] }, - { id: 'JOB', name: 'Job' }, - { id: 'PSA', name: 'Psalms', aliases: ['ps', 'psa', 'psalm'] }, - { id: 'PRO', name: 'Proverbs', aliases: ['prov'] }, - { id: 'ECC', name: 'Ecclesiastes', aliases: ['eccl'] }, - { id: 'SNG', name: 'Song of Solomon', aliases: ['song'] }, - { id: 'ISA', name: 'Isaiah', aliases: ['isa'] }, - { id: 'JER', name: 'Jeremiah', aliases: ['jer'] }, - { id: 'LAM', name: 'Lamentations', aliases: ['lam'] }, - { id: 'EZK', name: 'Ezekiel', aliases: ['ezek'] }, - { id: 'DAN', name: 'Daniel', aliases: ['dan'] }, - { id: 'HOS', name: 'Hosea', aliases: ['hos'] }, - { id: 'JOL', name: 'Joel' }, - { id: 'AMO', name: 'Amos' }, - { id: 'OBA', name: 'Obadiah', aliases: ['obad'] }, - { id: 'JNA', name: 'Jonah' }, - { id: 'MIC', name: 'Micah', aliases: ['mic'] }, - { id: 'NAH', name: 'Nahum', aliases: ['nah'] }, - { id: 'HAB', name: 'Habakkuk', aliases: ['hab'] }, - { id: 'ZEP', name: 'Zephaniah', aliases: ['zeph'] }, - { id: 'HAG', name: 'Haggai', aliases: ['hag'] }, - { id: 'ZEC', name: 'Zechariah', aliases: ['zech'] }, - { id: 'MAL', name: 'Malachi', aliases: ['mal'] }, - { id: 'MAT', name: 'Matthew', aliases: ['matt'] }, - { id: 'MRK', name: 'Mark' }, - { id: 'LUK', name: 'Luke' }, - { id: 'JHN', name: 'John' }, - { id: 'ACT', name: 'Acts' }, - { id: 'ROM', name: 'Romans', aliases: ['rom'] }, - { id: 'CO1', name: '1 Corinthians', aliases: ['1 cor'] }, - { id: 'CO2', name: '2 Corinthians', aliases: ['2 cor'] }, - { id: 'GAL', name: 'Galatians', aliases: ['gal'] }, - { id: 'EPH', name: 'Ephesians', aliases: ['eph'] }, - { id: 'PHP', name: 'Philippians', aliases: ['phil'] }, - { id: 'COL', name: 'Colossians', aliases: ['col'] }, - { id: 'TH1', name: '1 Thessalonians', aliases: ['1 thess'] }, - { id: 'TH2', name: '2 Thessalonians', aliases: ['2 thess'] }, - { id: 'TI1', name: '1 Timothy', aliases: ['1 tim'] }, - { id: 'TI2', name: '2 Timothy', aliases: ['2 tim'] }, - { id: 'TIT', name: 'Titus' }, - { id: 'PHM', name: 'Philemon', aliases: ['philem'] }, - { id: 'HEB', name: 'Hebrews', aliases: ['heb'] }, - { id: 'JAM', name: 'James', aliases: ['jas'] }, - { id: 'PE1', name: '1 Peter', aliases: ['1 pet'] }, - { id: 'PE2', name: '2 Peter', aliases: ['2 pet'] }, - { id: 'JO1', name: '1 John' }, - { id: 'JO2', name: '2 John' }, - { id: 'JO3', name: '3 John' }, - { id: 'JDE', name: 'Jude', aliases: ['jude'] }, - { id: 'REV', name: 'Revelation', aliases: ['rev'] }, -] - +// Canonical book list lives in shared/bible-books.js so the client (Scripture +// Index book matching, below) and the server (server/routes/bible-passage.js, +// the BSB passage-fetch API) can't drift out of sync with each other. const SI_BOOK_MAP: Record = {} const SI_BOOK_NAME: Record = {} const SI_BOOK_ORDER: string[] = [] diff --git a/tsconfig.app.json b/tsconfig.app.json index af516fc..33bf506 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -7,6 +7,7 @@ "module": "ESNext", "types": ["vite/client"], "skipLibCheck": true, + "allowJs": true, /* Bundler mode */ "moduleResolution": "bundler", @@ -24,5 +25,5 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["src"] + "include": ["src", "shared"] }