Stabilize Bible pages and add Strong's markers

This commit is contained in:
2026-07-12 08:45:33 -05:00
parent e0b5ee9e58
commit c0774c8e52
9 changed files with 163 additions and 24 deletions
+106 -11
View File
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useState } from 'react'
import { addTagToVerse, getLibraryState, searchVerses } from './studyClient'
import type { ReactNode } from 'react'
import type { Verse } from './types'
import './App.css'
@@ -17,6 +18,7 @@ function App() {
const [libraryVerses, setLibraryVerses] = useState<Verse[]>([])
const [verses, setVerses] = useState<Verse[]>([])
const [selectedId, setSelectedId] = useState<number | null>(null)
const [pageStartId, setPageStartId] = useState<number | null>(null)
const [selectedStrong, setSelectedStrong] = useState<string | null>(null)
const [tagInput, setTagInput] = useState('')
const [noteDraft, setNoteDraft] = useState('')
@@ -28,7 +30,9 @@ function App() {
const state = await getLibraryState()
setLibraryVerses(state.verses)
setVerses(state.verses)
setSelectedId((current) => current ?? state.verses[0]?.id ?? null)
const firstId = state.verses[0]?.id ?? null
setSelectedId((current) => current ?? firstId)
setPageStartId((current) => current ?? firstId)
}
async function runSearch(nextQuery = query) {
@@ -37,7 +41,9 @@ function App() {
setSelectedStrong(/^[gh]\d+$/i.test(trimmed) ? trimmed.toUpperCase() : null)
const results = await searchVerses(nextQuery)
setVerses(results)
setSelectedId(results[0]?.id ?? null)
const firstResult = results[0]
setSelectedId(firstResult?.id ?? null)
setPageStartId(firstResult ? findPageStartId(firstResult) : null)
}
async function addTag() {
@@ -76,13 +82,24 @@ function App() {
selectedVerse.strongs[0]
)
}, [selectedStrong, selectedVerse])
const pageAnchors = useMemo(() => {
const source = libraryVerses.length ? libraryVerses : verses
const anchors: Verse[] = []
for (let index = 0; index < source.length; index += pageSize) {
anchors.push(source[index])
}
return anchors
}, [libraryVerses, verses])
const pageVerses = useMemo(() => {
if (!selectedVerse) return []
const source = libraryVerses.length ? libraryVerses : verses
const pageStartIndex = source.findIndex((verse) => verse.id === pageStartId)
const selectedIndex = source.findIndex((verse) => verse.id === selectedVerse.id)
const start = Math.max(0, selectedIndex)
const start = Math.max(0, pageStartIndex >= 0 ? pageStartIndex : selectedIndex)
return source.slice(start, start + pageSize)
}, [libraryVerses, selectedVerse, verses])
}, [libraryVerses, pageStartId, selectedVerse, verses])
const pageRange = pageVerses.length
? `${reference(pageVerses[0])} - ${reference(pageVerses[pageVerses.length - 1])}`
: 'No page loaded'
@@ -130,16 +147,94 @@ function App() {
setSelectedStrong(null)
setVerses(libraryVerses)
setSelectedId(next.id)
setPageStartId(findPageStartId(next))
}
function shiftPage(direction: -1 | 1) {
if (!selectedVerse) return
const currentStartIndex = pageAnchors.findIndex((verse) => verse.id === pageStartId)
const selectedPageStartId = findPageStartId(selectedVerse)
const selectedIndex = pageAnchors.findIndex((verse) => verse.id === selectedPageStartId)
const index = currentStartIndex >= 0 ? currentStartIndex : selectedIndex
const next =
pageAnchors[Math.min(pageAnchors.length - 1, Math.max(0, index + direction))]
if (!next) return
setPageStartId(next.id)
setSelectedId(next.id)
setSelectedStrong(null)
}
function findPageStartId(verse: Verse) {
const source = libraryVerses.length ? libraryVerses : verses
const index = source.findIndex((verse) => verse.id === selectedVerse.id)
setSelectedId(
source[Math.min(source.length - 1, Math.max(0, index + direction * pageSize))]
?.id ?? selectedVerse.id,
)
const index = source.findIndex((entry) => entry.id === verse.id)
if (index < 0) return pageStartId ?? verse.id
const pageIndex = Math.floor(index / pageSize) * pageSize
return source[pageIndex]?.id ?? verse.id
}
function selectVerse(verse: Verse) {
setSelectedId(verse.id)
setSelectedStrong(null)
}
function selectStrong(verse: Verse, strong: string) {
setSelectedId(verse.id)
setSelectedStrong(strong)
}
function renderVerseText(verse: Verse) {
if (!verse.strongs.length) return verse.text
const parts: ReactNode[] = []
let cursor = 0
const lowerText = verse.text.toLowerCase()
const sortedLinks = [...verse.strongs].sort((left, right) => left.position - right.position)
sortedLinks.forEach((entry) => {
const surface = entry.surface.trim()
if (!surface) return
const index = lowerText.indexOf(surface.toLowerCase(), cursor)
if (index < 0) return
if (index > cursor) {
parts.push(verse.text.slice(cursor, index))
}
const linkedText = verse.text.slice(index, index + surface.length)
const isActive = entry.strong === selectedStrong
parts.push(
<span
key={`${verse.id}-${entry.strong}-${entry.position}`}
className={isActive ? 'strong-token active' : 'strong-token'}
role="button"
tabIndex={0}
title={`${entry.surface}: ${entry.strong}`}
onClick={(event) => {
event.stopPropagation()
selectStrong(verse, entry.strong)
}}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
selectStrong(verse, entry.strong)
}
}}
>
<span className="strong-surface">{linkedText}</span>
{(verse.id === selectedId || isActive) ? (
<small className="strong-code">{entry.strong}</small>
) : null}
</span>,
)
cursor = index + surface.length
})
if (cursor < verse.text.length) {
parts.push(verse.text.slice(cursor))
}
return parts.length ? parts : verse.text
}
function renderBiblePage(label = 'KJV') {
@@ -156,10 +251,10 @@ function App() {
<p
key={verse.id}
className={verse.id === selectedVerse.id ? 'page-verse selected' : 'page-verse'}
onClick={() => setSelectedId(verse.id)}
onClick={() => selectVerse(verse)}
>
<sup>{verse.verse}</sup>
{verse.text}
{renderVerseText(verse)}
</p>
))}
</div>