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>
This commit is contained in:
1 parent
9e8420a1ae
commit
9e722a296d
17 files changed
+903
-791
No files matched your search
@@ -0,0 +1,77 @@
|
||||
// Search this chapter, or the whole Bible (BSB — the only translation that can be
|
||||
// downloaded once and searched in the browser).
|
||||
import { useMemo } from 'react';
|
||||
import Sheet from './Sheet.jsx';
|
||||
import { SearchIcon } from './icons.jsx';
|
||||
|
||||
export default function SearchSheet({
|
||||
open, onClose, query, onQuery, scope, onScope, verses, bookName, chapter,
|
||||
indexStatus, onLoadIndex, bibleMatches, deferredBibleQuery, onJump, currentBook,
|
||||
}) {
|
||||
const q = query.trim().toLowerCase();
|
||||
const chapterMatches = useMemo(
|
||||
() => (scope === 'chapter' && q ? verses.filter((v) => v.text.toLowerCase().includes(q)) : []),
|
||||
[scope, q, verses],
|
||||
);
|
||||
|
||||
const snippet = (text, needle) => {
|
||||
const i = text.toLowerCase().indexOf(needle);
|
||||
if (i === -1 || !needle) return text;
|
||||
return (<>{text.slice(0, i)}<mark className="rounded bg-[var(--r-hl)] px-0.5 text-[var(--r-text)]">{text.slice(i, i + needle.length)}</mark>{text.slice(i + needle.length)}</>);
|
||||
};
|
||||
|
||||
const scopeBtn = (id, label, onClick) => (
|
||||
<button type="button" onClick={onClick}
|
||||
className={`px-3 py-1.5 text-xs ${scope === id ? 'bg-[var(--r-text)] text-[var(--r-bg)]' : 'text-[var(--r-sub)] hover:bg-black/5'}`}>{label}</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Sheet open={open} onClose={onClose} title="Search">
|
||||
<div className="mb-3 flex items-center gap-2 rounded-xl border border-[var(--r-line)] bg-[var(--r-bg)] px-3 py-2">
|
||||
<span className="text-[var(--r-sub)]"><SearchIcon /></span>
|
||||
<input autoFocus value={query} onChange={(e) => onQuery(e.target.value)}
|
||||
placeholder={scope === 'bible' ? 'Search the whole Bible (BSB)…' : `Search ${bookName} ${chapter}…`}
|
||||
className="min-w-0 flex-1 bg-transparent text-sm text-[var(--r-text)] outline-none placeholder:text-[var(--r-sub)]" />
|
||||
</div>
|
||||
<div className="mb-3 inline-flex overflow-hidden rounded-lg border border-[var(--r-line)]">
|
||||
{scopeBtn('chapter', 'This chapter', () => onScope('chapter'))}
|
||||
{scopeBtn('bible', 'Whole Bible (BSB)', () => { onScope('bible'); if (indexStatus === 'idle') onLoadIndex(); })}
|
||||
</div>
|
||||
|
||||
{scope === 'chapter' && (
|
||||
!q ? <p className="text-sm text-[var(--r-sub)]">Type a word to find it in this chapter.</p>
|
||||
: chapterMatches.length === 0 ? <p className="text-sm text-[var(--r-sub)]">No verses match “{query}”.</p>
|
||||
: chapterMatches.map((v) => (
|
||||
<button key={v.number} type="button" onClick={() => onJump(currentBook, chapter, v.number)}
|
||||
className="block w-full rounded-lg px-2 py-2 text-left text-sm hover:bg-black/5">
|
||||
<span className="mr-1.5 font-medium">{v.number}</span>{snippet(v.text, q)}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
|
||||
{scope === 'bible' && (
|
||||
<>
|
||||
{indexStatus === 'loading' && <p className="text-sm text-[var(--r-sub)]">Loading the full Bible for search. This happens once per visit (about 7 MB).</p>}
|
||||
{indexStatus === 'error' && <p className="text-sm text-red-600">Couldn’t load the full Bible. <button type="button" onClick={onLoadIndex} className="underline">Try again</button></p>}
|
||||
{indexStatus === 'ready' && (!deferredBibleQuery
|
||||
? <p className="text-sm text-[var(--r-sub)]">Type a word to search all 66 books.</p>
|
||||
: bibleMatches.length === 0
|
||||
? <p className="text-sm text-[var(--r-sub)]">No verses match “{query}”.</p>
|
||||
: (
|
||||
<>
|
||||
<p className="mb-2 text-[11px] uppercase tracking-wide text-[var(--r-sub)]">
|
||||
{bibleMatches.length} match{bibleMatches.length === 1 ? '' : 'es'}{bibleMatches.length > 100 ? ' (showing first 100)' : ''}
|
||||
</p>
|
||||
{bibleMatches.slice(0, 100).map((v) => (
|
||||
<button key={`${v.bookAbbrev}-${v.chapter}-${v.verse}`} type="button" onClick={() => onJump(v.bookAbbrev, v.chapter, v.verse)}
|
||||
className="block w-full rounded-lg px-2 py-2 text-left text-sm hover:bg-black/5">
|
||||
<span className="mr-1.5 font-medium">{v.bookName} {v.chapter}:{v.verse}</span>{snippet(v.text, deferredBibleQuery)}
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user