Build and Push Docker Image / build-and-push (push) Successful in 7m14s
Sync and data safety - Autosave flushes on leaving a study or the tab; storage-full errors are reported - Unsynced studies, deletes and ink are queued and retried when back online - Deletes propagate across devices (with a "Keep it" undo) and no longer resurrect - Server assigns study versions (no client-clock conflicts); "Combine both" merges a conflicted study chunk by chunk - Reading-plan progress made offline is merged, not overwritten; plans, ink and marks are cleared from the device on sign-out Security - Block SSRF through the ntfy reminder URL; failure-only rate limiting per IP and account; constant-time login; TOTP replay protection; input validation; SESSION_SECRET required in production; admin user delete fixed - Forgot password: one-time code sent to the account's ntfy topic Features - IQ Bible proxy (route allow-list, text-only caching per its terms): more commentaries, lexicons, study aids, daily verse and devotional, plan calendar export, narrations, podcast background - Study templates, reading-plan catch-up, account export, daily database snapshots Tests and tooling - ESLint config; App tests split into src/test; tests for sync queues, library hook, merge, templates, IQ Bible UI and reset form Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
57 lines
3.2 KiB
React
57 lines
3.2 KiB
React
// More commentaries from IQ Bible: Gill and Matthew Henry plus hundreds of church-history sources.
|
||
// Only sources that actually have notes on the chapter being read are offered.
|
||
import { useState } from 'react';
|
||
import { useIq } from '../../hooks/useIq.js';
|
||
import { listOf, pickIqEntries } from '../../utils/iqbible.js';
|
||
|
||
const KEY = 'iq-commentary';
|
||
const muted = 'text-xs text-[var(--r-sub,#64748b)]';
|
||
|
||
export default function IqCommentary({ book, chapter, verse }) {
|
||
const [chosen, setChosen] = useState(() => { try { return localStorage.getItem(KEY) || 'mhenry'; } catch { return 'mhenry'; } });
|
||
const sources = useIq(`books/${book}/commentaries`, { chapter });
|
||
const list = listOf(sources.data);
|
||
const source = list.find((s) => s.name === chosen) ?? list.find((s) => s.name === 'mhenry') ?? list[0];
|
||
const notes = useIq(source ? `commentaries/${source.name}/${book}/${chapter}` : null);
|
||
const entries = pickIqEntries(notes.data?.entries, chapter, verse);
|
||
|
||
const pick = (name) => { setChosen(name); try { localStorage.setItem(KEY, name); } catch { /* storage unavailable */ } };
|
||
const groups = list.reduce((acc, s) => { (acc[s.category || 'Other'] ??= []).push(s); return acc; }, {});
|
||
|
||
if (sources.status === 'loading') return <p className={muted}>Loading…</p>;
|
||
if (sources.status === 'error') return <p className={muted}>{sources.error}</p>;
|
||
if (!list.length) return <p className={muted}>No IQ Bible commentaries cover this chapter.</p>;
|
||
|
||
return (
|
||
<div>
|
||
<label className={muted}>
|
||
Source ({list.length}){' '}
|
||
<select value={source?.name ?? ''} onChange={(e) => pick(e.target.value)} className="max-w-[16rem] rounded-lg border border-[var(--r-line,#cbd5e1)] bg-[var(--r-bg,#ffffff)] px-2 py-1 text-xs">
|
||
{Object.entries(groups).map(([cat, items]) => (
|
||
<optgroup key={cat} label={cat}>
|
||
{items.map((s) => <option key={s.name} value={s.name}>{s.author_name}{s.year ? ` (${s.year})` : ''}</option>)}
|
||
</optgroup>
|
||
))}
|
||
</select>
|
||
</label>
|
||
<div className="mt-2 space-y-2">
|
||
{notes.status === 'loading' && <p className={muted}>Loading…</p>}
|
||
{notes.status === 'error' && <p className={muted}>{notes.error}</p>}
|
||
{(notes.status === 'empty' || (notes.status === 'ready' && entries.length === 0)) && (
|
||
<p className={muted}>{source?.author_name} has no notes at or before verse {verse} in this chapter.</p>
|
||
)}
|
||
{entries.map((e, i) => (
|
||
<div key={i}>
|
||
<p className="mb-1 text-[11px] text-[var(--r-sub,#64748b)]">
|
||
{e.verse !== verse || e.verse_end ? `Notes on verse${e.verse_end && e.verse_end !== e.verse ? `s ${e.verse}–${e.verse_end}` : ` ${e.verse}`}` : ''}
|
||
{e.source_title ? ` · ${e.source_title}` : ''}
|
||
</p>
|
||
<div className="max-h-96 overflow-y-auto whitespace-pre-line rounded-xl bg-[var(--r-bg,#ffffff)] p-3 text-[13px] leading-relaxed text-[var(--r-text,#334155)]">{e.text}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
{source && <p className="mt-2 text-[10px] text-[var(--r-sub,#64748b)]">{source.author_name}{source.year ? `, ${source.year}` : ''} · via IQ Bible{source.license ? ` · ${source.license}` : ''}</p>}
|
||
</div>
|
||
);
|
||
}
|