Verse of the day in a translation you choose
Build and Push Docker Image / build-and-push (push) Successful in 5m44s
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:
1 parent
31fb3f958d
commit
8939a7f49f
2 files changed
+54
-4
No files matched your search
@@ -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 && (
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user