diff --git a/__tests__/lib/bible-search.test.ts b/__tests__/lib/bible-search.test.ts new file mode 100644 index 0000000..9406beb --- /dev/null +++ b/__tests__/lib/bible-search.test.ts @@ -0,0 +1,36 @@ +import { searchBooks, parseReference } from '@/lib/bible-search' + +describe('searchBooks', () => { + it('returns results for exact book prefix', () => { + const results = searchBooks('Genesis') + expect(results.length).toBeGreaterThan(0) + expect(results[0].bookName).toBe('Genesis') + }) + + it('parses "Book Chapter" format', () => { + const results = searchBooks('Genesis 5') + expect(results[0].chapter).toBe(5) + }) + + it('works with abbreviations', () => { + const results = searchBooks('Gen 1') + expect(results[0].bookName).toBe('Genesis') + }) + + it('returns empty array for empty query', () => { + expect(searchBooks('').length).toBe(0) + }) +}) + +describe('parseReference', () => { + it('parses full book name with chapter', () => { + const result = parseReference('Genesis 3') + expect(result?.bookId).toBe(1) + expect(result?.chapter).toBe(3) + }) + + it('defaults to chapter 1', () => { + const result = parseReference('Genesis') + expect(result?.chapter).toBe(1) + }) +}) diff --git a/components/bible/search-navigator.tsx b/components/bible/search-navigator.tsx new file mode 100644 index 0000000..fab75dd --- /dev/null +++ b/components/bible/search-navigator.tsx @@ -0,0 +1,95 @@ +'use client' +import { useState, useEffect } from 'react' +import { Search, Close } from '@mui/icons-material' +import { Box, TextField, InputAdornment, Paper, List, ListItem, ListItemButton, Typography } from '@mui/material' +import { searchBooks, type SearchResult } from '@/lib/bible-search' + +interface SearchNavigatorProps { + onNavigate: (bookId: number, chapter: number) => void +} + +export function SearchNavigator({ onNavigate }: SearchNavigatorProps) { + const [query, setQuery] = useState('') + const [results, setResults] = useState([]) + const [isOpen, setIsOpen] = useState(false) + + useEffect(() => { + if (query.trim()) { + setResults(searchBooks(query)) + setIsOpen(true) + } else { + setResults([]) + setIsOpen(false) + } + }, [query]) + + const handleSelect = (result: SearchResult) => { + onNavigate(result.bookId, result.chapter) + setQuery('') + setIsOpen(false) + } + + return ( + + setQuery(e.target.value)} + onFocus={() => query && setIsOpen(true)} + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: query && ( + + setQuery('')} + /> + + ), + }} + sx={{ + width: '100%', + '& .MuiOutlinedInput-root': { + fontSize: '0.95rem', + '@media (max-width: 600px)': { + fontSize: '1rem' // Larger on mobile to avoid zoom + } + } + }} + /> + + {isOpen && results.length > 0 && ( + + + {results.map((result, idx) => ( + + handleSelect(result)}> + + + {result.reference} + + + + + ))} + + + )} + + ) +} diff --git a/lib/bible-search.ts b/lib/bible-search.ts new file mode 100644 index 0000000..6bb0d15 --- /dev/null +++ b/lib/bible-search.ts @@ -0,0 +1,143 @@ +// Bible books data with abbreviations +const BIBLE_BOOKS = [ + // Old Testament + { id: 1, name: 'Genesis', abbr: 'Gen', chapters: 50 }, + { id: 2, name: 'Exodus', abbr: 'Ex', chapters: 40 }, + { id: 3, name: 'Leviticus', abbr: 'Lev', chapters: 27 }, + { id: 4, name: 'Numbers', abbr: 'Num', chapters: 36 }, + { id: 5, name: 'Deuteronomy', abbr: 'Deut', chapters: 34 }, + { id: 6, name: 'Joshua', abbr: 'Josh', chapters: 24 }, + { id: 7, name: 'Judges', abbr: 'Judg', chapters: 21 }, + { id: 8, name: 'Ruth', abbr: 'Ruth', chapters: 4 }, + { id: 9, name: '1 Samuel', abbr: '1Sam', chapters: 31 }, + { id: 10, name: '2 Samuel', abbr: '2Sam', chapters: 24 }, + { id: 11, name: '1 Kings', abbr: '1Kgs', chapters: 22 }, + { id: 12, name: '2 Kings', abbr: '2Kgs', chapters: 25 }, + { id: 13, name: '1 Chronicles', abbr: '1Chr', chapters: 29 }, + { id: 14, name: '2 Chronicles', abbr: '2Chr', chapters: 36 }, + { id: 15, name: 'Ezra', abbr: 'Ezra', chapters: 10 }, + { id: 16, name: 'Nehemiah', abbr: 'Neh', chapters: 13 }, + { id: 17, name: 'Esther', abbr: 'Esth', chapters: 10 }, + { id: 18, name: 'Job', abbr: 'Job', chapters: 42 }, + { id: 19, name: 'Psalms', abbr: 'Ps', chapters: 150 }, + { id: 20, name: 'Proverbs', abbr: 'Prov', chapters: 31 }, + { id: 21, name: 'Ecclesiastes', abbr: 'Eccl', chapters: 12 }, + { id: 22, name: 'Song of Solomon', abbr: 'Song', chapters: 8 }, + { id: 23, name: 'Isaiah', abbr: 'Isa', chapters: 66 }, + { id: 24, name: 'Jeremiah', abbr: 'Jer', chapters: 52 }, + { id: 25, name: 'Lamentations', abbr: 'Lam', chapters: 5 }, + { id: 26, name: 'Ezekiel', abbr: 'Ezek', chapters: 48 }, + { id: 27, name: 'Daniel', abbr: 'Dan', chapters: 12 }, + { id: 28, name: 'Hosea', abbr: 'Hos', chapters: 14 }, + { id: 29, name: 'Joel', abbr: 'Joel', chapters: 3 }, + { id: 30, name: 'Amos', abbr: 'Amos', chapters: 9 }, + { id: 31, name: 'Obadiah', abbr: 'Obad', chapters: 1 }, + { id: 32, name: 'Jonah', abbr: 'Jonah', chapters: 4 }, + { id: 33, name: 'Micah', abbr: 'Mic', chapters: 7 }, + { id: 34, name: 'Nahum', abbr: 'Nah', chapters: 3 }, + { id: 35, name: 'Habakkuk', abbr: 'Hab', chapters: 3 }, + { id: 36, name: 'Zephaniah', abbr: 'Zeph', chapters: 3 }, + { id: 37, name: 'Haggai', abbr: 'Hag', chapters: 2 }, + { id: 38, name: 'Zechariah', abbr: 'Zech', chapters: 14 }, + { id: 39, name: 'Malachi', abbr: 'Mal', chapters: 4 }, + // New Testament + { id: 40, name: 'Matthew', abbr: 'Matt', chapters: 28 }, + { id: 41, name: 'Mark', abbr: 'Mark', chapters: 16 }, + { id: 42, name: 'Luke', abbr: 'Luke', chapters: 24 }, + { id: 43, name: 'John', abbr: 'John', chapters: 21 }, + { id: 44, name: 'Acts', abbr: 'Acts', chapters: 28 }, + { id: 45, name: 'Romans', abbr: 'Rom', chapters: 16 }, + { id: 46, name: '1 Corinthians', abbr: '1Cor', chapters: 16 }, + { id: 47, name: '2 Corinthians', abbr: '2Cor', chapters: 13 }, + { id: 48, name: 'Galatians', abbr: 'Gal', chapters: 6 }, + { id: 49, name: 'Ephesians', abbr: 'Eph', chapters: 6 }, + { id: 50, name: 'Philippians', abbr: 'Phil', chapters: 4 }, + { id: 51, name: 'Colossians', abbr: 'Col', chapters: 4 }, + { id: 52, name: '1 Thessalonians', abbr: '1Thess', chapters: 5 }, + { id: 53, name: '2 Thessalonians', abbr: '2Thess', chapters: 3 }, + { id: 54, name: '1 Timothy', abbr: '1Tim', chapters: 6 }, + { id: 55, name: '2 Timothy', abbr: '2Tim', chapters: 4 }, + { id: 56, name: 'Titus', abbr: 'Titus', chapters: 3 }, + { id: 57, name: 'Philemon', abbr: 'Phlm', chapters: 1 }, + { id: 58, name: 'Hebrews', abbr: 'Heb', chapters: 13 }, + { id: 59, name: 'James', abbr: 'Jas', chapters: 5 }, + { id: 60, name: '1 Peter', abbr: '1Pet', chapters: 5 }, + { id: 61, name: '2 Peter', abbr: '2Pet', chapters: 3 }, + { id: 62, name: '1 John', abbr: '1John', chapters: 5 }, + { id: 63, name: '2 John', abbr: '2John', chapters: 1 }, + { id: 64, name: '3 John', abbr: '3John', chapters: 1 }, + { id: 65, name: 'Jude', abbr: 'Jude', chapters: 1 }, + { id: 66, name: 'Revelation', abbr: 'Rev', chapters: 22 } +] + +export interface SearchResult { + bookId: number + bookName: string + chapter: number + reference: string +} + +export function searchBooks(query: string): SearchResult[] { + if (!query.trim()) return [] + + const lowerQuery = query.toLowerCase() + const results: SearchResult[] = [] + + // Try to parse as "Book Chapter" format (e.g., "Genesis 1", "Gen 1") + const refMatch = query.match(/^([a-z\s]+)\s*(\d+)?/i) + if (refMatch) { + const bookQuery = refMatch[1].toLowerCase().trim() + const chapterNum = refMatch[2] ? parseInt(refMatch[2]) : 1 + + for (const book of BIBLE_BOOKS) { + if (book.name.toLowerCase().startsWith(bookQuery) || + book.abbr.toLowerCase().startsWith(bookQuery)) { + if (chapterNum <= book.chapters) { + results.push({ + bookId: book.id, + bookName: book.name, + chapter: chapterNum, + reference: `${book.name} ${chapterNum}` + }) + } + } + } + } + + // Fuzzy match on book names if exact prefix didn't work + if (results.length === 0) { + for (const book of BIBLE_BOOKS) { + if (book.name.toLowerCase().includes(lowerQuery) || + book.abbr.toLowerCase().includes(lowerQuery)) { + results.push({ + bookId: book.id, + bookName: book.name, + chapter: 1, + reference: book.name + }) + } + } + } + + return results.slice(0, 10) // Return top 10 +} + +export function parseReference(ref: string): { bookId: number; chapter: number } | null { + const match = ref.match(/^([a-z\s]+)\s*(\d+)?/i) + if (!match) return null + + const bookQuery = match[1].toLowerCase().trim() + const chapterNum = match[2] ? parseInt(match[2]) : 1 + + for (const book of BIBLE_BOOKS) { + if (book.name.toLowerCase().startsWith(bookQuery) || + book.abbr.toLowerCase() === bookQuery) { + return { + bookId: book.id, + chapter: Math.max(1, Math.min(chapterNum, book.chapters)) + } + } + } + + return null +}