RED LETTER CODE
This commit is contained in:
+33
-7
@@ -1,8 +1,11 @@
|
||||
import { useEffect, useMemo, useState, type ReactNode } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { loadBible, searchBible, type SearchResult } from './bible'
|
||||
import { getVerseText, loadBible, searchBible, type SearchResult } from './bible'
|
||||
import { allRedLetter, loadRedLetter } from './redletter'
|
||||
import TopBar from './TopBar'
|
||||
|
||||
const RED_LETTER_CODE = /^jesusredletter$/i
|
||||
|
||||
function highlight(text: string, query: string): ReactNode {
|
||||
const ql = query.trim().toLowerCase()
|
||||
if (!ql) return text
|
||||
@@ -28,7 +31,7 @@ export default function SearchPage() {
|
||||
const [total, setTotal] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
loadBible().then(() => setReady(true))
|
||||
Promise.all([loadBible(), loadRedLetter()]).then(() => setReady(true))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -36,7 +39,22 @@ export default function SearchPage() {
|
||||
return () => window.clearTimeout(id)
|
||||
}, [query])
|
||||
|
||||
const outcome = useMemo(() => searchBible(debounced), [debounced])
|
||||
const secret = RED_LETTER_CODE.test(debounced.trim())
|
||||
|
||||
const outcome = useMemo(() => {
|
||||
if (secret) {
|
||||
const results: SearchResult[] = allRedLetter().map((e) => ({
|
||||
text: e.quote ?? getVerseText(e.book, e.chapter, e.verse) ?? '',
|
||||
reference: e.reference,
|
||||
book: e.book,
|
||||
chapter: e.chapter,
|
||||
verse: e.verse,
|
||||
}))
|
||||
return { results, total: results.length, redLetter: true }
|
||||
}
|
||||
const res = searchBible(debounced)
|
||||
return { results: res.results, total: res.total, redLetter: false }
|
||||
}, [secret, debounced, ready])
|
||||
|
||||
useEffect(() => {
|
||||
setResults(outcome.results)
|
||||
@@ -66,14 +84,20 @@ export default function SearchPage() {
|
||||
|
||||
{ready && searching && <p className="search-meta">Searching…</p>}
|
||||
|
||||
{ready && !searching && query.trim() === '' && (
|
||||
{ready && !searching && query.trim() === '' && (
|
||||
<p className="search-meta">Enter a word or phrase to search all 31,102 verses.</p>
|
||||
)}
|
||||
|
||||
{ready && !searching && query.trim() !== '' && (
|
||||
{ready && !searching && secret && (
|
||||
<div className="redletter-banner" aria-live="polite">
|
||||
<h2>The Words of Christ</h2>
|
||||
<p>{total} verses — the recorded words of Jesus</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ready && !searching && query.trim() !== '' && !secret && (
|
||||
<p className="search-meta" aria-live="polite">
|
||||
{total > 0 ? `${total} verse${total === 1 ? '' : 's'} found` : 'No verses found.'}
|
||||
{total > results.length && ` · showing the first ${results.length}`}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -85,7 +109,9 @@ export default function SearchPage() {
|
||||
to={`/read/${encodeURIComponent(r.book)}/${r.chapter}#v${r.verse}`}
|
||||
>
|
||||
<span className="search-result-ref">{r.reference}</span>
|
||||
<span className="search-result-text">{highlight(r.text, query)}</span>
|
||||
<span className="search-result-text">
|
||||
{outcome.redLetter ? <span className="jesus">{r.text}</span> : highlight(r.text, debounced)}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
+7
-7
@@ -65,7 +65,7 @@ export function loadBible(): Promise<void> {
|
||||
index.push({
|
||||
book: b.book,
|
||||
chapter: cn,
|
||||
verse: v.verse,
|
||||
verse: Number(v.verse),
|
||||
text: v.text,
|
||||
lower: v.text.toLowerCase(),
|
||||
refLower: `${displayName(b.book)} ${cn}:${v.verse}`.toLowerCase(),
|
||||
@@ -93,6 +93,10 @@ export function getChapter(book: KJVBookData, chapter: number): KJVChapterData |
|
||||
return book.chapters[chapter - 1]
|
||||
}
|
||||
|
||||
export function getVerseText(book: string, chapter: number, verse: number): string | undefined {
|
||||
return index.find((e) => e.book === book && Number(e.chapter) === chapter && Number(e.verse) === verse)?.text
|
||||
}
|
||||
|
||||
function displayName(bookName: string): string {
|
||||
return bookName === 'Psalms' ? 'Psalm' : bookName
|
||||
}
|
||||
@@ -101,18 +105,14 @@ export function referenceFor(book: KJVBookData, chapter: number, verse: number):
|
||||
return `${displayName(book.book)} ${chapter}:${verse}`
|
||||
}
|
||||
|
||||
export function searchBible(query: string, limit = 60): SearchOutcome {
|
||||
export function searchBible(query: string): SearchOutcome {
|
||||
const q = query.trim().toLowerCase()
|
||||
if (!q) return { results: [], total: 0 }
|
||||
const results: SearchResult[] = []
|
||||
let total = 0
|
||||
for (const e of index) {
|
||||
if (e.lower.includes(q) || e.refLower.includes(q)) {
|
||||
total++
|
||||
if (results.length < limit) {
|
||||
results.push({ text: e.text, reference: `${displayName(e.book)} ${e.chapter}:${e.verse}`, book: e.book, chapter: e.chapter, verse: e.verse })
|
||||
}
|
||||
}
|
||||
}
|
||||
return { results, total }
|
||||
return { results, total: results.length }
|
||||
}
|
||||
@@ -1,8 +1,19 @@
|
||||
export type RedLetterEntry = string | null
|
||||
|
||||
export interface RedLetterVerse {
|
||||
book: string
|
||||
chapter: number
|
||||
verse: number
|
||||
reference: string
|
||||
quote: string | null
|
||||
}
|
||||
|
||||
let loaded = false
|
||||
let promise: Promise<void> | null = null
|
||||
let map: Map<string, string | null> = new Map()
|
||||
let entries: RedLetterVerse[] = []
|
||||
|
||||
const refRe = /^([A-Za-z]+) (\d+):(\d+)$/
|
||||
|
||||
export function loadRedLetter(): Promise<void> {
|
||||
if (loaded) return Promise.resolve()
|
||||
@@ -14,8 +25,19 @@ export function loadRedLetter(): Promise<void> {
|
||||
})
|
||||
.then((data) => {
|
||||
map = new Map()
|
||||
entries = []
|
||||
for (const [ref, value] of Object.entries(data.verses)) {
|
||||
map.set(ref.toLowerCase(), value === 'full' ? null : value)
|
||||
const m = refRe.exec(ref)
|
||||
if (m) {
|
||||
entries.push({
|
||||
book: m[1],
|
||||
chapter: Number(m[2]),
|
||||
verse: Number(m[3]),
|
||||
reference: ref,
|
||||
quote: value === 'full' ? null : value,
|
||||
})
|
||||
}
|
||||
}
|
||||
loaded = true
|
||||
})
|
||||
@@ -28,6 +50,10 @@ export function redLetterFor(book: string, chapter: number, verse: number): RedL
|
||||
return map.get(`${name} ${chapter}:${verse}`.toLowerCase())
|
||||
}
|
||||
|
||||
export function allRedLetter(): RedLetterVerse[] {
|
||||
return entries
|
||||
}
|
||||
|
||||
function normalize(s: string): string {
|
||||
let out = ''
|
||||
for (const c of s) {
|
||||
|
||||
@@ -719,6 +719,33 @@ a {
|
||||
margin: 1.1rem 0 2rem;
|
||||
}
|
||||
|
||||
.redletter-banner {
|
||||
text-align: center;
|
||||
margin: 1.1rem 0 2rem;
|
||||
padding: 1.4rem 1.5rem;
|
||||
border: 1px solid rgba(208, 95, 90, 0.45);
|
||||
border-radius: 8px;
|
||||
background: rgba(208, 95, 90, 0.08);
|
||||
}
|
||||
|
||||
.redletter-banner h2 {
|
||||
font-family: var(--display);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3em;
|
||||
text-indent: 0.3em;
|
||||
font-size: 1.15rem;
|
||||
color: var(--red);
|
||||
margin: 0 0 0.35rem;
|
||||
}
|
||||
|
||||
.redletter-banner p {
|
||||
font-style: italic;
|
||||
color: var(--ink-dim);
|
||||
font-size: 0.88rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
max-width: 46rem;
|
||||
margin: 0 auto;
|
||||
|
||||
+3
-3
@@ -51,14 +51,14 @@ export async function loadKJV(): Promise<DailyVerse[]> {
|
||||
for (const chapter of book.chapters) {
|
||||
const chapterNumber = Number(chapter.chapter)
|
||||
for (const v of chapter.verses) {
|
||||
const entry = redLetterFor(book.book, chapterNumber, v.verse)
|
||||
const entry = redLetterFor(book.book, chapterNumber, Number(v.verse))
|
||||
if (entry === undefined) continue
|
||||
flatRed.push({
|
||||
text: entry === null ? v.text : entry,
|
||||
reference: `${name} ${chapterNumber}:${v.verse}`,
|
||||
reference: `${name} ${chapterNumber}:${Number(v.verse)}`,
|
||||
book: book.book,
|
||||
chapter: chapterNumber,
|
||||
verse: v.verse,
|
||||
verse: Number(v.verse),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user