From 8919901a49b8cecd756c6967f8759fe1481be4d4 Mon Sep 17 00:00:00 2001 From: h202-wq Date: Wed, 19 Aug 2026 10:38:33 -0400 Subject: [PATCH] RED LETTER CODE --- src/SearchPage.tsx | 40 +++++++++++++++++++++++++++++++++------- src/bible.ts | 16 ++++++++-------- src/redletter.ts | 26 ++++++++++++++++++++++++++ src/style.css | 27 +++++++++++++++++++++++++++ src/verse.ts | 6 +++--- 5 files changed, 97 insertions(+), 18 deletions(-) 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), }) } }