Redesign Scripture Index with BSB verse text and row layout; v1.2.1
Publish Container / docker (push) Successful in 13m38s

- Replace cramped auto-fill grid with full-width row layout (reference | verse text | arrow)
- Fetch BSB passage text from bible.helloao.org, grouped by chapter to minimize API calls
- Display 2-line italic excerpt of each passage beneath the lesson title
- Hover state reveals arrow and highlights row in gold
- Mobile-responsive collapse to single column

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-09-03 11:04:22 -04:00
1 parent 83d631608b
commit 36dfa0c1d7
3 files changed
+110 -18

No files matched your search

+92 -9
View File
@@ -1939,16 +1939,63 @@ function ResourcesPage({ content }: { content: SiteContent }) {
)
}
const SI_BOOK_MAP: Record<string, string> = {
gen: 'GEN', genesis: 'GEN', exo: 'EXO', exodus: 'EXO', lev: 'LEV', leviticus: 'LEV',
num: 'NUM', numbers: 'NUM', deut: 'DEU', deuteronomy: 'DEU', josh: 'JOS', joshua: 'JOS',
judg: 'JDG', judges: 'JDG', ruth: 'RUT', '1 sam': 'SA1', '1 samuel': 'SA1',
'2 sam': 'SA2', '2 samuel': 'SA2', '1 kgs': 'KI1', '1 kings': 'KI1', '2 kgs': 'KI2', '2 kings': 'KI2',
'1 chr': 'CH1', '1 chronicles': 'CH1', '2 chr': 'CH2', '2 chronicles': 'CH2',
ezra: 'EZR', neh: 'NEH', nehemiah: 'NEH', esth: 'EST', esther: 'EST',
job: 'JOB', ps: 'PSA', psa: 'PSA', psalms: 'PSA', psalm: 'PSA',
prov: 'PRO', proverbs: 'PRO', eccl: 'ECC', ecclesiastes: 'ECC', song: 'SNG',
isa: 'ISA', isaiah: 'ISA', jer: 'JER', jeremiah: 'JER', lam: 'LAM', lamentations: 'LAM',
ezek: 'EZK', ezekiel: 'EZK', dan: 'DAN', daniel: 'DAN', hos: 'HOS', hosea: 'HOS',
joel: 'JOL', amos: 'AMO', obad: 'OBA', obadiah: 'OBA', jonah: 'JNA', mic: 'MIC', micah: 'MIC',
nah: 'NAH', nahum: 'NAH', hab: 'HAB', habakkuk: 'HAB', zeph: 'ZEP', zephaniah: 'ZEP',
hag: 'HAG', haggai: 'HAG', zech: 'ZEC', zechariah: 'ZEC', mal: 'MAL', malachi: 'MAL',
matt: 'MAT', matthew: 'MAT', mark: 'MRK', luke: 'LUK', john: 'JHN', acts: 'ACT',
rom: 'ROM', romans: 'ROM', '1 cor': 'CO1', '1 corinthians': 'CO1', '2 cor': 'CO2', '2 corinthians': 'CO2',
gal: 'GAL', galatians: 'GAL', eph: 'EPH', ephesians: 'EPH', phil: 'PHP', philippians: 'PHP',
col: 'COL', colossians: 'COL', '1 thess': 'TH1', '1 thessalonians': 'TH1', '2 thess': 'TH2', '2 thessalonians': 'TH2',
'1 tim': 'TI1', '1 timothy': 'TI1', '2 tim': 'TI2', '2 timothy': 'TI2',
titus: 'TIT', philem: 'PHM', philemon: 'PHM', heb: 'HEB', hebrews: 'HEB',
jas: 'JAM', james: 'JAM', '1 pet': 'PE1', '1 peter': 'PE1', '2 pet': 'PE2', '2 peter': 'PE2',
'1 john': 'JO1', '2 john': 'JO2', '3 john': 'JO3', jude: 'JDE', rev: 'REV', revelation: 'REV',
}
function parseSIRef(ref: string) {
const m = ref.match(/^(.*?)\s+(\d+):(\d+)(?:-(\d+))?$/)
if (!m) return null
const bookId = SI_BOOK_MAP[m[1].toLowerCase().replace(/\.\s*/g, ' ').trim()]
if (!bookId) return null
return { bookId, chapter: parseInt(m[2], 10), verseStart: parseInt(m[3], 10), verseEnd: m[4] ? parseInt(m[4], 10) : parseInt(m[3], 10) }
}
type HellaoContent = Array<{ type: string; number?: number; content?: Array<{ text?: string } | string> }>
function extractVerseRange(chContent: HellaoContent, verseStart: number, verseEnd: number): string {
const texts: string[] = []
for (const item of chContent) {
if (item.type === 'verse' && item.number != null && item.number >= verseStart && item.number <= verseEnd) {
const t = (item.content ?? []).map(c => (typeof c === 'string' ? c : ((c as { text?: string }).text ?? ''))).join('').trim()
if (t) texts.push(t)
}
}
return texts.join(' ')
}
function ScriptureIndexPage({ content }: { content: SiteContent }) {
const siteTitle = content.seo?.title || DEFAULTS.seo.title
usePageMeta(
`Scripture Index | ${siteTitle}`,
'Every passage referenced across all Bible studies — find the lesson that covers a particular verse.',
'Every passage covered in our studies, organized by reference. Find the lesson for any verse.',
)
interface ScriptureEntry { sectionId: string; title: string; reference: string }
interface StudyGroup { slug: string; title: string; entries: ScriptureEntry[] }
const [studies, setStudies] = useState<StudyGroup[]>([])
const [loading, setLoading] = useState(true)
const [verseTexts, setVerseTexts] = useState<Record<string, string>>({})
useEffect(() => {
fetch('/api/public/scripture-index')
.then(r => r.json())
@@ -1956,6 +2003,37 @@ function ScriptureIndexPage({ content }: { content: SiteContent }) {
.catch(() => {})
.finally(() => setLoading(false))
}, [])
useEffect(() => {
if (studies.length === 0) return
// Group entries by chapter to minimize API calls
const chapterMap = new Map<string, Array<{ key: string; verseStart: number; verseEnd: number }>>()
for (const study of studies) {
for (const entry of study.entries) {
const parsed = parseSIRef(entry.reference)
if (!parsed) continue
const ck = `${parsed.bookId}/${parsed.chapter}`
if (!chapterMap.has(ck)) chapterMap.set(ck, [])
chapterMap.get(ck)!.push({ key: `${study.slug}/${entry.sectionId}`, verseStart: parsed.verseStart, verseEnd: parsed.verseEnd })
}
}
for (const [ck, entries] of chapterMap) {
const [bookId, chapter] = ck.split('/')
fetch(`https://bible.helloao.org/api/BSB/${bookId}/${chapter}.json`)
.then(r => r.ok ? r.json() : Promise.reject())
.then((data: { chapter: { content: HellaoContent } }) => {
const chContent = data?.chapter?.content ?? []
const updates: Record<string, string> = {}
for (const e of entries) {
const text = extractVerseRange(chContent, e.verseStart, e.verseEnd)
if (text) updates[e.key] = text
}
setVerseTexts(prev => ({ ...prev, ...updates }))
})
.catch(() => {})
}
}, [studies])
return (
<div className="site">
<SiteHeader content={content} />
@@ -1969,16 +2047,21 @@ function ScriptureIndexPage({ content }: { content: SiteContent }) {
) : studies.map(study => (
<section key={study.slug} className="scripture-study-group">
<h2 className="scripture-study-title">{study.title}</h2>
<ul className="scripture-entry-list">
{study.entries.map(entry => (
<li key={entry.sectionId} className="scripture-entry">
<Link to={`/study/${study.slug}/${entry.sectionId}`} className="scripture-entry-link">
<div className="scripture-entry-table">
{study.entries.map(entry => {
const verse = verseTexts[`${study.slug}/${entry.sectionId}`]
return (
<Link key={entry.sectionId} to={`/study/${study.slug}/${entry.sectionId}`} className="scripture-entry-row">
<span className="scripture-entry-ref">{entry.reference}</span>
{entry.title && <span className="scripture-entry-title">{entry.title}</span>}
<span className="scripture-entry-body">
{verse && <span className="scripture-entry-verse">"{verse}"</span>}
{entry.title && <span className="scripture-entry-lesson">{entry.title}</span>}
</span>
<span className="scripture-entry-arrow" aria-hidden="true">→</span>
</Link>
</li>
))}
</ul>
)
})}
</div>
</section>
))}
</main>