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>
118 lines
8.5 KiB
React
118 lines
8.5 KiB
React
// Home card: today's verse and Spurgeon's Morning & Evening reading, both from IQ Bible.
|
||
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';
|
||
|
||
export default function IqDailyCard() {
|
||
const on = useIqAvailable();
|
||
if (!on) return null;
|
||
return <Card />;
|
||
}
|
||
|
||
function Card() {
|
||
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 ''; } });
|
||
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 [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;
|
||
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 [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 (
|
||
<section className="mb-4 rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-5" aria-label="Daily verse and devotional">
|
||
{verse && (
|
||
<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 */}
|
||
{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>
|
||
)}
|
||
</div>
|
||
)}
|
||
{reading && (
|
||
<details className="mt-3 border-t border-[var(--r-line)] pt-3">
|
||
<summary className="cursor-pointer text-sm font-medium">Spurgeon, {part === 'morning' ? 'Morning' : 'Evening'}: “{reading.heading_text}”</summary>
|
||
<div className="mt-2 flex gap-1.5">
|
||
{['morning', 'evening'].map((p) => (
|
||
<button key={p} type="button" onClick={() => setPart(p)} aria-pressed={part === p}
|
||
className={`rounded-full border px-2.5 py-0.5 text-[11px] ${part === p ? 'border-[var(--r-text)] bg-[var(--r-text)] text-[var(--r-bg)]' : 'border-[var(--r-line)] text-[var(--r-sub)]'}`}>{p === 'morning' ? 'Morning' : 'Evening'}</button>
|
||
))}
|
||
</div>
|
||
<p className="mt-1 text-xs text-[var(--r-sub)]">{reading.heading_citation}</p>
|
||
<div className="mt-2 max-h-72 overflow-y-auto whitespace-pre-line font-serif text-[15px] leading-relaxed">{reading.body}</div>
|
||
<p className="mt-2 text-[10px] text-[var(--r-sub)]">Charles Spurgeon, Morning and Evening (1866), via IQ Bible.</p>
|
||
</details>
|
||
)}
|
||
</section>
|
||
);
|
||
}
|