Files
biblical-guide.com/app/[locale]/bible/reader.tsx

1028 lines
30 KiB
TypeScript

'use client'
import { useState, useEffect, useRef, useCallback } from 'react'
import { useTranslations, useLocale } from 'next-intl'
import { useAuth } from '@/hooks/use-auth'
import { useSearchParams } from 'next/navigation'
import {
Box,
Typography,
IconButton,
Drawer,
List,
ListItem,
ListItemButton,
ListItemText,
AppBar,
Toolbar,
Button,
Menu,
MenuItem,
Slider,
Switch,
FormControlLabel,
Tooltip,
Fab,
Paper,
Divider,
Chip,
ButtonGroup,
useTheme,
useMediaQuery,
Collapse,
ListItemIcon,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
Snackbar,
Alert,
Backdrop,
CircularProgress,
Grid,
FormControl,
InputLabel,
Select,
Container
} from '@mui/material'
import {
Menu as MenuIcon,
Settings,
Bookmark,
BookmarkBorder,
Share,
ContentCopy,
ArrowBack,
ArrowForward,
FullscreenExit,
Fullscreen,
KeyboardArrowUp,
KeyboardArrowDown,
FormatSize,
Palette,
Note,
Close,
ExpandLess,
ExpandMore,
MenuBook,
Visibility,
Speed
} from '@mui/icons-material'
interface BibleVerse {
id: string
verseNum: number
text: string
}
interface BibleChapter {
id: string
chapterNum: number
verses: BibleVerse[]
}
interface BibleBook {
id: string
versionId: string
name: string
testament: string
orderNum: number
bookKey: string
chapters: BibleChapter[]
}
interface ReadingPreferences {
fontSize: number
lineHeight: number
fontFamily: string
theme: 'light' | 'dark' | 'sepia'
showVerseNumbers: boolean
columnLayout: boolean
readingMode: boolean
}
const defaultPreferences: ReadingPreferences = {
fontSize: 18,
lineHeight: 1.6,
fontFamily: 'serif',
theme: 'light',
showVerseNumbers: true,
columnLayout: false,
readingMode: false
}
export default function BibleReaderNew() {
const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
const t = useTranslations('pages.bible')
const locale = useLocale()
const searchParams = useSearchParams()
const { user } = useAuth()
// Core state
const [books, setBooks] = useState<BibleBook[]>([])
const [selectedBook, setSelectedBook] = useState<string>('')
const [selectedChapter, setSelectedChapter] = useState<number>(1)
const [verses, setVerses] = useState<BibleVerse[]>([])
const [loading, setLoading] = useState(true)
// UI state
const [settingsOpen, setSettingsOpen] = useState(false)
const [preferences, setPreferences] = useState<ReadingPreferences>(defaultPreferences)
const [highlightedVerse, setHighlightedVerse] = useState<number | null>(null)
const [showScrollTop, setShowScrollTop] = useState(false)
// Bookmark state
const [isChapterBookmarked, setIsChapterBookmarked] = useState(false)
const [verseBookmarks, setVerseBookmarks] = useState<{[key: string]: any}>({})
const [bookmarkLoading, setBookmarkLoading] = useState(false)
// Note dialog state
const [noteDialog, setNoteDialog] = useState<{
open: boolean
verse?: BibleVerse
note: string
}>({
open: false,
note: ''
})
// Copy feedback
const [copyFeedback, setCopyFeedback] = useState<{
open: boolean
message: string
}>({
open: false,
message: ''
})
// Refs
const contentRef = useRef<HTMLDivElement>(null)
const verseRefs = useRef<{[key: number]: HTMLDivElement}>({})
// Load user preferences from localStorage
useEffect(() => {
const savedPrefs = localStorage.getItem('bibleReaderPreferences')
if (savedPrefs) {
try {
const parsed = JSON.parse(savedPrefs)
setPreferences({ ...defaultPreferences, ...parsed })
} catch (e) {
console.error('Failed to parse preferences:', e)
}
}
}, [])
// Save preferences to localStorage
useEffect(() => {
localStorage.setItem('bibleReaderPreferences', JSON.stringify(preferences))
}, [preferences])
// Scroll handler for show scroll to top button
useEffect(() => {
const handleScroll = () => {
setShowScrollTop(window.scrollY > 300)
}
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])
// Fetch books
useEffect(() => {
fetch(`/api/bible/books?locale=${locale}`)
.then(res => res.json())
.then(data => {
setBooks(data.books || [])
if (data.books && data.books.length > 0) {
setSelectedBook(data.books[0].id)
}
setLoading(false)
})
.catch(err => {
console.error('Error fetching books:', err)
setLoading(false)
})
}, [locale])
// Handle URL parameters
useEffect(() => {
if (books.length > 0) {
const bookParam = searchParams.get('book')
const chapterParam = searchParams.get('chapter')
const verseParam = searchParams.get('verse')
if (bookParam) {
const book = books.find(b => b.id === bookParam) || books.find(b => b.bookKey === bookParam)
if (book) {
setSelectedBook(book.id)
if (chapterParam) {
const chapter = parseInt(chapterParam)
if (chapter > 0) {
setSelectedChapter(chapter)
}
}
if (verseParam) {
const verse = parseInt(verseParam)
if (verse > 0) {
setHighlightedVerse(verse)
setTimeout(() => {
scrollToVerse(verse)
setTimeout(() => setHighlightedVerse(null), 3000)
}, 500)
}
}
}
}
}
}, [books, searchParams])
// Fetch verses when book/chapter changes
useEffect(() => {
if (selectedBook && selectedChapter) {
setLoading(true)
fetch(`/api/bible/verses?bookId=${selectedBook}&chapter=${selectedChapter}`)
.then(res => res.json())
.then(data => {
setVerses(data.verses || [])
setLoading(false)
})
.catch(err => {
console.error('Error fetching verses:', err)
setLoading(false)
})
}
}, [selectedBook, selectedChapter])
// Check chapter bookmark status
useEffect(() => {
if (selectedBook && selectedChapter && user) {
const token = localStorage.getItem('authToken')
if (token) {
fetch(`/api/bookmarks/chapter/check?bookId=${selectedBook}&chapterNum=${selectedChapter}&locale=${locale}`, {
headers: { 'Authorization': `Bearer ${token}` }
})
.then(res => res.json())
.then(data => setIsChapterBookmarked(data.isBookmarked || false))
.catch(err => console.error('Error checking bookmark:', err))
}
} else {
setIsChapterBookmarked(false)
}
}, [selectedBook, selectedChapter, user, locale])
// Check verse bookmarks
useEffect(() => {
if (verses.length > 0 && user) {
const token = localStorage.getItem('authToken')
if (token) {
const verseIds = verses.map(verse => verse.id)
fetch(`/api/bookmarks/verse/bulk-check?locale=${locale}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ verseIds })
})
.then(res => res.json())
.then(data => setVerseBookmarks(data.bookmarks || {}))
.catch(err => console.error('Error checking verse bookmarks:', err))
}
} else {
setVerseBookmarks({})
}
}, [verses, user, locale])
// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Don't trigger shortcuts if user is typing in an input field
const activeElement = document.activeElement as HTMLElement
if (activeElement && (
activeElement.tagName === 'INPUT' ||
activeElement.tagName === 'TEXTAREA' ||
activeElement.isContentEditable ||
activeElement.closest('[role="dialog"]') // Don't trigger if a dialog/modal is open
)) {
return
}
// Check if the floating chat is open
const floatingChat = document.querySelector('[data-floating-chat="true"]')
if (floatingChat) {
return
}
if (e.ctrlKey || e.metaKey) return
switch (e.key) {
case 'ArrowLeft':
e.preventDefault()
handlePreviousChapter()
break
case 'ArrowRight':
e.preventDefault()
handleNextChapter()
break
case 's':
e.preventDefault()
setSettingsOpen(prev => !prev)
break
case 'r':
e.preventDefault()
setPreferences(prev => ({ ...prev, readingMode: !prev.readingMode }))
break
case 'Escape':
e.preventDefault()
if (preferences.readingMode) {
setPreferences(prev => ({ ...prev, readingMode: false }))
}
break
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [selectedBook, selectedChapter, books, preferences.readingMode])
const currentBook = books.find(book => book.id === selectedBook)
const maxChapters = currentBook?.chapters?.length || 1
const updateUrl = (bookId: string, chapter: number) => {
const url = new URL(window.location.href)
url.searchParams.set('book', bookId)
url.searchParams.set('chapter', chapter.toString())
window.history.replaceState({}, '', url.toString())
}
const scrollToVerse = (verseNum: number) => {
const verseElement = verseRefs.current[verseNum]
if (verseElement) {
verseElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
}
const handlePreviousChapter = () => {
if (selectedChapter > 1) {
const newChapter = selectedChapter - 1
setSelectedChapter(newChapter)
updateUrl(selectedBook, newChapter)
} else {
const currentBookIndex = books.findIndex(book => book.id === selectedBook)
if (currentBookIndex > 0) {
const previousBook = books[currentBookIndex - 1]
const lastChapter = previousBook.chapters?.length || 1
setSelectedBook(previousBook.id)
setSelectedChapter(lastChapter)
updateUrl(previousBook.id, lastChapter)
}
}
}
const handleNextChapter = () => {
if (selectedChapter < maxChapters) {
const newChapter = selectedChapter + 1
setSelectedChapter(newChapter)
updateUrl(selectedBook, newChapter)
} else {
const currentBookIndex = books.findIndex(book => book.id === selectedBook)
if (currentBookIndex < books.length - 1) {
const nextBook = books[currentBookIndex + 1]
setSelectedBook(nextBook.id)
setSelectedChapter(1)
updateUrl(nextBook.id, 1)
}
}
}
const handleChapterBookmark = async () => {
if (!user || !selectedBook || !selectedChapter) return
setBookmarkLoading(true)
const token = localStorage.getItem('authToken')
if (!token) {
setBookmarkLoading(false)
return
}
try {
if (isChapterBookmarked) {
const response = await fetch(`/api/bookmarks/chapter?bookId=${selectedBook}&chapterNum=${selectedChapter}&locale=${locale}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
})
if (response.ok) {
setIsChapterBookmarked(false)
}
} else {
const response = await fetch(`/api/bookmarks/chapter?locale=${locale}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
bookId: selectedBook,
chapterNum: selectedChapter
})
})
if (response.ok) {
setIsChapterBookmarked(true)
}
}
} catch (error) {
console.error('Error toggling bookmark:', error)
} finally {
setBookmarkLoading(false)
}
}
const handleVerseBookmark = async (verse: BibleVerse) => {
if (!user) return
const token = localStorage.getItem('authToken')
if (!token) return
try {
const isCurrentlyBookmarked = !!verseBookmarks[verse.id]
if (isCurrentlyBookmarked) {
const response = await fetch(`/api/bookmarks/verse?verseId=${verse.id}&locale=${locale}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
})
if (response.ok) {
setVerseBookmarks(prev => {
const newBookmarks = { ...prev }
delete newBookmarks[verse.id]
return newBookmarks
})
}
} else {
const response = await fetch(`/api/bookmarks/verse?locale=${locale}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ verseId: verse.id })
})
if (response.ok) {
const data = await response.json()
setVerseBookmarks(prev => ({
...prev,
[verse.id]: data.bookmark
}))
}
}
} catch (error) {
console.error('Error toggling verse bookmark:', error)
}
}
const handleCopyVerse = (verse: BibleVerse) => {
const text = `${currentBook?.name} ${selectedChapter}:${verse.verseNum} - ${verse.text}`
navigator.clipboard.writeText(text).then(() => {
setCopyFeedback({
open: true,
message: t('verseCopied')
})
})
}
const handleShare = () => {
const url = `${window.location.origin}/${locale}/bible?book=${selectedBook}&chapter=${selectedChapter}`
navigator.clipboard.writeText(url).then(() => {
setCopyFeedback({
open: true,
message: t('linkCopied')
})
})
}
const getThemeStyles = () => {
switch (preferences.theme) {
case 'dark':
return {
backgroundColor: '#1a1a1a',
color: '#e0e0e0',
borderColor: '#333'
}
case 'sepia':
return {
backgroundColor: '#f7f3e9',
color: '#5c4b3a',
borderColor: '#d4c5a0'
}
default:
return {
backgroundColor: '#ffffff',
color: '#000000',
borderColor: '#e0e0e0'
}
}
}
const renderVerse = (verse: BibleVerse) => {
const isBookmarked = !!verseBookmarks[verse.id]
const isHighlighted = highlightedVerse === verse.verseNum
return (
<Box
key={verse.id}
ref={(el: HTMLDivElement | null) => { if (el) verseRefs.current[verse.verseNum] = el }}
sx={{
mb: 1,
display: 'flex',
alignItems: 'flex-start',
gap: 1,
'&:hover .verse-actions': {
opacity: 1
}
}}
>
<Box sx={{ flex: 1 }}>
<Typography
component="span"
sx={{
fontSize: `${preferences.fontSize}px`,
lineHeight: preferences.lineHeight,
fontFamily: preferences.fontFamily === 'serif' ? 'Georgia, serif' : 'Arial, sans-serif',
display: 'inline',
backgroundColor: isHighlighted
? 'primary.light'
: isBookmarked
? 'warning.light'
: 'transparent',
borderRadius: (isBookmarked || isHighlighted) ? 1 : 0,
padding: (isBookmarked || isHighlighted) ? '4px 8px' : 0,
transition: 'all 0.3s ease',
border: isHighlighted ? '2px solid' : 'none',
borderColor: 'primary.main',
}}
>
{preferences.showVerseNumbers && (
<Typography
component="span"
sx={{
fontWeight: 'bold',
color: 'primary.main',
mr: 1,
fontSize: '0.9em',
userSelect: 'none'
}}
>
{verse.verseNum}
</Typography>
)}
{verse.text}
</Typography>
</Box>
{user && !preferences.readingMode && (
<Box className="verse-actions" sx={{ opacity: 0.3, transition: 'opacity 0.2s', display: 'flex', gap: 0.5 }}>
<IconButton
size="small"
onClick={() => handleVerseBookmark(verse)}
sx={{ color: isBookmarked ? 'warning.main' : 'action.active' }}
>
{isBookmarked ? <Bookmark fontSize="small" /> : <BookmarkBorder fontSize="small" />}
</IconButton>
<IconButton
size="small"
onClick={() => handleCopyVerse(verse)}
sx={{ color: 'action.active' }}
>
<ContentCopy fontSize="small" />
</IconButton>
</Box>
)}
</Box>
)
}
const renderNavigation = () => (
<Paper
elevation={1}
sx={{
mb: 2,
p: 2,
...getThemeStyles(),
border: `1px solid ${getThemeStyles().borderColor}`
}}
>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'center', justifyContent: 'center' }}>
{/* Books Selection */}
<Box sx={{ flex: { xs: '1 1 100%', sm: '1 1 auto' }, minWidth: { sm: 200, md: 250 } }}>
<FormControl fullWidth size="small">
<InputLabel>{t('book')}</InputLabel>
<Select
value={selectedBook}
label={t('book')}
onChange={(e) => {
setSelectedBook(e.target.value)
setSelectedChapter(1)
updateUrl(e.target.value, 1)
}}
>
{books.map((book) => (
<MenuItem key={book.id} value={book.id}>
{book.name}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
{/* Chapter Selection */}
<Box sx={{ flex: { xs: '1 1 100%', sm: '0 1 auto' }, minWidth: { sm: 120, md: 150 } }}>
<FormControl fullWidth size="small">
<InputLabel>{t('chapter')}</InputLabel>
<Select
value={selectedChapter}
label={t('chapter')}
onChange={(e) => {
const newChapter = Number(e.target.value)
setSelectedChapter(newChapter)
updateUrl(selectedBook, newChapter)
}}
MenuProps={{
PaperProps: {
style: {
maxHeight: 400,
},
},
}}
>
{Array.from({ length: maxChapters }, (_, i) => (
<MenuItem key={i + 1} value={i + 1}>
{t('chapter')} {i + 1}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
{/* Font Size Controls */}
<Box sx={{ flex: { xs: '1 1 100%', sm: '0 1 auto' }, minWidth: { sm: 150 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<IconButton
size="small"
onClick={() => setPreferences(prev => ({
...prev,
fontSize: Math.max(12, prev.fontSize - 1)
}))}
disabled={preferences.fontSize <= 12}
>
<Typography variant="h6">A</Typography>
</IconButton>
<IconButton
size="small"
onClick={() => setPreferences(prev => ({
...prev,
fontSize: Math.min(28, prev.fontSize + 1)
}))}
disabled={preferences.fontSize >= 28}
>
<Typography variant="h6">A</Typography>
</IconButton>
</Box>
</Box>
{/* Action Buttons */}
<Box sx={{ flex: { xs: '1 1 100%', sm: '1 1 auto' } }}>
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'center' }}>
<Tooltip title={t('toggleFullscreen')}>
<IconButton
size="small"
onClick={() => setPreferences(prev => ({ ...prev, readingMode: !prev.readingMode }))}
sx={{ color: preferences.readingMode ? 'primary.main' : 'inherit' }}
>
{preferences.readingMode ? <FullscreenExit /> : <Fullscreen />}
</IconButton>
</Tooltip>
<Tooltip title={t('settings')}>
<IconButton size="small" onClick={() => setSettingsOpen(true)}>
<Settings />
</IconButton>
</Tooltip>
{user && (
<Tooltip title={isChapterBookmarked ? t('removeBookmark') : t('addBookmark')}>
<IconButton
size="small"
onClick={handleChapterBookmark}
disabled={bookmarkLoading}
sx={{ color: isChapterBookmarked ? 'warning.main' : 'inherit' }}
>
{isChapterBookmarked ? <Bookmark /> : <BookmarkBorder />}
</IconButton>
</Tooltip>
)}
<Tooltip title={t('share')}>
<IconButton size="small" onClick={handleShare}>
<Share />
</IconButton>
</Tooltip>
</Box>
</Box>
</Box>
</Paper>
)
const renderSettings = () => (
<Dialog
open={settingsOpen}
onClose={() => setSettingsOpen(false)}
maxWidth="sm"
fullWidth
>
<DialogTitle>
{t('readingSettings')}
</DialogTitle>
<DialogContent>
<Box sx={{ py: 2, display: 'flex', flexDirection: 'column', gap: 3 }}>
<Box>
<Typography gutterBottom>{t('fontSize')}</Typography>
<Slider
value={preferences.fontSize}
onChange={(_, value) => setPreferences(prev => ({ ...prev, fontSize: value as number }))}
min={12}
max={24}
marks
valueLabelDisplay="auto"
/>
</Box>
<Box>
<Typography gutterBottom>{t('lineHeight')}</Typography>
<Slider
value={preferences.lineHeight}
onChange={(_, value) => setPreferences(prev => ({ ...prev, lineHeight: value as number }))}
min={1.2}
max={2.0}
step={0.1}
marks
valueLabelDisplay="auto"
/>
</Box>
<Box>
<Typography gutterBottom>{t('fontFamily')}</Typography>
<ButtonGroup fullWidth>
<Button
variant={preferences.fontFamily === 'serif' ? 'contained' : 'outlined'}
onClick={() => setPreferences(prev => ({ ...prev, fontFamily: 'serif' }))}
>
Serif
</Button>
<Button
variant={preferences.fontFamily === 'sans-serif' ? 'contained' : 'outlined'}
onClick={() => setPreferences(prev => ({ ...prev, fontFamily: 'sans-serif' }))}
>
Sans-serif
</Button>
</ButtonGroup>
</Box>
<Box>
<Typography gutterBottom>{t('theme')}</Typography>
<ButtonGroup fullWidth>
<Button
variant={preferences.theme === 'light' ? 'contained' : 'outlined'}
onClick={() => setPreferences(prev => ({ ...prev, theme: 'light' }))}
>
{t('light')}
</Button>
<Button
variant={preferences.theme === 'dark' ? 'contained' : 'outlined'}
onClick={() => setPreferences(prev => ({ ...prev, theme: 'dark' }))}
>
{t('dark')}
</Button>
<Button
variant={preferences.theme === 'sepia' ? 'contained' : 'outlined'}
onClick={() => setPreferences(prev => ({ ...prev, theme: 'sepia' }))}
>
{t('sepia')}
</Button>
</ButtonGroup>
</Box>
<FormControlLabel
control={
<Switch
checked={preferences.showVerseNumbers}
onChange={(e) => setPreferences(prev => ({ ...prev, showVerseNumbers: e.target.checked }))}
/>
}
label={t('showVerseNumbers')}
/>
<FormControlLabel
control={
<Switch
checked={preferences.readingMode}
onChange={(e) => setPreferences(prev => ({ ...prev, readingMode: e.target.checked }))}
/>
}
label={t('readingMode')}
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setSettingsOpen(false)}>
{t('close')}
</Button>
</DialogActions>
</Dialog>
)
if (loading && books.length === 0) {
return (
<Backdrop open>
<CircularProgress color="inherit" />
</Backdrop>
)
}
return (
<Box
sx={{
minHeight: '100vh',
...getThemeStyles()
}}
>
{/* Top Toolbar - Simplified */}
{!preferences.readingMode && (
<AppBar position="static" sx={{ ...getThemeStyles(), boxShadow: 1 }}>
<Toolbar variant="dense">
<Typography variant="h6" sx={{ flexGrow: 1 }}>
{currentBook?.name} {selectedChapter}
</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Tooltip title={t('previousChapter')}>
<IconButton
onClick={handlePreviousChapter}
disabled={selectedBook === books[0]?.id && selectedChapter === 1}
size="small"
>
<ArrowBack />
</IconButton>
</Tooltip>
<Tooltip title={t('nextChapter')}>
<IconButton
onClick={handleNextChapter}
disabled={selectedBook === books[books.length - 1]?.id && selectedChapter === maxChapters}
size="small"
>
<ArrowForward />
</IconButton>
</Tooltip>
</Box>
</Toolbar>
</AppBar>
)}
{/* Main Content */}
<Container
maxWidth="lg"
sx={{
py: preferences.readingMode ? 0 : 3,
px: preferences.readingMode ? 0 : 3
}}
>
{/* Navigation Section */}
{!preferences.readingMode && renderNavigation()}
{/* Reading Content */}
<Box
ref={contentRef}
sx={{
maxWidth: preferences.columnLayout ? 'none' : '800px',
mx: 'auto',
width: '100%'
}}
>
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
<CircularProgress />
</Box>
) : (
<Paper
elevation={preferences.readingMode ? 0 : 1}
sx={{
...getThemeStyles(),
borderRadius: preferences.readingMode ? 0 : 2,
p: preferences.readingMode ? 4 : 3,
minHeight: preferences.readingMode ? '100vh' : 'auto',
border: preferences.readingMode ? 'none' : `1px solid ${getThemeStyles().borderColor}`
}}
>
{/* Chapter Header */}
<Box sx={{ mb: 4, textAlign: 'center' }}>
<Typography
variant="h3"
component="h1"
sx={{
mb: 2,
fontFamily: preferences.fontFamily === 'serif' ? 'Georgia, serif' : 'Arial, sans-serif'
}}
>
{currentBook?.name} {selectedChapter}
</Typography>
<Typography variant="body2" color="text.secondary">
{verses.length} {t('verses')}
</Typography>
</Box>
{/* Verses */}
<Box sx={{ mb: 4 }}>
{verses.map(renderVerse)}
</Box>
{/* Chapter Navigation */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 4, pt: 3, borderTop: 1, borderColor: 'divider' }}>
<Button
startIcon={<ArrowBack />}
onClick={handlePreviousChapter}
disabled={selectedBook === books[0]?.id && selectedChapter === 1}
variant="outlined"
>
{t('previousChapter')}
</Button>
<Button
endIcon={<ArrowForward />}
onClick={handleNextChapter}
disabled={selectedBook === books[books.length - 1]?.id && selectedChapter === maxChapters}
variant="outlined"
>
{t('nextChapter')}
</Button>
</Box>
</Paper>
)}
</Box>
</Container>
{/* Floating Action Buttons */}
{!preferences.readingMode && (
<>
{showScrollTop && (
<Fab
color="primary"
size="small"
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
sx={{
position: 'fixed',
bottom: 16,
right: 16
}}
>
<KeyboardArrowUp />
</Fab>
)}
</>
)}
{preferences.readingMode && (
<Fab
color="secondary"
size="small"
onClick={() => setPreferences(prev => ({ ...prev, readingMode: false }))}
sx={{
position: 'fixed',
top: 16,
right: 16
}}
>
<FullscreenExit />
</Fab>
)}
{/* Settings Dialog */}
{renderSettings()}
{/* Copy Feedback */}
<Snackbar
open={copyFeedback.open}
autoHideDuration={3000}
onClose={() => setCopyFeedback({ open: false, message: '' })}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert severity="success" onClose={() => setCopyFeedback({ open: false, message: '' })}>
{copyFeedback.message}
</Alert>
</Snackbar>
</Box>
)
}