Build and Push Docker Image / build-and-push (push) Failing after 19s
- 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>
145 lines
6.7 KiB
React
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>
|
|
);
|
|
}
|