Stabilize Bible pages and add Strong's markers
This commit is contained in:
+106
-11
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user