Features added: - Database schema for pages and media files with content types (Rich Text, HTML, Markdown) - Admin API routes for full page CRUD operations - Image upload functionality with file management - Rich text editor using TinyMCE with image insertion - Admin interface for creating/editing pages with SEO options - Dynamic navigation and footer integration - Public page display routes with proper SEO metadata - Support for featured images and content excerpts Admin features: - Create/edit/delete pages with rich content editor - Upload and manage images through media library - Configure pages to appear in navigation or footer - Set page status (Draft, Published, Archived) - SEO title and description management - Real-time preview of content changes 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1116 lines
33 KiB
TypeScript
1116 lines
33 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 BibleVersion {
|
|
id: string
|
|
name: string
|
|
abbreviation: string
|
|
language: string
|
|
isDefault?: boolean
|
|
}
|
|
|
|
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 [versions, setVersions] = useState<BibleVersion[]>([])
|
|
const [selectedVersion, setSelectedVersion] = useState<string>('')
|
|
const [selectedBook, setSelectedBook] = useState<string>('')
|
|
const [selectedChapter, setSelectedChapter] = useState<number>(1)
|
|
const [verses, setVerses] = useState<BibleVerse[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [versionsLoading, setVersionsLoading] = 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)
|
|
}
|
|
}
|
|
|
|
// Load saved version preference
|
|
const savedVersion = localStorage.getItem('selectedBibleVersion')
|
|
if (savedVersion && versions.length > 0) {
|
|
const version = versions.find(v => v.id === savedVersion)
|
|
if (version) {
|
|
setSelectedVersion(savedVersion)
|
|
}
|
|
}
|
|
}, [versions])
|
|
|
|
// Save preferences to localStorage
|
|
useEffect(() => {
|
|
localStorage.setItem('bibleReaderPreferences', JSON.stringify(preferences))
|
|
}, [preferences])
|
|
|
|
// Save selected version to localStorage
|
|
useEffect(() => {
|
|
if (selectedVersion) {
|
|
localStorage.setItem('selectedBibleVersion', selectedVersion)
|
|
}
|
|
}, [selectedVersion])
|
|
|
|
// 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 versions based on current locale
|
|
useEffect(() => {
|
|
setVersionsLoading(true)
|
|
fetch(`/api/bible/versions?language=${locale}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.success && data.versions) {
|
|
setVersions(data.versions)
|
|
// Select default version or first available
|
|
const defaultVersion = data.versions.find((v: BibleVersion) => v.isDefault) || data.versions[0]
|
|
if (defaultVersion) {
|
|
setSelectedVersion(defaultVersion.id)
|
|
}
|
|
}
|
|
setVersionsLoading(false)
|
|
})
|
|
.catch(err => {
|
|
console.error('Error fetching versions:', err)
|
|
setVersionsLoading(false)
|
|
})
|
|
}, [locale])
|
|
|
|
// Fetch books when version changes
|
|
useEffect(() => {
|
|
if (selectedVersion) {
|
|
setLoading(true)
|
|
fetch(`/api/bible/books?locale=${locale}&version=${selectedVersion}`)
|
|
.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, selectedVersion])
|
|
|
|
// Handle URL parameters
|
|
useEffect(() => {
|
|
if (books.length > 0 && versions.length > 0) {
|
|
const bookParam = searchParams.get('book')
|
|
const chapterParam = searchParams.get('chapter')
|
|
const verseParam = searchParams.get('verse')
|
|
const versionParam = searchParams.get('version')
|
|
|
|
// Handle version parameter
|
|
if (versionParam && versions.find(v => v.id === versionParam)) {
|
|
setSelectedVersion(versionParam)
|
|
}
|
|
|
|
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, versions, 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, version?: string) => {
|
|
const url = new URL(window.location.href)
|
|
url.searchParams.set('book', bookId)
|
|
url.searchParams.set('chapter', chapter.toString())
|
|
if (version) {
|
|
url.searchParams.set('version', version)
|
|
}
|
|
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, selectedVersion)
|
|
} 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, selectedVersion)
|
|
}
|
|
}
|
|
}
|
|
|
|
const handleNextChapter = () => {
|
|
if (selectedChapter < maxChapters) {
|
|
const newChapter = selectedChapter + 1
|
|
setSelectedChapter(newChapter)
|
|
updateUrl(selectedBook, newChapter, selectedVersion)
|
|
} 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, selectedVersion)
|
|
}
|
|
}
|
|
}
|
|
|
|
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}&version=${selectedVersion}`
|
|
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' }}>
|
|
{/* Version Selection */}
|
|
<Box sx={{ flex: { xs: '1 1 100%', sm: '1 1 auto' }, minWidth: { sm: 180, md: 200 } }}>
|
|
<FormControl fullWidth size="small">
|
|
<InputLabel>{t('version')}</InputLabel>
|
|
<Select
|
|
value={selectedVersion}
|
|
label={t('version')}
|
|
onChange={(e) => {
|
|
setSelectedVersion(e.target.value)
|
|
// Reset to first book when version changes
|
|
if (books.length > 0) {
|
|
setSelectedBook(books[0].id)
|
|
setSelectedChapter(1)
|
|
updateUrl(books[0].id, 1, e.target.value)
|
|
}
|
|
}}
|
|
disabled={versionsLoading}
|
|
>
|
|
{versions.map((version) => (
|
|
<MenuItem key={version.id} value={version.id}>
|
|
{version.abbreviation} - {version.name}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
</Box>
|
|
|
|
{/* 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, selectedVersion)
|
|
}}
|
|
>
|
|
{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, selectedVersion)
|
|
}}
|
|
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>
|
|
)
|
|
}
|