Files
biblical-guide.com/app/[locale]/search/page.tsx

490 lines
17 KiB
TypeScript

'use client'
import {
Container,
Grid,
Card,
CardContent,
Typography,
Box,
TextField,
Button,
Paper,
List,
ListItem,
ListItemText,
Chip,
InputAdornment,
FormControl,
InputLabel,
Select,
MenuItem,
Accordion,
AccordionSummary,
AccordionDetails,
useTheme,
CircularProgress,
Skeleton,
} from '@mui/material'
import {
Search,
FilterList,
ExpandMore,
MenuBook,
Close,
History,
} from '@mui/icons-material'
import { useState, useEffect } from 'react'
import { useTranslations, useLocale } from 'next-intl'
interface SearchResult {
id: string
book: string
chapter: number
verse: number
text: string
relevance: number
}
interface SearchFilter {
testament: 'all' | 'old' | 'new'
bookKeys: string[]
exactMatch: boolean
}
interface BookOption {
id: string
name: string
bookKey: string
orderNum: number
testament: string
}
export default function SearchPage() {
const theme = useTheme()
const t = useTranslations('pages.search')
const locale = useLocale()
const [searchQuery, setSearchQuery] = useState('')
const [results, setResults] = useState<SearchResult[]>([])
const [loading, setLoading] = useState(false)
const [searchHistory, setSearchHistory] = useState<string[]>([])
const [filters, setFilters] = useState<SearchFilter>({ testament: 'all', bookKeys: [], exactMatch: false })
const [booksData, setBooksData] = useState<BookOption[]>([])
const [versions, setVersions] = useState<Array<{ id: string; name: string; abbreviation: string; isDefault: boolean }>>([])
const [selectedVersion, setSelectedVersion] = useState<string>('')
const oldTestamentBooks = booksData.filter(b => b.orderNum <= 39)
const newTestamentBooks = booksData.filter(b => b.orderNum > 39)
const popularSearches: string[] = t.raw('popular.items')
useEffect(() => {
// Load search history from localStorage
const saved = localStorage.getItem('searchHistory')
if (saved) {
setSearchHistory(JSON.parse(saved))
}
}, [])
useEffect(() => {
// Fetch available versions for locale
fetch(`/api/bible/versions?locale=${locale}`)
.then(res => res.json())
.then(data => {
const list = (data.versions || []) as Array<{ id: string; name: string; abbreviation: string; isDefault: boolean }>
setVersions(list)
const def = list.find(v => v.isDefault) || list[0]
setSelectedVersion(def?.abbreviation || '')
})
.catch(() => {
setVersions([])
setSelectedVersion('')
})
}, [locale])
useEffect(() => {
if (!selectedVersion && versions.length === 0) return
// Fetch available books for current locale/version
const qs = new URLSearchParams({ locale, ...(selectedVersion ? { version: selectedVersion } : {}) })
fetch(`/api/bible/books?${qs}`)
.then(res => res.json())
.then(data => {
const mapped: BookOption[] = (data.books || []).map((b: any) => ({
id: b.id,
name: b.name,
bookKey: b.bookKey,
orderNum: b.orderNum,
testament: b.testament,
}))
setBooksData(mapped)
})
.catch(() => setBooksData([]))
}, [locale, selectedVersion, versions.length])
const handleSearch = async () => {
if (!searchQuery.trim()) return
setLoading(true)
// Add to search history
const newHistory = [searchQuery, ...searchHistory.filter(s => s !== searchQuery)].slice(0, 10)
setSearchHistory(newHistory)
localStorage.setItem('searchHistory', JSON.stringify(newHistory))
try {
const params = new URLSearchParams({
q: searchQuery,
testament: filters.testament,
exactMatch: filters.exactMatch.toString(),
bookKeys: filters.bookKeys.join(','),
locale,
...(selectedVersion ? { version: selectedVersion } : {}),
})
const response = await fetch(`/api/search/verses?${params}`)
if (!response.ok) {
throw new Error('Search failed')
}
const data = await response.json()
setResults(data.results || [])
} catch (error) {
console.error('Error searching:', error)
// Mock results for demo
setResults([
{
id: '1',
book: 'Ioan',
chapter: 3,
verse: 16,
text: 'Fiindcă atât de mult a iubit Dumnezeu lumea, că a dat pe singurul Său Fiu, pentru ca oricine crede în El să nu piară, ci să aibă viața veșnică.',
relevance: 0.95,
},
{
id: '2',
book: '1 Corinteni',
chapter: 13,
verse: 4,
text: 'Dragostea este îndelung răbdătoare, dragostea este binevoitoare; dragostea nu pizmuiește...',
relevance: 0.89,
},
])
} finally {
setLoading(false)
}
}
const handleKeyPress = (event: React.KeyboardEvent) => {
if (event.key === 'Enter') {
handleSearch()
}
}
const clearFilters = () => {
setFilters({
testament: 'all',
books: [],
exactMatch: false,
})
}
const highlightSearchTerm = (text: string, query: string) => {
if (!query) return text
const regex = new RegExp(`(${query})`, 'gi')
const parts = text.split(regex)
return parts.map((part, index) =>
regex.test(part) ? (
<Typography
key={index}
component="span"
sx={{ backgroundColor: 'yellow', fontWeight: 'bold' }}
>
{part}
</Typography>
) : (
part
)
)
}
return (
<Box>
<Container maxWidth="lg" sx={{ py: 4 }}>
{/* Header */}
<Box sx={{ mb: 4, textAlign: 'center' }}>
<Typography variant="h3" component="h1" gutterBottom>
<Search sx={{ fontSize: 40, mr: 2, verticalAlign: 'middle' }} />
{t('title')}
</Typography>
<Typography variant="body1" color="text.secondary">
{t('subtitle')}
</Typography>
</Box>
<Grid container spacing={4}>
{/* Search Sidebar */}
<Grid item xs={12} md={3}>
{/* Search Filters */}
<Card sx={{ mb: 3 }}>
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6">
<FilterList sx={{ mr: 1, verticalAlign: 'middle' }} />
{t('filters.title')}
</Typography>
<Button size="small" onClick={clearFilters}>
{t('filters.clear')}
</Button>
</Box>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel>{t('filters.testament')}</InputLabel>
<Select
value={filters.testament}
label={t('filters.testament')}
onChange={(e) => setFilters({ ...filters, testament: e.target.value as any })}
>
<MenuItem value="all">{t('filters.options.all')}</MenuItem>
<MenuItem value="old">{t('filters.options.old')}</MenuItem>
<MenuItem value="new">{t('filters.options.new')}</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel>{t('filters.version')}</InputLabel>
<Select
value={selectedVersion}
label={t('filters.version')}
onChange={(e) => setSelectedVersion(e.target.value as string)}
>
{versions.map(v => (
<MenuItem key={v.abbreviation} value={v.abbreviation}>
{v.name} ({v.abbreviation})
</MenuItem>
))}
</Select>
</FormControl>
<Accordion>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="body2">{t('filters.specificBooks')}</Typography>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ maxHeight: 200, overflow: 'auto' }}>
{(filters.testament === 'old' || filters.testament === 'all' ? oldTestamentBooks : [])
.concat(filters.testament === 'new' || filters.testament === 'all' ? newTestamentBooks : [])
.map((book) => (
<Chip
key={book.bookKey}
label={book.name}
size="small"
variant={filters.bookKeys.includes(book.bookKey) ? 'filled' : 'outlined'}
onClick={() => {
const exists = filters.bookKeys.includes(book.bookKey)
const newBookKeys = exists
? filters.bookKeys.filter(b => b !== book.bookKey)
: [...filters.bookKeys, book.bookKey]
setFilters({ ...filters, bookKeys: newBookKeys })
}}
sx={{ mb: 0.5, mr: 0.5 }}
/>
))}
</Box>
</AccordionDetails>
</Accordion>
</CardContent>
</Card>
{/* Search History */}
{searchHistory.length > 0 && (
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="h6" gutterBottom>
<History sx={{ mr: 1, verticalAlign: 'middle' }} />
{t('history.title')}
</Typography>
{searchHistory.slice(0, 5).map((query, index) => (
<Chip
key={index}
label={query}
size="small"
variant="outlined"
onClick={() => setSearchQuery(query)}
sx={{ mb: 0.5, mr: 0.5 }}
/>
))}
</CardContent>
</Card>
)}
{/* Popular Searches */}
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
{t('popular.title')}
</Typography>
{popularSearches.map((query, index) => (
<Chip
key={index}
label={query}
size="small"
variant="outlined"
onClick={() => setSearchQuery(query)}
sx={{ mb: 0.5, mr: 0.5 }}
/>
))}
</CardContent>
</Card>
</Grid>
{/* Main Search Area */}
<Grid item xs={12} md={9}>
{/* Search Input */}
<Card sx={{ mb: 3 }}>
<CardContent>
<Box sx={{ display: 'flex', gap: 2 }}>
<TextField
fullWidth
variant="outlined"
placeholder={t('input.placeholder')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyPress={handleKeyPress}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Search />
</InputAdornment>
),
endAdornment: searchQuery && (
<InputAdornment position="end">
<Button
size="small"
onClick={() => setSearchQuery('')}
>
<Close />
</Button>
</InputAdornment>
),
}}
/>
<Button
variant="contained"
onClick={handleSearch}
disabled={!searchQuery.trim() || loading}
sx={{ minWidth: 100 }}
>
{loading ? <CircularProgress size={20} color="inherit" /> : t('button.search')}
</Button>
</Box>
{filters.bookKeys.length > 0 && (
<Box sx={{ mt: 2 }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{t('searchIn', { books: filters.bookKeys.map(k => booksData.find(b => b.bookKey === k)?.name || k).join(', ') })}
</Typography>
</Box>
)}
</CardContent>
</Card>
{/* Search Results */}
{loading && searchQuery && (
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
{t('searching')}
</Typography>
<List>
{Array.from({ length: 3 }).map((_, index) => (
<ListItem key={index} divider>
<ListItemText
primary={
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Skeleton variant="text" width="40%" height={28} />
<Skeleton variant="rounded" width={100} height={24} />
</Box>
}
secondary={
<Box>
<Skeleton variant="text" width="100%" height={24} />
<Skeleton variant="text" width="90%" height={24} />
<Skeleton variant="text" width="95%" height={24} />
</Box>
}
/>
</ListItem>
))}
</List>
</CardContent>
</Card>
)}
{results.length > 0 && !loading && (
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
{t('results', { count: results.length })}
</Typography>
<List>
{results.map((result) => (
<ListItem key={result.id} divider>
<ListItemText
primary={
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Typography variant="subtitle1" color="primary">
{result.book} {result.chapter}:{result.verse}
</Typography>
<Chip
label={`${Math.round(result.relevance * 100)}% ${t('relevance')}`}
size="small"
color="primary"
variant="outlined"
/>
</Box>
}
secondary={
<Typography variant="body1" sx={{ lineHeight: 1.6, mt: 1 }}>
{highlightSearchTerm(result.text, searchQuery)}
</Typography>
}
/>
</ListItem>
))}
</List>
</CardContent>
</Card>
)}
{!loading && searchQuery && results.length === 0 && (
<Paper sx={{ p: 4, textAlign: 'center' }}>
<Typography variant="h6" color="text.secondary" gutterBottom>
{t('noResults.title')}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('noResults.description')}
</Typography>
</Paper>
)}
{!searchQuery && !loading && (
<Paper sx={{ p: 4, textAlign: 'center' }}>
<MenuBook sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" color="text.secondary" gutterBottom>
{t('empty.title')}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('empty.description')}
</Typography>
</Paper>
)}
</Grid>
</Grid>
</Container>
</Box>
)
}