diff --git a/src/SearchPage.tsx b/src/SearchPage.tsx
index 28d5b0d..9bbf12c 100644
--- a/src/SearchPage.tsx
+++ b/src/SearchPage.tsx
@@ -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 &&
Searching…
}
- {ready && !searching && query.trim() === '' && (
+{ready && !searching && query.trim() === '' && (
Enter a word or phrase to search all 31,102 verses.
)}
- {ready && !searching && query.trim() !== '' && (
+ {ready && !searching && secret && (
+
+
The Words of Christ
+
{total} verses — the recorded words of Jesus
+
+ )}
+
+ {ready && !searching && query.trim() !== '' && !secret && (
{total > 0 ? `${total} verse${total === 1 ? '' : 's'} found` : 'No verses found.'}
- {total > results.length && ` · showing the first ${results.length}`}
)}
@@ -85,7 +109,9 @@ export default function SearchPage() {
to={`/read/${encodeURIComponent(r.book)}/${r.chapter}#v${r.verse}`}
>
{r.reference}
- {highlight(r.text, query)}
+
+ {outcome.redLetter ? {r.text} : highlight(r.text, debounced)}
+
))}
diff --git a/src/bible.ts b/src/bible.ts
index f4f9f52..a1fb78b 100644
--- a/src/bible.ts
+++ b/src/bible.ts
@@ -65,7 +65,7 @@ export function loadBible(): Promise {
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 })
- }
+ 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 }
}
\ No newline at end of file
diff --git a/src/redletter.ts b/src/redletter.ts
index f6d11fc..dcbc36a 100644
--- a/src/redletter.ts
+++ b/src/redletter.ts
@@ -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 | null = null
let map: Map = new Map()
+let entries: RedLetterVerse[] = []
+
+const refRe = /^([A-Za-z]+) (\d+):(\d+)$/
export function loadRedLetter(): Promise {
if (loaded) return Promise.resolve()
@@ -14,8 +25,19 @@ export function loadRedLetter(): Promise {
})
.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) {
diff --git a/src/style.css b/src/style.css
index 235c727..ee730f6 100644
--- a/src/style.css
+++ b/src/style.css
@@ -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;
diff --git a/src/verse.ts b/src/verse.ts
index c7189e8..0b0c2b6 100644
--- a/src/verse.ts
+++ b/src/verse.ts
@@ -51,14 +51,14 @@ export async function loadKJV(): Promise {
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),
})
}
}