Files
study-bible-project/src/components/reader/VerseSheet.jsx
T
nmemmertandClaude Sonnet 5.5 9e722a296d
Build and Push Docker Image / build-and-push (push) Failing after 20s
Redesign the Reader; add ESV
New reading surface with paper/sepia/dark themes, slim top and bottom bars,
and sheets for navigation, search, settings, audio and per-verse study tools.
Plan progress is built into the reader. Page-flip mode removed. ESV added via
the server-side proxy (ESV_API_KEY).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-29 09:11:47 -04:00

104 lines
5.0 KiB
React

// The single home for per-verse tools: original words, cross-references,
// commentary, and people & places, plus bookmark / copy.
import { useEffect, 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';
const TABS = [['commentary', 'Commentary'], ['original', 'Original'], ['refs', 'Cross-refs'], ['people', 'People & places']];
function OriginalTab({ words }) {
if (!words) return <p className="text-sm text-[var(--r-sub)]">No original-language data for this verse.</p>;
return (
<div className="flex flex-wrap gap-2">
{words.map((w, i) => {
const isHebrew = w.s?.startsWith('H');
const canSpeak = isHebrew || w.s?.startsWith('G');
return (
<div key={i} className="rounded-xl border border-[var(--r-line)] bg-[var(--r-bg)] p-2 text-center" style={{ minWidth: '4.5rem', maxWidth: '9rem' }}>
<div className={`text-lg leading-tight ${isHebrew ? 'font-serif' : ''}`} dir={isHebrew ? 'rtl' : 'ltr'}>{w.o}</div>
<div className="mt-0.5 text-xs italic text-[var(--r-sub)]">{w.t}</div>
<div className="mt-1 text-xs font-medium">{w.g}</div>
{w.p && <div className="mt-0.5 text-[10px] leading-tight text-[var(--r-sub)]">{w.p}</div>}
{w.s && <div className="mt-0.5 text-[10px] text-[var(--r-sub)]">{w.s}</div>}
{canSpeak && (
<button type="button" onClick={() => speakOriginalWord(w.o, w.s)}
className="mt-1.5 rounded-lg bg-[var(--r-bar)] px-2 py-0.5 text-[11px] hover:bg-black/5"
title={`Pronounce in ${isHebrew ? 'Hebrew' : 'Greek'}`}>Speak</button>
)}
</div>
);
})}
</div>
);
}
function RefsTab({ refs, loading, onJump }) {
if (loading && !refs) return <p className="text-sm text-[var(--r-sub)]">Loading…</p>;
if (!refs?.length) return <p className="text-sm text-[var(--r-sub)]">No cross-references for this verse.</p>;
return (
<div className="flex flex-wrap gap-1.5">
{refs.slice(0, 20).map((ref, i) => (
<button key={i} type="button" onClick={() => onJump(ref.book, ref.chapter, ref.verse)}
className="rounded-full border border-[var(--r-line)] px-2.5 py-1 text-xs hover:bg-black/5">
{formatCrossRef(ref)}
</button>
))}
</div>
);
}
export default function VerseSheet({
verse, book, bookName, chapter, translation, words, crossRefs, crossRefsLoading, onLoadCrossRefs,
bookmarkColor, onToggleBookmark, onCycleColor, onJump, onClose,
}) {
const [tab, setTab] = useState('commentary');
const [copied, setCopied] = useState(false);
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 (
<Sheet
open modal={false} pane onClose={onClose}
title={`${bookName} ${chapter}:${verse.number}`}
headerRight={(
<>
<button type="button" onClick={onToggleBookmark} className={`${actionBtn} ${bookmarkColor ? 'text-[var(--r-accent)]' : ''}`} aria-label={bookmarkColor ? 'Remove bookmark' : 'Bookmark verse'}>
<BookmarkIcon filled={!!bookmarkColor} />
</button>
{bookmarkColor && (
<button type="button" onClick={onCycleColor} className={actionBtn} aria-label="Change bookmark colour">
<span className="block h-4 w-4 rounded-full border border-black/10" style={{ backgroundColor: bookmarkColor }} />
</button>
)}
<button type="button" onClick={copy} className={actionBtn} aria-label="Copy verse">
{copied ? <span className="px-1 text-xs">Copied</span> : <CopyIcon />}
</button>
</>
)}
>
<div className="mb-3 flex gap-1.5 overflow-x-auto pb-1">
{TABS.map(([id, label]) => (
<button key={id} type="button" onClick={() => setTab(id)}
className={`shrink-0 rounded-full border px-3 py-1 text-xs ${tab === id ? 'border-[var(--r-text)] bg-[var(--r-text)] text-[var(--r-bg)]' : 'border-[var(--r-line)] text-[var(--r-sub)] hover:bg-black/5'}`}>
{label}
</button>
))}
</div>
{tab === 'commentary' && <CommentaryTab book={book} chapter={chapter} verse={verse.number} />}
{tab === 'original' && <OriginalTab words={words} />}
{tab === 'refs' && <RefsTab refs={crossRefs} loading={crossRefsLoading} onJump={onJump} />}
{tab === 'people' && <PeoplePlacesTab book={book} chapter={chapter} verse={verse.number} />}
</Sheet>
);
}