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

989 lines
35 KiB
TypeScript

'use client'
import { useState, useEffect, useRef, useCallback } from 'react'
import { useTranslations, useLocale } from 'next-intl'
import { useAuth } from '@/hooks/use-auth'
import { useRouter, useSearchParams } from 'next/navigation'
import {
Box,
Container,
Typography,
TextField,
Button,
Paper,
Card,
CardContent,
Chip,
List,
ListItem,
ListItemButton,
ListItemText,
IconButton,
Tooltip,
FormControl,
InputLabel,
Select,
MenuItem,
Checkbox,
FormControlLabel,
Collapse,
Divider,
Alert,
Skeleton,
Badge,
ButtonGroup,
InputAdornment,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Fab,
useTheme,
useMediaQuery
} from '@mui/material'
import {
Search,
FilterList,
History,
Bookmark,
BookmarkBorder,
Share,
Launch,
ExpandMore,
ExpandLess,
Clear,
TuneRounded,
MenuBook,
Article,
ContentCopy,
KeyboardArrowUp,
AutoAwesome,
SearchOff
} from '@mui/icons-material'
interface SearchResult {
id: string
verseId: string
book: string
bookId: string
bookKey?: string
chapter: number
verse: number
text: string
relevance: number
context?: {
before?: string
after?: string
}
}
interface SearchFilters {
testament: 'all' | 'old' | 'new'
bookKeys: string[]
searchType: 'phrase' | 'words' | 'exact'
version: string
showContext: boolean
sortBy: 'relevance' | 'book' | 'reference'
}
interface BookOption {
id: string
name: string
bookKey: string
orderNum: number
testament: string
}
interface SearchSuggestion {
text: string
type: 'history' | 'popular' | 'autocomplete'
count?: number
}
export default function SearchPage() {
const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
const t = useTranslations('pages.search')
const locale = useLocale()
const router = useRouter()
const searchParams = useSearchParams()
const { user } = useAuth()
// Core search state
const [searchQuery, setSearchQuery] = useState('')
const [results, setResults] = useState<SearchResult[]>([])
const [loading, setLoading] = useState(false)
const [totalResults, setTotalResults] = useState(0)
const [currentPage, setCurrentPage] = useState(1)
const [shouldSearchFromUrl, setShouldSearchFromUrl] = useState(false)
// UI state
const [filtersOpen, setFiltersOpen] = useState(false)
const [showSuggestions, setShowSuggestions] = useState(false)
const [suggestions, setSuggestions] = useState<SearchSuggestion[]>([])
const [searchHistory, setSearchHistory] = useState<string[]>([])
const [savedSearches, setSavedSearches] = useState<string[]>([])
// Search configuration
const [filters, setFilters] = useState<SearchFilters>({
testament: 'all',
bookKeys: [],
searchType: 'words',
version: '',
showContext: true,
sortBy: 'relevance'
})
// Data
const [books, setBooks] = useState<BookOption[]>([])
const [versions, setVersions] = useState<Array<{ id: string; name: string; abbreviation: string; isDefault: boolean }>>([])
const [bookmarks, setBookmarks] = useState<{[key: string]: boolean}>({})
// Refs
const searchInputRef = useRef<HTMLInputElement>(null)
const resultsRef = useRef<HTMLDivElement>(null)
const oldTestamentBooks = books.filter(b => b.orderNum <= 39)
const newTestamentBooks = books.filter(b => b.orderNum > 39)
const popularSearches = [
{ text: t('popular.items.0'), type: 'popular' as const, count: 1250 },
{ text: t('popular.items.1'), type: 'popular' as const, count: 980 },
{ text: t('popular.items.2'), type: 'popular' as const, count: 875 },
{ text: t('popular.items.3'), type: 'popular' as const, count: 720 },
{ text: t('popular.items.4'), type: 'popular' as const, count: 680 },
{ text: t('popular.items.5'), type: 'popular' as const, count: 650 },
{ text: t('popular.items.6'), type: 'popular' as const, count: 580 },
{ text: t('popular.items.7'), type: 'popular' as const, count: 520 },
{ text: t('popular.items.8'), type: 'popular' as const, count: 480 },
{ text: t('popular.items.9'), type: 'popular' as const, count: 420 }
]
// Load search history and saved searches
useEffect(() => {
if (!user) return
const userKey = `searchHistory_${user.id}`
const history = localStorage.getItem(userKey)
const saved = localStorage.getItem('savedSearches')
if (history) {
setSearchHistory(JSON.parse(history))
}
if (saved) {
setSavedSearches(JSON.parse(saved))
}
}, [user])
// Load search parameters from URL (run only once on mount)
useEffect(() => {
const urlQuery = searchParams.get('q')
const urlTestament = searchParams.get('testament')
const urlSearchType = searchParams.get('type')
const urlBooks = searchParams.get('books')
if (urlQuery) {
setSearchQuery(urlQuery)
setShouldSearchFromUrl(true)
}
if (urlTestament || urlSearchType || urlBooks) {
setFilters(prev => ({
...prev,
...(urlTestament && { testament: urlTestament as any }),
...(urlSearchType && { searchType: urlSearchType as any }),
...(urlBooks && { bookKeys: urlBooks.split(',').filter(Boolean) })
}))
}
}, []) // Empty dependency array to run only once
// Load versions and books
useEffect(() => {
const loadVersions = async () => {
try {
const response = await fetch(`/api/bible/versions?locale=${locale}`)
const data = await response.json()
const versionList: Array<{ id: string; name: string; abbreviation: string; isDefault: boolean }> = data.versions || []
setVersions(versionList)
const defaultVersion = versionList.find(v => v.isDefault) || versionList[0]
if (defaultVersion) {
setFilters(prev => ({ ...prev, version: defaultVersion.abbreviation }))
}
} catch (error) {
console.error('Failed to load versions:', error)
}
}
loadVersions()
}, [locale])
useEffect(() => {
const loadBooks = async () => {
if (!filters.version) return
try {
const response = await fetch(`/api/bible/books?locale=${locale}&version=${filters.version}`)
const data = await response.json()
const bookList = (data.books || []).map((b: any) => ({
id: b.id,
name: b.name,
bookKey: b.bookKey,
orderNum: b.orderNum,
testament: b.testament
}))
setBooks(bookList)
} catch (error) {
console.error('Failed to load books:', error)
}
}
loadBooks()
}, [locale, filters.version])
// Update suggestions based on search query
useEffect(() => {
if (!searchQuery.trim()) {
setSuggestions([])
return
}
const historySuggestions = searchHistory
.filter(h => h.toLowerCase().includes(searchQuery.toLowerCase()))
.slice(0, 3)
.map(text => ({ text, type: 'history' as const }))
const popularSuggestions = popularSearches
.filter(p => p.text.toLowerCase().includes(searchQuery.toLowerCase()))
.slice(0, 3)
setSuggestions([...historySuggestions, ...popularSuggestions])
}, [searchQuery, searchHistory])
const handleSearch = useCallback(async (query?: string, page = 1) => {
const searchTerm = query || searchQuery
if (!searchTerm.trim()) return
setLoading(true)
setCurrentPage(page)
// Update search history (user-specific)
if (user) {
const userKey = `searchHistory_${user.id}`
const newHistory = [searchTerm, ...searchHistory.filter(s => s !== searchTerm)].slice(0, 20)
setSearchHistory(newHistory)
localStorage.setItem(userKey, JSON.stringify(newHistory))
}
try {
const params = new URLSearchParams({
q: searchTerm,
page: page.toString(),
limit: '20',
testament: filters.testament,
searchType: filters.searchType,
showContext: filters.showContext.toString(),
sortBy: filters.sortBy,
locale,
version: filters.version,
bookKeys: filters.bookKeys.join(',')
})
const response = await fetch(`/api/search/verses?${params}`)
if (!response.ok) {
throw new Error('Search failed')
}
const data = await response.json()
setResults(data.results || [])
setTotalResults(data.total || 0)
// Update URL with search parameters for sharing and tracking
const urlParams = new URLSearchParams({
q: searchTerm,
...(filters.testament !== 'all' && { testament: filters.testament }),
...(filters.searchType !== 'words' && { type: filters.searchType }),
...(filters.bookKeys.length > 0 && { books: filters.bookKeys.join(',') })
})
const newUrl = `${window.location.pathname}?${urlParams.toString()}`
window.history.replaceState({}, '', newUrl)
// Scroll to results on mobile
if (isMobile && resultsRef.current) {
resultsRef.current.scrollIntoView({ behavior: 'smooth' })
}
} catch (error) {
console.error('Search error:', error)
// Show demo results for development
setResults([
{
id: '1',
verseId: 'verse-1',
book: 'Ioan',
bookId: 'john',
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,
context: {
before: 'Și nimeni nu s-a suit în cer, afară de cel ce s-a coborât din cer, adică Fiul omului care este în cer.',
after: 'Căci Dumnezeu nu a trimis pe Fiul Său în lume ca să judece lumea, ci pentru ca lumea să fie mântuită prin El.'
}
},
{
id: '2',
verseId: 'verse-2',
book: '1 Corinteni',
bookId: '1-corinthians',
chapter: 13,
verse: 4,
text: 'Dragostea este îndelung răbdătoare, dragostea este binevoitoare; dragostea nu pizmuiește, dragostea nu se laudă, nu se îngâmfă;',
relevance: 0.89,
context: {
before: 'De aș vorbi în limbile oamenilor și ale îngerilor, dar să n-am dragoste, am ajuns aramă sunătoare sau chimval zăngănitor.',
after: 'nu lucrează cu necuviință, nu caută ale sale, nu se mânie, nu ține seama de rău;'
}
}
])
setTotalResults(2)
} finally {
setLoading(false)
setShowSuggestions(false)
}
}, [searchQuery, filters, searchHistory, locale, isMobile])
// Trigger search from URL after handleSearch is defined
useEffect(() => {
if (shouldSearchFromUrl && searchQuery) {
handleSearch(searchQuery, 1)
setShouldSearchFromUrl(false)
}
}, [shouldSearchFromUrl, searchQuery, handleSearch])
const handleVerseBookmark = useCallback(async (result: SearchResult) => {
if (!user) return
const token = localStorage.getItem('authToken')
if (!token) return
try {
const isBookmarked = bookmarks[result.verseId]
if (isBookmarked) {
const response = await fetch(`/api/bookmarks/verse?verseId=${result.verseId}&locale=${locale}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
})
if (response.ok) {
setBookmarks(prev => {
const updated = { ...prev }
delete updated[result.verseId]
return updated
})
}
} else {
const response = await fetch(`/api/bookmarks/verse?locale=${locale}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ verseId: result.verseId })
})
if (response.ok) {
setBookmarks(prev => ({ ...prev, [result.verseId]: true }))
}
}
} catch (error) {
console.error('Bookmark error:', error)
}
}, [user, bookmarks, locale])
const handleCopyVerse = useCallback((result: SearchResult) => {
const text = `${result.book} ${result.chapter}:${result.verse} - ${result.text}`
navigator.clipboard.writeText(text)
}, [])
const handleNavigateToVerse = useCallback((result: SearchResult) => {
const bookIdentifier = result.bookId || result.bookKey || result.book
router.push(`/${locale}/bible?book=${bookIdentifier}&chapter=${result.chapter}&verse=${result.verse}`)
}, [router, locale])
const clearFilters = useCallback(() => {
setFilters(prev => ({
...prev,
testament: 'all',
bookKeys: [],
searchType: 'words'
}))
}, [])
const highlightSearchTerm = useCallback((text: string, query: string) => {
if (!query.trim()) return text
const words = query.trim().split(/\s+/)
let highlightedText = text
words.forEach(word => {
const regex = new RegExp(`(${word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi')
highlightedText = highlightedText.replace(regex, '<mark>$1</mark>')
})
return <span dangerouslySetInnerHTML={{ __html: highlightedText }} />
}, [])
const activeFiltersCount = (filters.testament !== 'all' ? 1 : 0) +
filters.bookKeys.length +
(filters.searchType !== 'words' ? 1 : 0)
return (
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default' }}>
<Box sx={{ px: { xs: 2, md: 4 }, py: { xs: 2, md: 4 } }}>
{/* Header */}
<Box sx={{ textAlign: 'center', mb: 4 }}>
<Typography
variant="h3"
component="h1"
gutterBottom
sx={{
fontWeight: 'bold',
background: 'linear-gradient(45deg, #1976d2 30%, #42a5f5 90%)',
backgroundClip: 'text',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 2
}}
>
<Search sx={{ fontSize: 40, color: 'primary.main' }} />
{t('title')}
</Typography>
<Typography variant="h6" color="text.secondary" sx={{ mb: 3 }}>
{t('subtitle')}
</Typography>
</Box>
{/* Search Bar */}
<Paper
elevation={3}
sx={{
p: 3,
mb: 4,
borderRadius: 3,
position: 'relative'
}}
>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-start' }}>
<Box sx={{ flex: 1, position: 'relative' }}>
<TextField
ref={searchInputRef}
fullWidth
variant="outlined"
placeholder={t('input.placeholder')}
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value)
setShowSuggestions(true)
}}
onFocus={() => setShowSuggestions(true)}
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Search color="primary" />
</InputAdornment>
),
endAdornment: searchQuery && (
<InputAdornment position="end">
<IconButton
size="small"
onClick={() => {
setSearchQuery('')
setShowSuggestions(false)
}}
>
<Clear />
</IconButton>
</InputAdornment>
),
sx: {
fontSize: '1.1rem',
'& .MuiOutlinedInput-root': {
borderRadius: 2
}
}
}}
/>
{/* Search Suggestions */}
<Collapse in={showSuggestions && suggestions.length > 0}>
<Paper
elevation={8}
sx={{
position: 'absolute',
top: '100%',
left: 0,
right: 0,
zIndex: 1000,
mt: 1,
borderRadius: 2
}}
>
<List dense>
{suggestions.map((suggestion, index) => (
<ListItem key={index} disablePadding>
<ListItemButton
onClick={() => {
setSearchQuery(suggestion.text)
handleSearch(suggestion.text)
}}
>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{suggestion.type === 'history' && <History fontSize="small" color="action" />}
{suggestion.type === 'popular' && <AutoAwesome fontSize="small" color="primary" />}
<Typography variant="body2">
{suggestion.text}
</Typography>
{suggestion.count && (
<Chip
label={suggestion.count}
size="small"
variant="outlined"
sx={{ ml: 'auto' }}
/>
)}
</Box>
}
/>
</ListItemButton>
</ListItem>
))}
</List>
</Paper>
</Collapse>
</Box>
<Button
variant="contained"
size="large"
onClick={() => handleSearch()}
disabled={!searchQuery.trim() || loading}
sx={{
minWidth: 120,
height: 56,
borderRadius: 2,
textTransform: 'none',
fontSize: '1rem'
}}
>
{loading ? t('searching') : t('button.search')}
</Button>
<Button
variant="outlined"
size="large"
onClick={() => setFiltersOpen(!filtersOpen)}
startIcon={<TuneRounded />}
sx={{
height: 56,
borderRadius: 2,
textTransform: 'none'
}}
>
<Badge badgeContent={activeFiltersCount} color="primary">
{t('filters.title')}
</Badge>
</Button>
</Box>
{/* Quick Filters */}
<Box sx={{ mt: 2, display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<ButtonGroup variant="outlined" size="small">
<Button
variant={filters.searchType === 'words' ? 'contained' : 'outlined'}
onClick={() => setFilters(prev => ({ ...prev, searchType: 'words' }))}
>
{t('searchTypes.anyWords')}
</Button>
<Button
variant={filters.searchType === 'phrase' ? 'contained' : 'outlined'}
onClick={() => setFilters(prev => ({ ...prev, searchType: 'phrase' }))}
>
{t('searchTypes.exactPhrase')}
</Button>
</ButtonGroup>
<ButtonGroup variant="outlined" size="small">
<Button
variant={filters.testament === 'all' ? 'contained' : 'outlined'}
onClick={() => setFilters(prev => ({ ...prev, testament: 'all' }))}
>
{t('filters.options.all')}
</Button>
<Button
variant={filters.testament === 'old' ? 'contained' : 'outlined'}
onClick={() => setFilters(prev => ({ ...prev, testament: 'old' }))}
>
{t('filters.options.old')}
</Button>
<Button
variant={filters.testament === 'new' ? 'contained' : 'outlined'}
onClick={() => setFilters(prev => ({ ...prev, testament: 'new' }))}
>
{t('filters.options.new')}
</Button>
</ButtonGroup>
{activeFiltersCount > 0 && (
<Button size="small" onClick={clearFilters} startIcon={<Clear />}>
{t('filters.clear')}
</Button>
)}
</Box>
</Paper>
{/* Advanced Filters */}
<Collapse in={filtersOpen}>
<Paper elevation={2} sx={{ p: 3, mb: 4, borderRadius: 2 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{/* First row of filters */}
<Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
<Box sx={{ flex: { xs: '1 1 100%', sm: '1 1 45%', md: '1 1 auto' }, minWidth: { md: 200 } }}>
<FormControl fullWidth size="small">
<InputLabel>{t('filters.version')}</InputLabel>
<Select
value={filters.version}
label={t('filters.version')}
onChange={(e) => setFilters(prev => ({ ...prev, version: e.target.value }))}
>
{versions.map(v => (
<MenuItem key={v.abbreviation} value={v.abbreviation}>
{v.name}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
<Box sx={{ flex: { xs: '1 1 100%', sm: '1 1 45%', md: '1 1 auto' }, minWidth: { md: 200 } }}>
<FormControl fullWidth size="small">
<InputLabel>Sort by</InputLabel>
<Select
value={filters.sortBy}
label="Sort by"
onChange={(e) => setFilters(prev => ({ ...prev, sortBy: e.target.value as any }))}
>
<MenuItem value="relevance">Relevance</MenuItem>
<MenuItem value="book">Book order</MenuItem>
<MenuItem value="reference">Reference</MenuItem>
</Select>
</FormControl>
</Box>
<Box sx={{ flex: { xs: '1 1 100%', sm: '1 1 45%', md: '1 1 auto' }, display: 'flex', alignItems: 'center' }}>
<FormControlLabel
control={
<Checkbox
checked={filters.showContext}
onChange={(e) => setFilters(prev => ({ ...prev, showContext: e.target.checked }))}
/>
}
label="Show context"
/>
</Box>
</Box>
{/* Popular searches */}
<Box>
<Typography variant="subtitle2" gutterBottom>
{t('popular.title')}
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 3 }}>
{popularSearches.slice(0, 10).map((search, index) => (
<Chip
key={index}
label={search.text}
size="small"
variant="outlined"
onClick={() => {
setSearchQuery(search.text)
handleSearch(search.text)
setFiltersOpen(false)
}}
/>
))}
</Box>
</Box>
{/* Book selection */}
<Box>
<Typography variant="subtitle2" gutterBottom>
{t('filters.specificBooks')}
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, maxHeight: 200, overflow: 'auto' }}>
{books.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(k => k !== book.bookKey)
: [...filters.bookKeys, book.bookKey]
setFilters(prev => ({ ...prev, bookKeys: newBookKeys }))
}}
color={filters.bookKeys.includes(book.bookKey) ? 'primary' : 'default'}
/>
))}
</Box>
</Box>
</Box>
</Paper>
</Collapse>
<Box sx={{ display: 'flex', gap: 4, flexDirection: { xs: 'column', md: 'row' } }}>
{/* Sidebar */}
<Box sx={{ width: { xs: '100%', md: '25%' }, flexShrink: 0 }}>
{/* Search History */}
{searchHistory.length > 0 && (
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<History />
{t('history.title')}
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{searchHistory.slice(0, 8).map((query, index) => (
<Chip
key={index}
label={query}
size="small"
variant="outlined"
onClick={() => {
setSearchQuery(query)
handleSearch(query)
}}
/>
))}
</Box>
</CardContent>
</Card>
)}
</Box>
{/* Main Results */}
<Box sx={{ flex: 1, width: { xs: '100%', md: '75%' } }}>
<Box ref={resultsRef}>
{/* Results Header */}
{(results.length > 0 || loading) && (
<Box sx={{ mb: 3, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="h6">
{loading ? t('searching') : t('results', { count: totalResults })}
</Typography>
{results.length > 0 && (
<Typography variant="body2" color="text.secondary">
Page {currentPage}
</Typography>
)}
</Box>
)}
{/* Loading State */}
{loading && (
<Box>
{Array.from({ length: 5 }).map((_, index) => (
<Card key={index} sx={{ mb: 2 }}>
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<Skeleton variant="text" width="30%" height={28} />
<Skeleton variant="rectangular" width={80} height={24} />
</Box>
<Skeleton variant="text" width="100%" height={24} />
<Skeleton variant="text" width="90%" height={24} />
<Skeleton variant="text" width="95%" height={24} />
</CardContent>
</Card>
))}
</Box>
)}
{/* Search Results */}
{results.length > 0 && !loading && (
<Box>
{results.map((result) => (
<Card key={result.id} sx={{ mb: 2, transition: 'all 0.2s', '&:hover': { elevation: 4 } }}>
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
<Typography
variant="h6"
color="primary"
sx={{ cursor: 'pointer' }}
onClick={() => handleNavigateToVerse(result)}
>
{result.book} {result.chapter}:{result.verse}
</Typography>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<Chip
label={`${Math.round(result.relevance * 100)}%`}
size="small"
color="primary"
variant="outlined"
/>
{user && (
<Tooltip title={bookmarks[result.verseId] ? t('removeBookmark') : t('addBookmark')}>
<IconButton
size="small"
onClick={() => handleVerseBookmark(result)}
color={bookmarks[result.verseId] ? 'warning' : 'default'}
>
{bookmarks[result.verseId] ? <Bookmark /> : <BookmarkBorder />}
</IconButton>
</Tooltip>
)}
<Tooltip title={t('copyVerse')}>
<IconButton
size="small"
onClick={() => handleCopyVerse(result)}
>
<ContentCopy />
</IconButton>
</Tooltip>
<Tooltip title={t('goTo')}>
<IconButton
size="small"
onClick={() => handleNavigateToVerse(result)}
>
<Launch />
</IconButton>
</Tooltip>
</Box>
</Box>
{filters.showContext && result.context?.before && (
<Typography
variant="body2"
color="text.secondary"
sx={{ fontStyle: 'italic', mb: 1 }}
>
...{result.context.before}
</Typography>
)}
<Typography
variant="body1"
sx={{
lineHeight: 1.8,
fontSize: '1.1rem',
mb: filters.showContext && result.context?.after ? 1 : 0,
'& mark': {
backgroundColor: 'rgba(25, 118, 210, 0.2)',
padding: '2px 4px',
borderRadius: '4px',
fontWeight: 'bold'
}
}}
>
{highlightSearchTerm(result.text, searchQuery)}
</Typography>
{filters.showContext && result.context?.after && (
<Typography
variant="body2"
color="text.secondary"
sx={{ fontStyle: 'italic' }}
>
{result.context.after}...
</Typography>
)}
</CardContent>
</Card>
))}
{/* Load More / Pagination */}
{totalResults > results.length && (
<Box sx={{ textAlign: 'center', mt: 4 }}>
<Button
variant="outlined"
size="large"
onClick={() => handleSearch(searchQuery, currentPage + 1)}
disabled={loading}
>
Load more results
</Button>
</Box>
)}
</Box>
)}
{/* No Results */}
{!loading && searchQuery && results.length === 0 && (
<Paper sx={{ p: 6, textAlign: 'center' }}>
<SearchOff sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
<Typography variant="h5" color="text.secondary" gutterBottom>
{t('noResults.title')}
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
{t('noResults.description')}
</Typography>
<Button variant="outlined" onClick={clearFilters}>
Try clearing filters
</Button>
</Paper>
)}
{/* Empty State */}
{!searchQuery && !loading && (
<Paper sx={{ p: 6, textAlign: 'center' }}>
<MenuBook sx={{ fontSize: 80, color: 'text.secondary', mb: 3 }} />
<Typography variant="h4" color="text.secondary" gutterBottom>
{t('empty.title')}
</Typography>
<Typography variant="h6" color="text.secondary" sx={{ mb: 4 }}>
{t('empty.description')}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{t('filters.title')} {t('popular.title')}
</Typography>
</Paper>
)}
</Box>
</Box>
</Box>
{/* Scroll to Top */}
{results.length > 5 && (
<Fab
color="primary"
size="small"
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
sx={{
position: 'fixed',
bottom: 16,
right: 16
}}
>
<KeyboardArrowUp />
</Fab>
)}
</Box>
</Box>
)
}