Verse of the day in a translation you choose
Build and Push Docker Image / build-and-push (push) Successful in 5m44s

IQ Bible picks the verse; its words come from BSB, NET, ESV, NLT, CSB or
NASB (default: your reading translation, remembered per device), with the
KJV as a fallback if that Bible can't load.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-06 08:22:57 -04:00
1 parent 31fb3f958d
commit 8939a7f49f
2 files changed
+54 -4

No files matched your search

+34 -4
View File
@@ -1,8 +1,9 @@
// Home card: today's verse and Spurgeon's Morning & Evening reading, both from IQ Bible. // Home card: today's verse and Spurgeon's Morning & Evening reading, both from IQ Bible.
import { useState } from 'react'; import { useEffect, useState } from 'react';
import { useApp } from '../../context/AppContext.js'; import { useApp } from '../../context/AppContext.js';
import { useIq, useIqAvailable } from '../../hooks/useIq.js'; import { useIq, useIqAvailable } from '../../hooks/useIq.js';
import { bookOptions } from '../../utils/bibleUtils.jsx'; import { bookOptions } from '../../utils/bibleUtils.jsx';
import { READER_TRANSLATIONS, fetchChapter } from '../../utils/bibleText.js';
export default function IqDailyCard() { export default function IqDailyCard() {
const on = useIqAvailable(); const on = useIqAvailable();
@@ -11,8 +12,26 @@ export default function IqDailyCard() {
} }
function Card() { function Card() {
const { jumpToReaderVerse, openBibleReader } = useApp(); const { jumpToReaderVerse, openBibleReader, readerTranslation } = useApp();
// Today's verse is chosen by IQ Bible (the same for everyone); its words come from the Bible you pick.
const votd = useIq('bibles/eng_kjv/verse-of-the-day'); const votd = useIq('bibles/eng_kjv/verse-of-the-day');
const [chosen, setChosen] = useState(() => { try { return localStorage.getItem('votd-translation') || ''; } catch { return ''; } });
const translation = READER_TRANSLATIONS.some((t) => t.id === chosen) ? chosen : (readerTranslation || 'BSB');
const pick = (id) => { setChosen(id); try { localStorage.setItem('votd-translation', id); } catch { /* storage unavailable */ } };
const ref = votd.data?.data;
const [text, setText] = useState({ key: '', value: '', error: false });
const key = ref ? `${translation}/${ref.book_usfm}/${ref.chapter_number}/${ref.verse_number}` : '';
useEffect(() => {
if (!ref) return undefined;
const controller = new AbortController();
fetchChapter(translation, ref.book_usfm, ref.chapter_number, { signal: controller.signal })
.then(({ verses }) => {
const found = verses.find((v) => v.number === ref.verse_number)?.text;
setText({ key, value: found ?? '', error: !found });
})
.catch(() => { if (!controller.signal.aborted) setText({ key, value: '', error: true }); });
return () => controller.abort();
}, [key]); // eslint-disable-line react-hooks/exhaustive-deps
const dev = useIq('devotionals/today'); const dev = useIq('devotionals/today');
const [part, setPart] = useState(() => (new Date().getHours() < 15 ? 'morning' : 'evening')); const [part, setPart] = useState(() => (new Date().getHours() < 15 ? 'morning' : 'evening'));
const verse = votd.data?.data; const verse = votd.data?.data;
@@ -23,10 +42,21 @@ function Card() {
<section className="mb-4 rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-5" aria-label="Daily verse and devotional"> <section className="mb-4 rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-5" aria-label="Daily verse and devotional">
{verse && ( {verse && (
<div> <div>
<p className="text-[11px] uppercase tracking-wide text-[var(--r-sub)]">Verse of the day · KJV</p> <div className="flex items-center justify-between gap-2">
<p className="mt-1 font-serif text-[15px] leading-relaxed">{verse.text}</p> <p className="text-[11px] uppercase tracking-wide text-[var(--r-sub)]">Verse of the day</p>
<select aria-label="Verse of the day translation" value={translation} onChange={(e) => pick(e.target.value)}
className="rounded-lg border border-[var(--r-line)] bg-[var(--r-bg)] px-2 py-0.5 text-xs">
{READER_TRANSLATIONS.map((t) => <option key={t.id} value={t.id}>{t.id}</option>)}
</select>
</div>
{/* the translation's text; the KJV from IQ Bible is the fallback if that Bible can't be loaded right now */}
<p className="mt-1 font-serif text-[15px] leading-relaxed">{text.key === key && text.value ? text.value : text.key === key && text.error ? verse.text : '…'}</p>
{text.key === key && text.error && translation !== 'KJV' && <p className="text-[11px] text-[var(--r-sub)]">Couldn’t load {translation} just now, so this is the KJV.</p>}
<button type="button" onClick={() => { jumpToReaderVerse(verse.book_usfm, verse.chapter_number, verse.verse_number); openBibleReader(); }} <button type="button" onClick={() => { jumpToReaderVerse(verse.book_usfm, verse.chapter_number, verse.verse_number); openBibleReader(); }}
className="mt-1 text-xs font-medium text-[var(--r-accent)] hover:underline">{name(verse.book_usfm)} {verse.chapter_number}:{verse.verse_number} — read in context</button> className="mt-1 text-xs font-medium text-[var(--r-accent)] hover:underline">{name(verse.book_usfm)} {verse.chapter_number}:{verse.verse_number} — read in context</button>
{text.key === key && text.value && READER_TRANSLATIONS.find((t) => t.id === translation)?.notice && (
<p className="mt-2 text-[10px] leading-snug text-[var(--r-sub)]">{READER_TRANSLATIONS.find((t) => t.id === translation).notice}</p>
)}
</div> </div>
)} )}
{reading && ( {reading && (
+20
View File
@@ -139,3 +139,23 @@ describe('IqPlanExport', () => {
expect(container).toBeEmptyDOMElement(); expect(container).toBeEmptyDOMElement();
}); });
}); });
describe('IqDailyCard', () => {
test('shows the verse of the day in the chosen translation, remembers it, and falls back to the KJV', async () => {
const { default: IqDailyCard } = await import('./IqDailyCard.jsx');
routes['/api/iq/bibles/eng_kjv/verse-of-the-day'] = { data: { book_usfm: 'JHN', chapter_number: 3, verse_number: 16, text: 'KJV text of John 3:16' } };
routes['/api/iq/devotionals/today'] = { data: {} };
routes['https://bible.helloao.org/api/BSB/JHN/3.json'] = { chapter: { content: [{ type: 'verse', number: 16, content: ['BSB text of John 3:16'] }] } };
routes['https://bible.helloao.org/api/eng_net/JHN/3.json'] = { chapter: { content: [{ type: 'verse', number: 16, content: ['NET text of John 3:16'] }] } };
const { AppContext } = await import('../../context/AppContext.js');
const ctx = { jumpToReaderVerse: vi.fn(), openBibleReader: vi.fn(), readerTranslation: 'BSB' };
render(<AppContext.Provider value={ctx}><IqDailyCard /></AppContext.Provider>);
expect(await screen.findByText('BSB text of John 3:16')).toBeInTheDocument();
await userEvent.setup().selectOptions(screen.getByLabelText('Verse of the day translation'), 'NET');
expect(await screen.findByText('NET text of John 3:16')).toBeInTheDocument();
expect(localStorage.getItem('votd-translation')).toBe('NET');
await userEvent.setup().selectOptions(screen.getByLabelText('Verse of the day translation'), 'NLT'); // not reachable in this test
expect(await screen.findByText('KJV text of John 3:16')).toBeInTheDocument();
expect(screen.getByText(/Couldn’t load NLT just now/)).toBeInTheDocument();
});
});