// The single home for per-verse tools: original words, cross-references, // commentary, and people & places, plus bookmark / copy. import { useEffect, useRef, useState } from 'react'; import Sheet from './Sheet.jsx'; import { CommentaryTab, PeoplePlacesTab } from '../StudyPanel.jsx'; import { BookmarkIcon, CopyIcon } from '../../utils/bibleUtils.jsx'; import { formatCrossRef, speakOriginalWord } from '../../utils/wordStudy.js'; import { READER_TRANSLATIONS, fetchChapter } from '../../utils/bibleText.js'; import { diffVerses, differenceLabel } from '../../utils/textDiff.js'; import { DiffText } from './DiffText.jsx'; import { HIGHLIGHT_COLORS, colorTitle } from '../../utils/highlightColors.js'; const TABS = [['commentary', 'Commentary'], ['compare', 'Compare'], ['original', 'Original'], ['refs', 'Cross-refs'], ['people', 'People & places']]; function OriginalTab({ words }) { if (!words) return

No original-language data for this verse.

; return (
{words.map((w, i) => { const isHebrew = w.s?.startsWith('H'); const canSpeak = isHebrew || w.s?.startsWith('G'); return (
{w.o}
{w.t}
{w.g}
{w.p &&
{w.p}
} {w.s &&
{w.s}
} {canSpeak && ( )}
); })}
); } // A cross-referenced passage shown in the panel: its words in the translation being read, without // leaving the chapter. "Open chapter" goes there when you want more. function RefPreview({ refItem, translation, onOpen, onClose }) { const [state, setState] = useState({ key: '', verses: [], error: '' }); const key = `${translation}/${refItem.book}/${refItem.chapter}`; useEffect(() => { const controller = new AbortController(); fetchChapter(translation, refItem.book, refItem.chapter, { signal: controller.signal }) .then(({ verses }) => setState({ key, verses, error: '' })) .catch((err) => { if (!controller.signal.aborted) setState({ key, verses: [], error: err.message || 'Unable to load that passage.' }); }); return () => controller.abort(); }, [key]); // eslint-disable-line react-hooks/exhaustive-deps const end = refItem.endVerse && refItem.endVerse > refItem.verse ? refItem.endVerse : refItem.verse; const picked = state.key === key ? state.verses.filter((v) => v.number >= refItem.verse && v.number <= end) : []; const loading = state.key !== key; return (

{formatCrossRef(refItem)} {translation}

{loading ?

Loading…

: state.error ?

{state.error}

: picked.length === 0 ?

Those verses weren’t found.

: (

{picked.map((v) => {picked.length > 1 && {v.number}}{v.text}{' '})}

)}
); } const sameRef = (a, b) => a && b && a.book === b.book && a.chapter === b.chapter && a.verse === b.verse && (a.endVerse ?? a.verse) === (b.endVerse ?? b.verse); function RefsTab({ refs, loading, active, onPick, preview }) { if (loading && !refs) return

Loading…

; if (!refs?.length) return

No cross-references for this verse.

; return ( <> {preview}

Tap one to read it here.

{refs.slice(0, 20).map((ref, i) => ( ))}
); } // This verse in every translation, one under the other. Each Bible loads the chapter it is in // (BSB and NET come from the free source; the rest through the server) and shows this verse. function CompareTab({ book, chapter, verse, current }) { const [rows, setRows] = useState({}); // id -> { text } | { error } | undefined while loading useEffect(() => { const controller = new AbortController(); setRows({}); READER_TRANSLATIONS.forEach((t) => { fetchChapter(t.id, book, chapter, { signal: controller.signal }) .then(({ verses }) => { const text = verses.find((v) => v.number === verse)?.text; setRows((r) => ({ ...r, [t.id]: text ? { text } : { error: 'No verse here in this translation.' } })); }) .catch((err) => { if (!controller.signal.aborted) setRows((r) => ({ ...r, [t.id]: { error: err.message || 'Unavailable.' } })); }); }); return () => controller.abort(); }, [book, chapter, verse]); const mine = rows[current]?.text; return (
{READER_TRANSLATIONS.map((t) => { const row = rows[t.id]; // How this wording differs from the Bible being read, word by word const d = row?.text && mine && t.id !== current ? diffVerses(mine, row.text) : null; return (

{t.id}{t.id === current ? ' · reading' : ''} {d && {differenceLabel(d.similarity)}}

{!row ?

Loading…

: row.error ?

{row.error}

:

}
); })}

{mine ? <>Marked words aren’t in the {current} wording. : null} Copyright notices for each translation appear at the foot of the chapter when you read it.

); } export default function VerseSheet({ verse, book, bookName, chapter, translation, words, crossRefs, crossRefsLoading, onLoadCrossRefs, bookmarkColor, onToggleBookmark, onCycleColor, onJump, onClose, request, verseHighlights = [], colorLabels = {}, onHighlightVerse, onClearVerseHighlights, panelRef, resizeHandle, }) { const [tab, setTab] = useState('commentary'); const [copied, setCopied] = useState(false); const [preview, setPreview] = useState(null); // the cross-reference being read in the panel const appliedRequest = useRef(0); // Leaving this verse closes the preview; a cross-reference tapped in the text opens it here. (Tracks the // previous verse so React's double-run of effects in development can't clear a preview it just opened.) const previousVerse = useRef(null); useEffect(() => { const verseChanged = previousVerse.current !== verse?.number; previousVerse.current = verse?.number; if (request && request.verse === verse?.number && request.n !== appliedRequest.current) { appliedRequest.current = request.n; setTab('refs'); setPreview(request.ref); } else if (verseChanged) { setPreview(null); } }, [verse?.number, request?.n]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (verse && tab === 'refs') onLoadCrossRefs(); }, [verse?.number, tab]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { setCopied(false); }, [verse?.number]); if (!verse) return null; const copy = () => { navigator.clipboard?.writeText(`${bookName} ${chapter}:${verse.number} ${translation} — ${verse.text}`); setCopied(true); window.setTimeout(() => setCopied(false), 1500); }; const actionBtn = 'rounded-lg p-1.5 hover:bg-black/5'; return ( {bookmarkColor && ( )} )} > {onHighlightVerse && ( // One-tap highlighting of the whole verse (to mark just some words, select them in the text)
Highlight {HIGHLIGHT_COLORS.map((color) => { const on = verseHighlights.some((h) => h.color === color && h.startOffset === 0 && h.endOffset >= verse.text.length); return ( ); })} {verseHighlights.length > 0 && ( )} Or select words in the text to highlight just those.
)}
{TABS.map(([id, label]) => ( ))}
{tab === 'commentary' && } {tab === 'compare' && } {tab === 'original' && } {tab === 'refs' && ( setPreview(null)} />} /> )} {tab === 'people' && }
); }