'use client' import React, { useState, useEffect, useRef, useCallback } from 'react' import { useTranslations, useLocale } from 'next-intl' import { useAuth } from '@/hooks/use-auth' import { useSearchParams, useRouter } from 'next/navigation' import { OfflineDownloadManager } from '@/components/bible/offline-download-manager' import { OfflineBibleReader } from '@/components/bible/offline-bible-reader' import { offlineStorage } from '@/lib/offline-storage' import { InstallPrompt, useInstallPrompt } from '@/components/pwa/install-prompt' 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, Autocomplete } 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, Chat, CloudDownload, WifiOff, Storage, MoreVert } 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 } interface BibleReaderProps { initialVersion?: string initialBook?: string initialChapter?: string } export default function BibleReaderNew({ initialVersion, initialBook, initialChapter }: BibleReaderProps = {}) { const theme = useTheme() const isMobile = useMediaQuery(theme.breakpoints.down('md')) const t = useTranslations('pages.bible') const locale = useLocale() const router = useRouter() const searchParams = useSearchParams() const { user } = useAuth() // Use initial props if provided, otherwise use search params const effectiveParams = React.useMemo(() => { if (initialVersion || initialBook || initialChapter) { // Create a params-like object from the initial props return { get: (key: string) => { if (key === 'version') return initialVersion || null if (key === 'book') return initialBook || null if (key === 'chapter') return initialChapter || null if (key === 'verse') return searchParams.get('verse') // Still get verse from URL query params return null }, has: (key: string) => { if (key === 'version') return !!initialVersion if (key === 'book') return !!initialBook if (key === 'chapter') return !!initialChapter if (key === 'verse') return searchParams.has('verse') return false }, toString: (): string => '' } } return searchParams }, [initialVersion, initialBook, initialChapter, searchParams]) // Core state const [books, setBooks] = useState([]) const [versions, setVersions] = useState([]) const [selectedVersion, setSelectedVersion] = useState('') const [selectedBook, setSelectedBook] = useState('') const [selectedChapter, setSelectedChapter] = useState(1) const [verses, setVerses] = useState([]) const [loading, setLoading] = useState(false) const [versionsLoading, setVersionsLoading] = useState(true) const [showAllVersions, setShowAllVersions] = useState(false) // Debounced version state to prevent rapid API calls const [debouncedVersion, setDebouncedVersion] = useState('') // UI state const [settingsOpen, setSettingsOpen] = useState(false) const [preferences, setPreferences] = useState(defaultPreferences) const [highlightedVerse, setHighlightedVerse] = useState(null) const [showScrollTop, setShowScrollTop] = useState(false) const [previousVerses, setPreviousVerses] = useState([]) // Keep previous content during loading // Offline/PWA state const [isOnline, setIsOnline] = useState(true) const [isOfflineMode, setIsOfflineMode] = useState(false) const [offlineDialogOpen, setOfflineDialogOpen] = 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: '' }) // Verse menu state const [verseMenuAnchor, setVerseMenuAnchor] = useState<{ element: HTMLElement | null verse: BibleVerse | null }>({ element: null, verse: null }) // Refs const contentRef = useRef(null) const verseRefs = useRef<{[key: number]: HTMLDivElement}>({}) // PWA install prompt const { canInstall, isInstalled, showInstallPrompt } = useInstallPrompt() // 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]) // Handle full screen mode - add/remove CSS class to body useEffect(() => { if (preferences.readingMode) { document.body.classList.add('bible-fullscreen-mode') } else { document.body.classList.remove('bible-fullscreen-mode') } // Cleanup on unmount return () => { document.body.classList.remove('bible-fullscreen-mode') } }, [preferences.readingMode]) // 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) }, []) // Online/offline detection useEffect(() => { const handleOnline = () => { setIsOnline(true) if (isOfflineMode) { // Show notification that connection is restored console.log('Connection restored, you can now access all features') } } const handleOffline = () => { setIsOnline(false) console.log('You are now offline. Only downloaded content is available.') } // Set initial state setIsOnline(navigator.onLine) // Check for offline mode preference const offlineParam = new URLSearchParams(window.location.search).get('offline') if (offlineParam === 'true') { setIsOfflineMode(true) } window.addEventListener('online', handleOnline) window.addEventListener('offline', handleOffline) return () => { window.removeEventListener('online', handleOnline) window.removeEventListener('offline', handleOffline) } }, [isOfflineMode]) // Fetch versions based on showAllVersions state and locale useEffect(() => { setVersionsLoading(true) const url = showAllVersions ? '/api/bible/versions?all=true&limit=200' // Limit to first 200 for performance : `/api/bible/versions?language=${locale}` fetch(url) .then(res => res.json()) .then(data => { if (data.success && data.versions) { setVersions(data.versions) // Keep current selection if it exists in new list, otherwise select default/first const currentVersionExists = data.versions.some((v: BibleVersion) => v.id === selectedVersion) if (!currentVersionExists || !selectedVersion) { 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, showAllVersions, selectedVersion]) // Handle URL parameters for bookmark navigation useEffect(() => { const urlVersion = effectiveParams.get('version') const urlBook = effectiveParams.get('book') const urlChapter = effectiveParams.get('chapter') const urlVerse = effectiveParams.get('verse') if (urlVersion && versions.length > 0) { // Check if this version exists const version = versions.find(v => v.id === urlVersion) if (version && selectedVersion !== urlVersion) { setSelectedVersion(urlVersion) } } if (urlBook && books.length > 0) { const book = books.find(b => b.id === urlBook) if (book && selectedBook !== urlBook) { setSelectedBook(urlBook) } } if (urlChapter) { const chapter = parseInt(urlChapter) if (chapter && selectedChapter !== chapter) { setSelectedChapter(chapter) } } if (urlVerse && verses.length > 0) { const verseNum = parseInt(urlVerse) if (verseNum) { // Highlight the verse setTimeout(() => { const verseElement = verseRefs.current[verseNum] if (verseElement) { verseElement.scrollIntoView({ behavior: 'smooth', block: 'center' }) setHighlightedVerse(verseNum) } }, 500) } } }, [effectiveParams, versions, books, verses, selectedVersion, selectedBook, selectedChapter]) // Function to update URL without causing full page reload const updateUrl = useCallback(async (bookId?: string, chapter?: number, versionId?: string) => { const targetVersionId = versionId || selectedVersion const targetBookId = bookId || selectedBook const targetChapter = chapter || selectedChapter // Try to generate SEO-friendly URL try { const version = versions.find(v => v.id === targetVersionId) const book = books.find(b => b.id === targetBookId) if (version && book && targetChapter) { // Generate SEO-friendly URL const versionSlug = version.abbreviation.toLowerCase() const bookSlug = book.bookKey.toLowerCase() const newUrl = `/${locale}/bible/${versionSlug}/${bookSlug}/${targetChapter}` router.replace(newUrl, { scroll: false }) return } } catch (error) { console.error('Error generating SEO-friendly URL:', error) } // Fallback to query parameter URL const params = new URLSearchParams() if (targetVersionId) { params.set('version', targetVersionId) } if (targetBookId) { params.set('book', targetBookId) } if (targetChapter) { params.set('chapter', String(targetChapter)) } const newUrl = `/${locale}/bible?${params.toString()}` router.replace(newUrl, { scroll: false }) }, [locale, selectedVersion, selectedBook, selectedChapter, router, versions, books]) // Debounce version changes to prevent rapid API calls useEffect(() => { const timer = setTimeout(() => { setDebouncedVersion(selectedVersion) }, 300) // 300ms delay return () => clearTimeout(timer) }, [selectedVersion]) // Show loading immediately when selectedVersion changes (before debounce) useEffect(() => { if (selectedVersion && selectedVersion !== debouncedVersion) { setLoading(true) } }, [selectedVersion, debouncedVersion]) // Fetch books when debounced version changes useEffect(() => { if (debouncedVersion) { // Create an AbortController for this request const abortController = new AbortController() // First, try to load books from offline storage const loadBooksOfflineFirst = async () => { try { // Check if we have the books offline const offlineBooks = await offlineStorage.getBooksForVersion(debouncedVersion) if (offlineBooks && offlineBooks.length > 0 && !abortController.signal.aborted) { console.log('Loading books from offline storage') // Transform offline books to match the expected interface const transformedBooks = offlineBooks.map(book => ({ id: book.id, name: book.name, testament: book.testament, orderNum: book.orderNum, bookKey: book.abbreviation, // Map abbreviation to bookKey versionId: debouncedVersion, chaptersCount: book.chaptersCount, chapters: [] // Add empty chapters array to match interface })) setBooks(transformedBooks) if (!initialBook) { setSelectedBook(transformedBooks[0].id) } setLoading(false) return // Exit early, we have offline content } } catch (offlineError) { console.log('Offline storage not available or books not found, falling back to API') } // Fallback to API if offline content not available try { const response = await fetch(`/api/bible/books?locale=${locale}&version=${debouncedVersion}`, { signal: abortController.signal }) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const data = await response.json() // Only update state if the request wasn't aborted if (!abortController.signal.aborted) { setBooks(data.books || []) if (data.books && data.books.length > 0 && !initialBook) { setSelectedBook(data.books[0].id) } setLoading(false) } } catch (err) { // Only log error if it's not an abort error if (!abortController.signal.aborted) { console.error('Error fetching books:', err) setLoading(false) } } } loadBooksOfflineFirst() // Cleanup function to abort the request if component unmounts or dependencies change return () => { abortController.abort() } } }, [locale, debouncedVersion, initialBook]) // Handle URL parameters (only when not using initial props from SEO URLs) useEffect(() => { if (books.length > 0 && versions.length > 0 && !initialVersion && !initialBook && !initialChapter) { 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, initialVersion, initialBook, initialChapter]) // Fetch verses when book/chapter changes useEffect(() => { if (selectedBook && selectedChapter) { setLoading(true) // Store scroll position to prevent jumping const scrollPosition = window.pageYOffset // Create an AbortController for this request const abortController = new AbortController() // First, try to load from offline storage if available const loadChapterOfflineFirst = async () => { try { // Check if we have the chapter offline const offlineChapter = await offlineStorage.getChapter(selectedVersion, selectedBook, selectedChapter) if (offlineChapter && !abortController.signal.aborted) { console.log('Loading chapter from offline storage') // Store previous verses before updating setPreviousVerses(verses) // Use requestAnimationFrame to ensure smooth transition requestAnimationFrame(() => { setVerses(offlineChapter.verses) // Small delay to allow content to render before removing loading state setTimeout(() => { setLoading(false) setPreviousVerses([]) // Clear previous content after transition // Restore scroll position if we're not navigating to a specific verse const urlVerse = new URLSearchParams(window.location.search).get('verse') if (!urlVerse) { // Maintain scroll position for better UX window.scrollTo({ top: Math.min(scrollPosition, document.body.scrollHeight), behavior: 'auto' }) } }, 50) // Small delay for smoother transition }) return // Exit early, we have offline content } } catch (offlineError) { console.log('Offline storage not available or chapter not found, falling back to API') } // Fallback to API if offline content not available try { const response = await fetch(`/api/bible/verses?bookId=${selectedBook}&chapter=${selectedChapter}`, { signal: abortController.signal }) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const data = await response.json() // Only update state if the request wasn't aborted if (!abortController.signal.aborted) { const newVerses = data.verses || [] // Store previous verses before updating setPreviousVerses(verses) // Use requestAnimationFrame to ensure smooth transition requestAnimationFrame(() => { setVerses(newVerses) // Small delay to allow content to render before removing loading state setTimeout(() => { setLoading(false) setPreviousVerses([]) // Clear previous content after transition // Restore scroll position if we're not navigating to a specific verse const urlVerse = new URLSearchParams(window.location.search).get('verse') if (!urlVerse) { // Maintain scroll position for better UX window.scrollTo({ top: Math.min(scrollPosition, document.body.scrollHeight), behavior: 'auto' }) } }, 50) // Small delay for smoother transition }) } } catch (err) { // Only log error if it's not an abort error if (!abortController.signal.aborted) { console.error('Error fetching verses:', err) setLoading(false) } } } loadChapterOfflineFirst() // Cleanup function to abort the request if component unmounts or dependencies change return () => { abortController.abort() } } }, [selectedBook, selectedChapter, selectedVersion]) // 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 handleShare = async () => { if (!selectedBook || !selectedChapter) return const shareUrl = `${window.location.origin}/${locale}/bible?version=${selectedVersion}&book=${selectedBook}&chapter=${selectedChapter}` const shareText = currentBook ? `${currentBook.name} ${selectedChapter}` : `Chapter ${selectedChapter}` if (navigator.share) { try { await navigator.share({ title: shareText, url: shareUrl }) } catch (error) { // User cancelled sharing or fallback to clipboard try { await navigator.clipboard.writeText(shareUrl) } catch (clipError) { console.error('Failed to copy link:', clipError) } } } else { // Fallback: copy to clipboard try { await navigator.clipboard.writeText(shareUrl) } catch (error) { console.error('Failed to copy link:', error) } } } 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 (!selectedBook || !selectedChapter) return // If user is not authenticated, redirect to login if (!user) { router.push(`/${locale}/login?redirect=${encodeURIComponent(`/${locale}/bible?version=${selectedVersion}&book=${selectedBook}&chapter=${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 is not authenticated, redirect to login if (!user) { router.push(`/${locale}/login?redirect=${encodeURIComponent(`/${locale}/bible?version=${selectedVersion}&book=${selectedBook}&chapter=${selectedChapter}&verse=${verse.verseNum}`)}`) 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('copied') }) }) } const handleVerseChat = (verse: BibleVerse) => { // If user is not authenticated, redirect to login if (!user) { router.push(`/${locale}/login?redirect=${encodeURIComponent(`/${locale}/bible?version=${selectedVersion}&book=${selectedBook}&chapter=${selectedChapter}&verse=${verse.verseNum}`)}`) return } // Exit full-screen mode if currently in reading mode so chat is visible if (preferences.readingMode) { setPreferences(prev => ({ ...prev, readingMode: false })) } const versionName = versions.find(v => v.id === selectedVersion)?.name || selectedVersion const bookName = currentBook?.name || 'Unknown Book' const initialMessage = `Explain in depth this verse "${verse.text}" from ${versionName}, ${bookName} ${selectedChapter}:${verse.verseNum} and its meaning` // Small delay to allow full-screen exit animation to complete setTimeout(() => { // Dispatch event to open floating chat with the pre-filled message window.dispatchEvent(new CustomEvent('floating-chat:open', { detail: { initialMessage: initialMessage, fullscreen: false } })) }, 200) } const handleVerseMenuOpen = (event: React.MouseEvent, verse: BibleVerse) => { setVerseMenuAnchor({ element: event.currentTarget, verse: verse }) } const handleVerseMenuClose = () => { setVerseMenuAnchor({ element: null, verse: null }) } const handleVerseMenuAction = (action: 'bookmark' | 'copy' | 'chat') => { if (!verseMenuAnchor.verse) return const verse = verseMenuAnchor.verse handleVerseMenuClose() switch (action) { case 'bookmark': handleVerseBookmark(verse) break case 'copy': handleCopyVerse(verse) break case 'chat': handleVerseChat(verse) break } } 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 ( { if (el) verseRefs.current[verse.verseNum] = el }} data-verse-container sx={{ mb: 1, display: 'flex', alignItems: 'flex-start', gap: 1, '&:hover .verse-actions': { opacity: 1 } }} > {preferences.showVerseNumbers && ( {verse.verseNum} )} {verse.text} handleVerseMenuOpen(e, verse)} sx={{ color: 'action.active', '&:hover': { backgroundColor: preferences.readingMode ? (preferences.theme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : preferences.theme === 'sepia' ? 'rgba(92, 75, 58, 0.1)' : 'rgba(0, 0, 0, 0.05)') : 'action.hover' } }} > ) } const renderNavigation = () => ( {/* First Row: Navigation Filters */} {/* Version Selection */} v.id === selectedVersion) || null} onChange={(event, newValue) => { if (newValue) { setSelectedVersion(newValue.id) // Reset to first book when version changes if (books.length > 0) { setSelectedBook(books[0].id) setSelectedChapter(1) updateUrl(books[0].id, 1, newValue.id) } } }} options={versions} getOptionLabel={(option) => `${option.name} - ${option.abbreviation}`} renderInput={(params) => ( )} renderOption={(props, option) => ( {option.name} {option.abbreviation} • {option.language.toUpperCase()} )} disabled={versionsLoading} loading={versionsLoading} filterOptions={(options, { inputValue }) => { const filtered = options.filter((option) => option.name.toLowerCase().includes(inputValue.toLowerCase()) || option.abbreviation.toLowerCase().includes(inputValue.toLowerCase()) || option.language.toLowerCase().includes(inputValue.toLowerCase()) ) return filtered }} ListboxProps={{ style: { maxHeight: 400 } }} /> setShowAllVersions(prev => !prev)} sx={{ color: showAllVersions ? 'primary.main' : 'inherit' }} > {/* Books Selection */} b.id === selectedBook) || null} onChange={(event, newValue) => { if (newValue) { setSelectedBook(newValue.id) setSelectedChapter(1) updateUrl(newValue.id, 1, selectedVersion) } }} options={books} getOptionLabel={(option) => option.name} renderInput={(params) => ( )} renderOption={(props, option) => ( {option.name} {option.testament} • {option.chapters?.length || 0} chapters )} filterOptions={(options, { inputValue }) => { return options.filter((option) => option.name.toLowerCase().includes(inputValue.toLowerCase()) || option.testament.toLowerCase().includes(inputValue.toLowerCase()) ) }} ListboxProps={{ style: { maxHeight: 300 } }} /> {/* Chapter Selection */} {t('chapter')} {/* Second Row: Settings and Controls */} {/* Font Size Controls */} setPreferences(prev => ({ ...prev, fontSize: Math.max(12, prev.fontSize - 1) }))} disabled={preferences.fontSize <= 12} > A⁻ setPreferences(prev => ({ ...prev, fontSize: Math.min(28, prev.fontSize + 1) }))} disabled={preferences.fontSize >= 28} > A⁺ {/* Action Buttons */} setPreferences(prev => ({ ...prev, readingMode: !prev.readingMode }))} sx={{ color: preferences.readingMode ? 'primary.main' : 'inherit' }} > {preferences.readingMode ? : } setSettingsOpen(true)}> {(isChapterBookmarked && user) ? : } setOfflineDialogOpen(true)} sx={{ color: !isOnline ? 'warning.main' : 'inherit' }} > {isOnline ? : } {canInstall && !isInstalled && ( )} ) const renderSettings = () => ( setSettingsOpen(false)} maxWidth="sm" fullWidth > {t('readingSettings')} {t('fontSize')} setPreferences(prev => ({ ...prev, fontSize: value as number }))} min={12} max={24} marks valueLabelDisplay="auto" /> {t('lineHeight')} setPreferences(prev => ({ ...prev, lineHeight: value as number }))} min={1.2} max={2.0} step={0.1} marks valueLabelDisplay="auto" /> {t('fontFamily')} {t('theme')} setPreferences(prev => ({ ...prev, showVerseNumbers: e.target.checked }))} /> } label={t('showVerseNumbers')} /> setPreferences(prev => ({ ...prev, readingMode: e.target.checked }))} /> } label={t('readingMode')} /> ) // Always render the UI - loading will be handled within components return ( {/* Top Toolbar - Simplified */} {!preferences.readingMode && ( {currentBook?.name} {selectedChapter} )} {/* Main Content */} {/* Navigation Section - Always show but with different styling in reading mode */} {renderNavigation()} {/* Reading Content */} {loading && ( )} {/* Chapter Header */} {loading && !currentBook ? ( // Skeleton loading for chapter header <> ) : ( <> {currentBook?.name} {selectedChapter} {(loading && previousVerses.length > 0 ? previousVerses : verses).length} {t('verses')} )} {/* Verses */} {loading && verses.length === 0 && previousVerses.length === 0 ? ( // Skeleton loading for verses <> {Array.from({ length: 8 }).map((_, index) => ( {Math.random() > 0.5 && ( )} ))} ) : ( (loading && previousVerses.length > 0 ? previousVerses : verses).map(renderVerse) )} {/* Chapter Navigation */} {/* Floating Action Buttons */} {!preferences.readingMode && ( <> {showScrollTop && ( window.scrollTo({ top: 0, behavior: 'smooth' })} sx={{ position: 'fixed', bottom: 16, right: 16 }} > )} )} {preferences.readingMode && ( setPreferences(prev => ({ ...prev, readingMode: false }))} sx={{ position: 'fixed', top: 16, right: 16 }} > )} {/* Settings Dialog */} {renderSettings()} {/* Offline Downloads Dialog */} setOfflineDialogOpen(false)} maxWidth="md" fullWidth fullScreen={isMobile} > { console.log(`Version ${versionId} downloaded successfully`) }} /> {/* PWA Install Prompt */} {/* Verse Menu */} handleVerseMenuAction('bookmark')}> {verseMenuAnchor.verse && verseBookmarks[verseMenuAnchor.verse.id] ? ( ) : ( )} {verseMenuAnchor.verse && verseBookmarks[verseMenuAnchor.verse.id] ? 'Remove Bookmark' : 'Bookmark'} handleVerseMenuAction('copy')}> Copy Verse handleVerseMenuAction('chat')}> Ask AI {/* Copy Feedback */} setCopyFeedback({ open: false, message: '' })} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} > setCopyFeedback({ open: false, message: '' })}> {copyFeedback.message} ) }