Files
study-bible-project/src/components/iq/IqStudyAids.jsx
T
nmemmertandClaude Sonnet 5.5 f70f1baf15
Build and Push Docker Image / build-and-push (push) Failing after 19s
IQ Bible on the Study side: commentaries, lexicons, study aids, outline
- Commentary tab: "More sources" switch with notes for the whole chunk and
  "Add to notes" quoting
- Word study: Lexicons button for words with a Strong's number
- New "Study aids" tab (any verse of the chunk; topics add as tags)
- Outline points: "Add from IQ Bible" attaches topics, Gospel parallels,
  manuscript notes and places

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 08:30:56 -04:00

145 lines
6.7 KiB
React

// A verse's "study aids" from IQ Bible: chapter overview, topics, places, people, textual notes, Gospel
// parallels and timeline. Each section loads only when opened, so nothing is spent on what isn't read.
import { useState } from 'react';
import { useIq } from '../../hooks/useIq.js';
import { listOf } from '../../utils/iqbible.js';
const muted = 'text-xs text-[var(--r-sub,#64748b)]';
const GOSPELS = ['MAT', 'MRK', 'LUK', 'JHN'];
function Aid({ title, path, query, children, empty = 'Nothing recorded here.' }) {
const [open, setOpen] = useState(false);
const { status, data, error } = useIq(open ? path : null, query);
return (
<details onToggle={(e) => setOpen(e.currentTarget.open)} className="rounded-xl bg-[var(--r-bg,#ffffff)] px-3 py-2">
<summary className="cursor-pointer text-sm font-medium">{title}</summary>
<div className="mt-2 text-[13px] leading-relaxed">
{status === 'loading' && <p className={muted}>Loading…</p>}
{status === 'error' && <p className={muted}>{error}</p>}
{status === 'empty' && <p className={muted}>{empty}</p>}
{status === 'ready' && children(data)}
</div>
</details>
);
}
function PersonProfile({ ustrong }) {
const { status, data } = useIq(`bible-characters/by-ustrong/${ustrong}`);
const p = data?.data;
if (status === 'loading') return <p className={muted}>Loading…</p>;
if (!p) return <p className={muted}>No profile.</p>;
return (
<div className="mt-1 space-y-1">
<p>{p.brief || p.briefest || p.description}</p>
{p.tribe_nation && <p className={muted}>{p.tribe_nation}</p>}
{p.first_appearance && <p className={muted}>First appears {p.first_appearance.book} {p.first_appearance.chapter}:{p.first_appearance.verse}</p>}
</div>
);
}
function People({ data, verse }) {
const [open, setOpen] = useState(null);
const people = listOf(data);
return (
<ul className="space-y-1">
{people.map((p) => (
<li key={p.ustrong}>
<button type="button" onClick={() => setOpen(open === p.ustrong ? null : p.ustrong)} className="text-left font-medium">
{p.name} <span className={muted}>{p.first_verse === verse ? 'this verse' : `v${p.first_verse}`} {open === p.ustrong ? '▾' : '▸'}</span>
</button>
{open === p.ustrong && <PersonProfile ustrong={p.ustrong} />}
</li>
))}
</ul>
);
}
function Places({ data }) {
const places = listOf(data);
return (
<ul className="space-y-1.5">
{places.map((p) => (
<li key={p.id} className="flex items-baseline justify-between gap-2">
<span>{p.name}<span className={muted}> {p.place_type}{p.modern_name && p.modern_name !== p.name ? ` · today: ${p.modern_name}` : ''}{p.special ? ` · ${p.special.replace(/_/g, ' ')}` : ''}</span></span>
{p.lat != null && p.lon != null && (
<a href={`https://www.openstreetmap.org/?mlat=${p.lat}&mlon=${p.lon}#map=8/${p.lat}/${p.lon}`} target="_blank" rel="noopener noreferrer" className="shrink-0 text-xs font-semibold text-sky-600 hover:underline">Map ↗</a>
)}
</li>
))}
</ul>
);
}
function Variants({ data }) {
const items = listOf(data);
return (
<ul className="space-y-3">
{items.map((v, i) => (
<li key={v.slug ?? i}>
<p className="font-medium">{v.title}</p>
{v.traditional_reading && <p><span className={muted}>Traditional (KJV/TR): </span>{v.traditional_reading}</p>}
{v.critical_reading && <p><span className={muted}>Critical text: </span>{v.critical_reading}</p>}
{v.explanation && <p className="mt-1">{v.explanation}</p>}
</li>
))}
</ul>
);
}
function Harmony({ data, book, chapter }) {
const items = listOf(data);
return (
<ul className="space-y-2">
{items.map((s) => (
<li key={s.number}>
<p className="font-medium">§{s.number} {String(s.title ?? '').toLowerCase().replace(/(^|[.\s])([a-z])/g, (m, a, b) => a + b.toUpperCase())}</p>
<p className={muted}>{(s.references ?? []).map((r) => r.citation ?? r).join(' · ')}</p>
</li>
))}
<li className={muted} aria-hidden="true">{book} {chapter}</li>
</ul>
);
}
export default function IqStudyAids({ book, chapter, verse, onTag }) {
return (
<div className="space-y-1.5">
<Aid title="About this chapter" path={`books/${book}/chapters/${chapter}/info`} empty="No chapter overview is available.">
{(d) => (
<>
<p className="whitespace-pre-line">{d.summary}</p>
{d.source_author && <p className={`mt-1 ${muted}`}>{d.source_author}{d.source_year ? `, ${d.source_year}` : ''}</p>}
</>
)}
</Aid>
<Aid title="Topics on this verse" path={`topics/${book}/${chapter}/${verse}`} empty="No topics are tagged on this verse.">
{(d) => (
<div className="flex flex-wrap gap-1.5">
{listOf(d).slice(0, 40).map((t, i) => (
onTag
? <button key={i} type="button" onClick={() => onTag(t.name)} className="rounded-full border border-[var(--r-line,#cbd5e1)] px-2 py-0.5 text-xs hover:bg-black/5" title={`Add “${t.name}” as a tag on this chunk`}>+ {t.name}</button>
: <span key={i} className="rounded-full border border-[var(--r-line,#cbd5e1)] px-2 py-0.5 text-xs" title={t.label || t.edition}>{t.name}</span>
))}
</div>
)}
</Aid>
<Aid title="Places on this verse" path={`geo/${book}/${chapter}/${verse}`} empty="No places are mentioned on this verse.">{(d) => <Places data={d} />}</Aid>
<Aid title="People in this chapter" path={`bible-people/${book}/${chapter}`} empty="No named people in this chapter.">{(d) => <People data={d} verse={verse} />}</Aid>
<Aid title="Textual notes" path={`textual-variants/${book}/${chapter}/${verse}`} empty="No known manuscript differences on this verse.">{(d) => <Variants data={d} />}</Aid>
{GOSPELS.includes(book) && (
<Aid title="Parallel accounts in the Gospels" path="harmony" query={{ book, chapter }} empty="No Gospel parallels recorded for this chapter.">{(d) => <Harmony data={d} book={book} chapter={chapter} />}</Aid>
)}
<Aid title="Where this sits in the timeline" path={`chronology/for/${book}/${chapter}`}>
{(d) => (
<>
<p>{d.placement ?? `${d.date ?? ''}`}</p>
{d.preceding?.length > 0 && <p className={muted}>Before: {d.preceding.map((e) => `${e.event} (${e.date})`).join('; ')}</p>}
{d.following?.length > 0 && <p className={muted}>After: {d.following.map((e) => `${e.event} (${e.date})`).join('; ')}</p>}
</>
)}
</Aid>
<p className="pt-1 text-[10px] text-[var(--r-sub,#64748b)]">Study aids via IQ Bible.</p>
</div>
);
}