Wire dynamic commentary and dictionary margin resources
This commit is contained in:
+263
-4
@@ -1,7 +1,13 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { addTagToVerse, getLibraryState, searchVerses } from './studyClient'
|
||||
import {
|
||||
addTagToVerse,
|
||||
getCommentaryForVerse,
|
||||
getDictionaryEntriesForTerms,
|
||||
getLibraryState,
|
||||
searchVerses,
|
||||
} from './studyClient'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Verse } from './types'
|
||||
import type { CommentaryEntry, DictionaryEntry, Verse } from './types'
|
||||
import './App.css'
|
||||
|
||||
const tagColors = ['#2f6f5e', '#735c0f', '#8a3ffc', '#b42318', '#245ba7']
|
||||
@@ -30,6 +36,8 @@ type StudySession = {
|
||||
baseTranslation: string
|
||||
footnotesEnabled: boolean
|
||||
strongsEnabled: boolean
|
||||
commentaryEnabled: boolean
|
||||
dictionaryEnabled: boolean
|
||||
timelineEnabled: boolean
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
@@ -58,6 +66,8 @@ function createSession(title = 'Study Session'): StudySession {
|
||||
baseTranslation: 'kjv-eng-kjv2006',
|
||||
footnotesEnabled: true,
|
||||
strongsEnabled: true,
|
||||
commentaryEnabled: true,
|
||||
dictionaryEnabled: true,
|
||||
timelineEnabled: true,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
@@ -151,6 +161,57 @@ function abbreviateBook(book: string) {
|
||||
return abbreviations[book] ?? book.slice(0, 4)
|
||||
}
|
||||
|
||||
function dictionaryTermsForVerse(verse: Verse) {
|
||||
const stopWords = new Set([
|
||||
'a',
|
||||
'an',
|
||||
'and',
|
||||
'are',
|
||||
'as',
|
||||
'at',
|
||||
'be',
|
||||
'but',
|
||||
'by',
|
||||
'for',
|
||||
'from',
|
||||
'he',
|
||||
'her',
|
||||
'him',
|
||||
'his',
|
||||
'i',
|
||||
'in',
|
||||
'is',
|
||||
'it',
|
||||
'of',
|
||||
'on',
|
||||
'or',
|
||||
'that',
|
||||
'the',
|
||||
'their',
|
||||
'them',
|
||||
'they',
|
||||
'this',
|
||||
'to',
|
||||
'unto',
|
||||
'was',
|
||||
'were',
|
||||
'with',
|
||||
'ye',
|
||||
'you',
|
||||
])
|
||||
const strongSurfaces = verse.strongs.map((entry) => entry.surface)
|
||||
const textWords = [...verse.text.matchAll(/[A-Za-z][A-Za-z'-]*/g)].map((match) => match[0])
|
||||
const unique = new Map<string, string>()
|
||||
|
||||
for (const term of [...strongSurfaces, ...textWords]) {
|
||||
const normalized = term.toLowerCase().replace(/[^a-z0-9\s'-]/g, '').trim()
|
||||
if (!normalized || stopWords.has(normalized) || normalized.length < 3) continue
|
||||
unique.set(normalized, term)
|
||||
}
|
||||
|
||||
return [...unique.values()].slice(0, 28)
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [railOpen, setRailOpen] = useState(false)
|
||||
const [sessionModalOpen, setSessionModalOpen] = useState(false)
|
||||
@@ -173,12 +234,19 @@ function App() {
|
||||
const [newSessionTitle, setNewSessionTitle] = useState('New KJV Study')
|
||||
const [newSessionFootnotes, setNewSessionFootnotes] = useState(true)
|
||||
const [newSessionStrongs, setNewSessionStrongs] = useState(true)
|
||||
const [newSessionCommentary, setNewSessionCommentary] = useState(true)
|
||||
const [newSessionDictionary, setNewSessionDictionary] = useState(true)
|
||||
const [newSessionTimeline, setNewSessionTimeline] = useState(true)
|
||||
const [lemmaFontSize, setLemmaFontSize] = useState(17)
|
||||
const [linkedFontSize, setLinkedFontSize] = useState(15)
|
||||
const [originalCollapsed, setOriginalCollapsed] = useState(false)
|
||||
const [linkedCollapsed, setLinkedCollapsed] = useState(false)
|
||||
const [definitionCollapsed, setDefinitionCollapsed] = useState(false)
|
||||
const [commentaryCollapsed, setCommentaryCollapsed] = useState(false)
|
||||
const [dictionaryCollapsed, setDictionaryCollapsed] = useState(false)
|
||||
const [commentaryEntries, setCommentaryEntries] = useState<CommentaryEntry[]>([])
|
||||
const [dictionaryEntries, setDictionaryEntries] = useState<DictionaryEntry[]>([])
|
||||
const [marginResourcesLoading, setMarginResourcesLoading] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const restoredSessionId = useRef('')
|
||||
@@ -350,6 +418,8 @@ function App() {
|
||||
baseTranslation: session.baseTranslation ?? 'kjv-eng-kjv2006',
|
||||
footnotesEnabled: session.footnotesEnabled ?? true,
|
||||
strongsEnabled: session.strongsEnabled ?? true,
|
||||
commentaryEnabled: session.commentaryEnabled ?? true,
|
||||
dictionaryEnabled: session.dictionaryEnabled ?? true,
|
||||
timelineEnabled: session.timelineEnabled ?? true,
|
||||
}))
|
||||
|
||||
@@ -422,6 +492,44 @@ function App() {
|
||||
() => sessions.find((session) => session.id === activeSessionId) ?? sessions[0],
|
||||
[activeSessionId, sessions],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
async function loadMarginResources() {
|
||||
if (!selectedVerse) {
|
||||
setCommentaryEntries([])
|
||||
setDictionaryEntries([])
|
||||
return
|
||||
}
|
||||
|
||||
setMarginResourcesLoading(true)
|
||||
try {
|
||||
const [nextCommentary, nextDictionary] = await Promise.all([
|
||||
activeSession?.commentaryEnabled === false
|
||||
? Promise.resolve([])
|
||||
: getCommentaryForVerse(selectedVerse),
|
||||
activeSession?.dictionaryEnabled === false
|
||||
? Promise.resolve([])
|
||||
: getDictionaryEntriesForTerms(dictionaryTermsForVerse(selectedVerse)),
|
||||
])
|
||||
|
||||
if (cancelled) return
|
||||
setCommentaryEntries(nextCommentary)
|
||||
setDictionaryEntries(nextDictionary)
|
||||
} catch (reason) {
|
||||
if (!cancelled) setError(String(reason))
|
||||
} finally {
|
||||
if (!cancelled) setMarginResourcesLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadMarginResources()
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [activeSession?.commentaryEnabled, activeSession?.dictionaryEnabled, selectedVerse])
|
||||
|
||||
const pageAnchors = useMemo(() => {
|
||||
const source = libraryVerses.length ? libraryVerses : verses
|
||||
const anchors: Verse[] = []
|
||||
@@ -629,6 +737,8 @@ function App() {
|
||||
...createSession(newSessionTitle.trim() || `Study Session ${sessions.length + 1}`),
|
||||
footnotesEnabled: newSessionFootnotes,
|
||||
strongsEnabled: newSessionStrongs,
|
||||
commentaryEnabled: newSessionCommentary,
|
||||
dictionaryEnabled: newSessionDictionary,
|
||||
timelineEnabled: newSessionTimeline,
|
||||
}
|
||||
setSessions((current) => [nextSession, ...current])
|
||||
@@ -641,7 +751,12 @@ function App() {
|
||||
if (patch.strongsEnabled === false) {
|
||||
setStrongDisplay('off')
|
||||
}
|
||||
if (typeof patch.strongsEnabled === 'boolean' || typeof patch.footnotesEnabled === 'boolean') {
|
||||
if (
|
||||
typeof patch.strongsEnabled === 'boolean' ||
|
||||
typeof patch.footnotesEnabled === 'boolean' ||
|
||||
typeof patch.commentaryEnabled === 'boolean' ||
|
||||
typeof patch.dictionaryEnabled === 'boolean'
|
||||
) {
|
||||
recordTimeline({ type: 'setting', label: 'Updated session resources' }, patch)
|
||||
}
|
||||
}
|
||||
@@ -873,6 +988,112 @@ function App() {
|
||||
)
|
||||
}
|
||||
|
||||
function renderCommentarySection() {
|
||||
if (activeSession?.commentaryEnabled === false) {
|
||||
return (
|
||||
<article className="resource-card">
|
||||
<header className="margin-section-header">
|
||||
<button type="button" className="collapse-button">Commentary</button>
|
||||
<span>Off</span>
|
||||
</header>
|
||||
<p className="resource-disabled">Commentary is disabled for this session.</p>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<article className="resource-card commentary-card" aria-label="Commentary">
|
||||
<header
|
||||
className="margin-section-header"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setCommentaryCollapsed((current) => !current)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
setCommentaryCollapsed((current) => !current)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<button type="button" className="collapse-button">Commentary</button>
|
||||
<span>{commentaryEntries.length ? 'MHCC' : marginResourcesLoading ? 'Loading' : 'None'}</span>
|
||||
</header>
|
||||
{!commentaryCollapsed ? (
|
||||
commentaryEntries.length ? (
|
||||
<div className="commentary-list">
|
||||
{commentaryEntries.map((entry) => (
|
||||
<section key={entry.id}>
|
||||
<h3>{entry.reference}</h3>
|
||||
<p>{entry.text}</p>
|
||||
<small>{entry.source}</small>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="resource-disabled">
|
||||
{marginResourcesLoading ? 'Loading commentary...' : 'No commentary matched this verse.'}
|
||||
</p>
|
||||
)
|
||||
) : null}
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
function renderDictionarySection() {
|
||||
if (activeSession?.dictionaryEnabled === false) {
|
||||
return (
|
||||
<article className="resource-card">
|
||||
<header className="margin-section-header">
|
||||
<button type="button" className="collapse-button">Dictionary</button>
|
||||
<span>Off</span>
|
||||
</header>
|
||||
<p className="resource-disabled">Dictionary lookup is disabled for this session.</p>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<article className="resource-card dictionary-card" aria-label="Dictionary">
|
||||
<header
|
||||
className="margin-section-header"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setDictionaryCollapsed((current) => !current)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
setDictionaryCollapsed((current) => !current)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<button type="button" className="collapse-button">Dictionary</button>
|
||||
<span>{dictionaryEntries.length ? `${dictionaryEntries.length} terms` : marginResourcesLoading ? 'Loading' : 'None'}</span>
|
||||
</header>
|
||||
{!dictionaryCollapsed ? (
|
||||
dictionaryEntries.length ? (
|
||||
<div className="dictionary-list">
|
||||
{dictionaryEntries.slice(0, 6).map((entry) => (
|
||||
<section key={entry.id}>
|
||||
<h3>{entry.term}</h3>
|
||||
{entry.definitions.slice(0, 2).map((definition) => (
|
||||
<p key={`${entry.id}-${definition.source}`}>
|
||||
<strong>{definition.sourceName}</strong>
|
||||
{definition.text}
|
||||
</p>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="resource-disabled">
|
||||
{marginResourcesLoading ? 'Loading dictionary...' : 'No dictionary terms matched this verse.'}
|
||||
</p>
|
||||
)
|
||||
) : null}
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
function renderBiblePage(label = 'KJV') {
|
||||
if (!selectedVerse) return null
|
||||
return (
|
||||
@@ -918,7 +1139,11 @@ function App() {
|
||||
<aside className="study-margin" aria-label="Study margin">
|
||||
<div className="panel-header">
|
||||
<h2>Study Margin</h2>
|
||||
<span>{activeSession?.strongsEnabled === false ? 'disabled' : `${selectedVerse.strongs.length} links`}</span>
|
||||
<span>
|
||||
{activeSession?.strongsEnabled === false
|
||||
? 'links off'
|
||||
: `${selectedVerse.strongs.length} links`}
|
||||
</span>
|
||||
</div>
|
||||
<p className="margin-reference">{reference(selectedVerse)}</p>
|
||||
{activeSession?.strongsEnabled === false ? (
|
||||
@@ -967,6 +1192,8 @@ function App() {
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
{renderCommentarySection()}
|
||||
{renderDictionarySection()}
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
@@ -1169,6 +1396,22 @@ function App() {
|
||||
/>
|
||||
Footnotes
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={activeSession?.commentaryEnabled ?? true}
|
||||
onChange={(event) => setActiveSessionOption({ commentaryEnabled: event.target.checked })}
|
||||
/>
|
||||
Commentary
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={activeSession?.dictionaryEnabled ?? true}
|
||||
onChange={(event) => setActiveSessionOption({ dictionaryEnabled: event.target.checked })}
|
||||
/>
|
||||
Dictionary
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -1212,6 +1455,22 @@ function App() {
|
||||
/>
|
||||
Include footnotes
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newSessionCommentary}
|
||||
onChange={(event) => setNewSessionCommentary(event.target.checked)}
|
||||
/>
|
||||
Include commentary
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newSessionDictionary}
|
||||
onChange={(event) => setNewSessionDictionary(event.target.checked)}
|
||||
/>
|
||||
Include dictionary
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
Reference in New Issue
Block a user