diff --git a/data/analytics-events.json b/data/analytics-events.json index 6167972..bdd0bde 100644 --- a/data/analytics-events.json +++ b/data/analytics-events.json @@ -9,8 +9,8 @@ }, "timeOnPage": { "/": { - "totalSeconds": 9, - "count": 2 + "totalSeconds": 51, + "count": 4 }, "/episodes": { "totalSeconds": 318967, @@ -22,6 +22,7 @@ } }, "outboundClicks": {}, + "linkClicks": {}, "utmSources": {}, "searchQueries": {}, "notFound": {}, diff --git a/data/study-reminders.json b/data/study-reminders.json index c23b4fa..02e42e8 100644 --- a/data/study-reminders.json +++ b/data/study-reminders.json @@ -1,4 +1,4 @@ { "users": {}, - "updatedAt": "2026-08-10T12:37:17.805Z" + "updatedAt": "2026-08-10T14:07:00.476Z" } \ No newline at end of file diff --git a/package.json b/package.json index cb05770..dd6b1b0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "1.1.34", + "version": "1.1.35", "type": "module", "scripts": { "dev": "vite", diff --git a/server.js b/server.js index 04cfa7a..2f659f7 100644 --- a/server.js +++ b/server.js @@ -63,6 +63,7 @@ import { register as registerDownloads } from './server/routes/downloads.js' import { register as registerEpisodes } from './server/routes/episodes.js' import { register as registerFeed } from './server/routes/feed.js' import { register as registerQrCodes } from './server/routes/qr-codes.js' +import { register as registerBiblePassage } from './server/routes/bible-passage.js' import { register as registerPublic } from './server/routes/public.js' const app = express() @@ -110,6 +111,7 @@ registerDownloads(app) registerEpisodes(app) registerFeed(app) registerQrCodes(app) +registerBiblePassage(app) // Hit-counting middleware (must come before public routes) app.use((req, res, next) => { diff --git a/server/routes/bible-passage.js b/server/routes/bible-passage.js new file mode 100644 index 0000000..b200673 --- /dev/null +++ b/server/routes/bible-passage.js @@ -0,0 +1,76 @@ +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, +} + +function parseReference(reference) { + const match = reference.match(/^(\d+):(\d+)(?:-(\d+))?$/) + if (!match) return null + return { + chapter: parseInt(match[1], 10), + verseStart: parseInt(match[2], 10), + verseEnd: match[3] ? parseInt(match[3], 10) : parseInt(match[2], 10), + } +} + +export function register(app) { + app.get('/api/bible-passage', async (req, res) => { + const book = String(req.query.book ?? '').trim().toLowerCase().replace(/[\s-]/g, '') + const reference = String(req.query.reference ?? '').trim() + + const bookNum = BOLLS_BOOK_MAP[book] + if (!bookNum) { + res.status(400).json({ message: 'Unknown book.' }) + return + } + + const parsed = parseReference(reference) + if (!parsed) { + res.status(400).json({ message: 'Invalid reference format. Expected format: chapter:verseStart-verseEnd' }) + return + } + + const cacheKey = `BSB:${bookNum}:${parsed.chapter}:${parsed.verseStart}:${parsed.verseEnd}` + if (passageCache.has(cacheKey)) { + res.json({ text: passageCache.get(cacheKey) }) + return + } + + try { + const verseNums = [] + for (let v = parsed.verseStart; v <= parsed.verseEnd; v++) verseNums.push(v) + + const results = await Promise.all( + verseNums.map(v => + fetch(`https://bolls.life/get-verse/BSB/${bookNum}/${parsed.chapter}/${v}/`, { signal: AbortSignal.timeout(8000) }) + .then(r => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`))) + ) + ) + + if (results.length === 0) { + res.status(404).json({ message: 'Passage not found.' }) + return + } + + const text = results.map(v => `v. ${v.verse} ${String(v.text).trim()}`).join('\n\n') + passageCache.set(cacheKey, text) + res.json({ text }) + } catch { + res.status(502).json({ message: 'Unable to fetch passage right now.' }) + } + }) +} diff --git a/src/AdminPage.tsx b/src/AdminPage.tsx index a7087b7..ea57ce9 100644 --- a/src/AdminPage.tsx +++ b/src/AdminPage.tsx @@ -4285,6 +4285,11 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { updateStudyProgram(study.id, 'slug', e.target.value.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-'))} /> +
Lowercase book name used to fetch BSB passage text. E.g. colossians, ephesians, 1corinthians. Leave blank if the slug matches the book name.
+