Files
study-bible-project/src/components/iq/IqCommentary.jsx
T
nmemmertandClaude Sonnet 5.5 f274bc9c8a
Build and Push Docker Image / build-and-push (push) Successful in 7m14s
Harden sync and security; add IQ Bible, templates, password reset, backups
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>
2026-10-06 08:13:36 -04:00

57 lines
3.2 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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>
);
}