Verse of the day with picture backgrounds, shareable image, and study covers
Build and Push Docker Image / build-and-push (push) Failing after 21s

- Verse of the day: card over a picture (a different one each day, or chosen),
  12 drawn scenes plus photos from public/verse-backgrounds, and "Share as
  image" (also on the Reader's verse panel) that saves or shares a PNG with the
  same background, a theme look, or a photo from the device
- Study cards get a cover banner named for the book, coloured by kind of book
- Outline "Build from my notes": abbreviations like "A.D. 63." no longer end a
  sentence

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-06 09:21:44 -04:00
1 parent d2bbf1feb4
commit aabd4267b8
15 files changed
+826 -8

No files matched your search

+44 -5
View File
@@ -2,6 +2,8 @@
import { useEffect, useState } from 'react';
import { useApp } from '../../context/AppContext.js';
import { useIq, useIqAvailable } from '../../hooks/useIq.js';
import VerseShareImage, { BACKGROUND_KEY } from '../VerseShareImage.jsx';
import { SCENES, loadPhotoList, resolveBackground, sceneDataUrl } from '../../utils/verseBackgrounds.js';
import { bookOptions } from '../../utils/bibleUtils.jsx';
import { READER_TRANSLATIONS, fetchChapter } from '../../utils/bibleText.js';
@@ -12,7 +14,7 @@ export default function IqDailyCard() {
}
function Card() {
const { jumpToReaderVerse, openBibleReader, readerTranslation } = useApp();
const { jumpToReaderVerse, openBibleReader, readerTranslation, readerTheme } = 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 [chosen, setChosen] = useState(() => { try { return localStorage.getItem('votd-translation') || ''; } catch { return ''; } });
@@ -20,6 +22,14 @@ function Card() {
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 [sharing, setSharing] = useState(false);
// The picture behind the verse: a different one each day by default, or the one you chose.
const [bgChoice, setBgChoice] = useState(() => { try { return localStorage.getItem(BACKGROUND_KEY) || 'auto'; } catch { return 'auto'; } });
const [photos, setPhotos] = useState([]);
useEffect(() => { let live = true; loadPhotoList().then((list) => { if (live) setPhotos(list); }); return () => { live = false; }; }, []);
const background = resolveBackground(bgChoice, photos);
const backgroundUrl = background?.kind === 'photo' ? background.url : background ? sceneDataUrl(background.id) : '';
const pickBackground = (id) => { setBgChoice(id); try { localStorage.setItem(BACKGROUND_KEY, id); } catch { /* storage unavailable */ } };
const key = ref ? `${translation}/${ref.book_usfm}/${ref.chapter_number}/${ref.verse_number}` : '';
useEffect(() => {
if (!ref) return undefined;
@@ -36,6 +46,9 @@ function Card() {
const [part, setPart] = useState(() => (new Date().getHours() < 15 ? 'morning' : 'evening'));
const verse = votd.data?.data;
const reading = dev.data?.data?.[part];
const loaded = text.key === key;
const shown = loaded && text.value ? text.value : loaded && text.error ? ref?.text ?? '' : '…';
const shownTranslation = loaded && text.error ? 'KJV' : translation;
const name = (abbrev) => bookOptions.find((b) => b.abbrev === abbrev)?.name ?? abbrev;
if (!verse && !reading) return null;
return (
@@ -44,16 +57,42 @@ function Card() {
<div>
<div className="flex items-center justify-between gap-2">
<p className="text-[11px] uppercase tracking-wide text-[var(--r-sub)]">Verse of the day</p>
<span className="flex items-center gap-1.5">
<select aria-label="Verse of the day background" value={bgChoice} onChange={(e) => pickBackground(e.target.value)}
className="rounded-lg border border-[var(--r-line)] bg-[var(--r-bg)] px-2 py-0.5 text-xs">
<option value="auto">Picture of the day</option>
<option value="plain">Plain</option>
<optgroup label="Scenes">{SCENES.map((sc) => <option key={sc.id} value={sc.id}>{sc.name}</option>)}</optgroup>
{photos.length > 0 && <optgroup label="Your photos">{photos.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}</optgroup>}
</select>
<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>
</span>
</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(); }}
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>
{backgroundUrl ? (
<figure className="relative mt-2 overflow-hidden rounded-xl bg-cover bg-center" style={{ backgroundImage: `url(${backgroundUrl})` }}>
<div className="bg-gradient-to-b from-black/35 via-black/45 to-black/60 px-6 py-8 text-center text-white" style={{ textShadow: '0 1px 8px rgba(0,0,0,0.55)' }}>
<blockquote className="font-serif text-[18px] leading-relaxed sm:text-[20px]">{shown}</blockquote>
<figcaption className="mt-3 text-sm font-semibold tracking-wide text-[#ffe0b0]">{name(verse.book_usfm)} {verse.chapter_number}:{verse.verse_number} <span className="font-normal text-white/80">· {shownTranslation}</span></figcaption>
</div>
</figure>
) : (
<blockquote className="mt-2 rounded-xl border border-[var(--r-line)] border-l-4 border-l-[var(--r-accent)] bg-[var(--r-bg)] px-4 py-3 font-serif text-[16px] leading-relaxed">{shown}</blockquote>
)}
{loaded && text.error && translation !== 'KJV' && <p className="mt-1 text-[11px] text-[var(--r-sub)]">Couldn’t load {translation} just now, so this is the KJV.</p>}
<div className="mt-2 flex flex-wrap items-center gap-x-4 gap-y-1">
<button type="button" onClick={() => { jumpToReaderVerse(verse.book_usfm, verse.chapter_number, verse.verse_number); openBibleReader(); }}
className="text-xs font-medium text-[var(--r-accent)] hover:underline">{name(verse.book_usfm)} {verse.chapter_number}:{verse.verse_number} — read in context</button>
<button type="button" onClick={() => setSharing((o) => !o)} aria-expanded={sharing} disabled={!loaded || !shown || shown === '…'}
className="text-xs font-medium text-[var(--r-accent)] hover:underline disabled:opacity-50">Share as image</button>
</div>
{sharing && loaded && shown && (
<VerseShareImage text={shown} reference={`${name(verse.book_usfm)} ${verse.chapter_number}:${verse.verse_number}`} translation={shownTranslation}
notice={loaded && !text.error ? READER_TRANSLATIONS.find((t) => t.id === translation)?.notice ?? '' : ''} defaultStyle={readerTheme || 'paper'} defaultBackground={bgChoice} onClose={() => setSharing(false)} />
)}
{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>
)}