'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 { useSwipeable } from 'react-swipeable' import { AuthModal } from '@/components/auth/auth-modal' 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, LinearProgress } 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, Star, StarBorder, Edit } from '@mui/icons-material' interface BibleVerse { id: string verseNum: number text: string } interface BibleChapter { id: string chapterNum: number verses: BibleVerse[] } interface TextHighlight { id: string verseId: string color: 'yellow' | 'green' | 'blue' | 'purple' | 'orange' | 'pink' | 'red' note?: string tags?: string[] createdAt: Date updatedAt: Date } 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[] chaptersCount?: number } interface ReadingPreferences { fontSize: number lineHeight: number fontFamily: string theme: 'light' | 'dark' | 'sepia' showVerseNumbers: boolean columnLayout: boolean readingMode: boolean letterSpacing: number // 0-2px range for character spacing wordSpacing: number // 0-4px range for word spacing paragraphSpacing: number // 1.0-2.5x line height for paragraph spacing maxLineLength: number // 50-100 characters (ch units) for optimal reading width enableSwipeGestures: boolean // Enable swipe left/right for chapter navigation enableTapZones: boolean // Enable tap zones (left=prev, right=next) paginationMode: boolean // Page-by-page vs continuous scroll } const defaultPreferences: ReadingPreferences = { fontSize: 18, lineHeight: 1.6, fontFamily: 'serif', theme: 'light', showVerseNumbers: true, columnLayout: false, readingMode: false, letterSpacing: 0.5, // 0.5px default (WCAG 2.1 SC 1.4.12 recommends 0.12em) wordSpacing: 0, // 0px default (browser default is optimal) paragraphSpacing: 1.8, // 1.8x line spacing (WCAG recommends ≥1.5x) maxLineLength: 75, // 75ch optimal reading width (50-75 for desktop) enableSwipeGestures: true, // Enable by default for mobile enableTapZones: true, // Enable by default for mobile paginationMode: false // Continuous scroll by default } 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() // Add global accessibility styles for focus indicators (WCAG AAA) useEffect(() => { const style = document.createElement('style') style.innerHTML = ` /* Global focus indicators - WCAG AAA Compliance */ button:focus-visible, a:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible, [role="button"]:focus-visible, [tabindex]:not([tabindex="-1"]):focus-visible { outline: 2px solid #1976d2 !important; outline-offset: 2px !important; } /* Ensure 200% zoom support - WCAG AAA */ @media (max-width: 1280px) { html { font-size: 100% !important; } } /* Prevent horizontal scroll at 200% zoom */ body { overflow-x: hidden; } ` document.head.appendChild(style) return () => { document.head.removeChild(style) } }, []) // 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' && typeof window !== 'undefined') return searchParams.get('verse') // Only on client return null }, has: (key: string) => { if (key === 'version') return !!initialVersion if (key === 'book') return !!initialBook if (key === 'chapter') return !!initialChapter if (key === 'verse' && typeof window !== 'undefined') return searchParams.has('verse') // Only on client 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) // Highlight state const [highlights, setHighlights] = useState<{[key: string]: TextHighlight}>({}) const [highlightColorPickerAnchor, setHighlightColorPickerAnchor] = useState<{ element: HTMLElement | null verse: BibleVerse | null }>({ element: null, verse: null }) // Reading progress state const [readingProgress, setReadingProgress] = useState(null) const [hasLoadedInitialProgress, setHasLoadedInitialProgress] = useState(false) // Page transition state const [isTransitioning, setIsTransitioning] = useState(false) // Accessibility announcement state const [ariaAnnouncement, setAriaAnnouncement] = useState('') // Note dialog state const [noteDialog, setNoteDialog] = useState<{ open: boolean verse?: BibleVerse note: string highlightId?: string }>({ open: false, note: '', highlightId: undefined }) // 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 }) // Auth modal state const [authModalOpen, setAuthModalOpen] = useState(false) const [authModalMessage, setAuthModalMessage] = useState('') const [pendingAction, setPendingAction] = useState<(() => void) | null>(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 (client-side only) if (typeof window !== 'undefined') { setIsOnline(navigator.onLine) } // Check for offline mode preference const offlineParam = typeof window !== 'undefined' ? new URLSearchParams(window.location.search).get('offline') : null 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(() => { const loadVersions = async () => { setVersionsLoading(true) const url = showAllVersions ? '/api/bible/versions?all=true' // Load ALL versions, no limit : `/api/bible/versions?language=${locale}` try { const res = await fetch(url) const data = await res.json() if (data.success && data.versions) { setVersions(data.versions) // Check if current version is available in the new locale's versions const currentVersionAvailable = selectedVersion && data.versions.find((v: BibleVersion) => v.id === selectedVersion) // Auto-select if there's NO current selection OR if current version is not available in new locale if (!selectedVersion || (!showAllVersions && !currentVersionAvailable)) { let versionToSelect = null // Try to load user's favorite version first, but only if it matches the current locale if (user && !showAllVersions) { const token = localStorage.getItem('authToken') if (token) { try { const favoriteRes = await fetch('/api/user/favorite-version', { headers: { 'Authorization': `Bearer ${token}` } }) const favoriteData = await favoriteRes.json() if (favoriteData.success && favoriteData.favoriteBibleVersion) { // Check if favorite version is in the current locale's versions const favoriteVersion = data.versions.find((v: BibleVersion) => v.id === favoriteData.favoriteBibleVersion) if (favoriteVersion) { versionToSelect = favoriteVersion } } } catch (error) { console.error('Error loading favorite version:', error) } } } // Fall back to default version for this locale or first version if (!versionToSelect) { versionToSelect = data.versions.find((v: BibleVersion) => v.isDefault && v.language.toLowerCase() === locale.toLowerCase()) || data.versions.find((v: BibleVersion) => v.isDefault) || data.versions[0] } if (versionToSelect) { setSelectedVersion(versionToSelect.id) } } } setVersionsLoading(false) } catch (err) { console.error('Error fetching versions:', err) setVersionsLoading(false) } } loadVersions() // eslint-disable-next-line react-hooks/exhaustive-deps }, [locale, showAllVersions, user]) // Removed selectedVersion from dependencies to prevent infinite loop // 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]) // Load reading progress when version changes useEffect(() => { // Only run on client side to avoid hydration mismatch if (typeof window === 'undefined') return const loadProgress = async () => { if (debouncedVersion && user && !hasLoadedInitialProgress) { const progress = await loadReadingProgress(debouncedVersion) if (progress) { setReadingProgress(progress) // Only restore position if we haven't loaded from URL params if (!effectiveParams.get('book') && !effectiveParams.get('chapter')) { setSelectedBook(progress.bookId) setSelectedChapter(progress.chapterNum) } } setHasLoadedInitialProgress(true) } } loadProgress() // eslint-disable-next-line react-hooks/exhaustive-deps }, [debouncedVersion, user, hasLoadedInitialProgress, effectiveParams]) // Save reading progress when chapter changes useEffect(() => { if (selectedVersion && selectedBook && selectedChapter && user) { // Debounce saving to avoid too many API calls const timer = setTimeout(() => { saveReadingProgress(selectedVersion, selectedBook, selectedChapter) }, 2000) // Save after 2 seconds of no changes return () => clearTimeout(timer) } }, [selectedVersion, selectedBook, selectedChapter, user]) // 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) { // Transform books to include chaptersCount const transformedBooks = (data.books || []).map((book: any) => ({ ...book, chaptersCount: book.chapters?.length || 0 })) setBooks(transformedBooks) if (transformedBooks.length > 0 && !initialBook) { setSelectedBook(transformedBooks[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]) // Load highlights for current verses useEffect(() => { if (verses.length > 0 && user) { const token = localStorage.getItem('authToken') if (token) { const verseIds = verses.map(verse => verse.id) fetch(`/api/highlights/bulk?locale=${locale}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ verseIds }) }) .then(res => res.json()) .then(data => setHighlights(data.highlights || {})) .catch(err => console.error('Error loading highlights:', err)) } } else { setHighlights({}) } }, [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 || typeof window === 'undefined') return const shareUrl = `${window.location.origin}/${locale}/bible?version=${selectedVersion}&book=${selectedBook}&chapter=${selectedChapter}` const shareText = currentBook ? `${currentBook.name} ${selectedChapter}` : `Chapter ${selectedChapter}` if (typeof navigator !== 'undefined' && 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 if (typeof navigator !== 'undefined') { // 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 requireAuth = (action: () => void, message: string) => { if (!user) { setAuthModalMessage(message) setPendingAction(() => action) setAuthModalOpen(true) return false } return true } const handleAuthSuccess = () => { setAuthModalOpen(false) setAuthModalMessage('') // Execute pending action if there is one if (pendingAction) { pendingAction() setPendingAction(null) } } const handlePreviousChapter = () => { // Trigger transition animation setIsTransitioning(true) setTimeout(() => setIsTransitioning(false), 300) // Match CSS transition duration if (selectedChapter > 1) { const newChapter = selectedChapter - 1 setSelectedChapter(newChapter) updateUrl(selectedBook, newChapter, selectedVersion) // Announce for screen readers setAriaAnnouncement(`Navigated to ${currentBook?.name} chapter ${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, selectedVersion) // Announce for screen readers setAriaAnnouncement(`Navigated to ${previousBook.name} chapter ${lastChapter}`) } } } const handleNextChapter = () => { // Trigger transition animation setIsTransitioning(true) setTimeout(() => setIsTransitioning(false), 300) // Match CSS transition duration if (selectedChapter < maxChapters) { const newChapter = selectedChapter + 1 setSelectedChapter(newChapter) updateUrl(selectedBook, newChapter, selectedVersion) // Announce for screen readers setAriaAnnouncement(`Navigated to ${currentBook?.name} chapter ${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, selectedVersion) // Announce for screen readers setAriaAnnouncement(`Navigated to ${nextBook.name} chapter 1`) } } } // Swipe handlers for mobile navigation const swipeHandlers = useSwipeable({ onSwipedLeft: () => { if (preferences.enableSwipeGestures && isMobile) { handleNextChapter() } }, onSwipedRight: () => { if (preferences.enableSwipeGestures && isMobile) { handlePreviousChapter() } }, preventScrollOnSwipe: false, trackMouse: false, // Only track touch, not mouse delta: 50 // Minimum swipe distance in pixels }) // Tap zone handler for quick navigation const handleTapZone = (event: React.MouseEvent) => { if (!preferences.enableTapZones || !isMobile) return const target = event.currentTarget const rect = target.getBoundingClientRect() const clickX = event.clientX - rect.left const tapZoneWidth = rect.width * 0.25 // 25% on each side if (clickX < tapZoneWidth) { // Left tap zone - previous chapter handlePreviousChapter() } else if (clickX > rect.width - tapZoneWidth) { // Right tap zone - next chapter handleNextChapter() } } const handleChapterBookmark = async () => { if (!selectedBook || !selectedChapter) return // If user is not authenticated, show auth modal if (!requireAuth(handleChapterBookmark, 'Please login to bookmark this chapter')) { 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, show auth modal if (!requireAuth(() => handleVerseBookmark(verse), 'Please login to bookmark this verse')) { 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) => { if (typeof navigator === 'undefined') return const text = `${currentBook?.name} ${selectedChapter}:${verse.verseNum} - ${verse.text}` navigator.clipboard.writeText(text).then(() => { setCopyFeedback({ open: true, message: t('copied') }) }).catch(err => { console.error('Failed to copy verse:', err) }) } const handleVerseChat = (verse: BibleVerse) => { // If user is not authenticated, show auth modal if (!requireAuth(() => handleVerseChat(verse), 'Please login to ask AI about this verse')) { 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' | 'highlight') => { if (!verseMenuAnchor.verse) return const verse = verseMenuAnchor.verse switch (action) { case 'bookmark': handleVerseBookmark(verse) handleVerseMenuClose() break case 'copy': handleCopyVerse(verse) handleVerseMenuClose() break case 'chat': handleVerseChat(verse) handleVerseMenuClose() break case 'highlight': // Keep menu open, show color picker instead setHighlightColorPickerAnchor({ element: verseMenuAnchor.element, verse: verse }) break } } const handleHighlightVerse = async (verse: BibleVerse, color: TextHighlight['color']) => { // If user is not authenticated, show auth modal if (!requireAuth(() => handleHighlightVerse(verse, color), 'Please login to highlight this verse')) { return } const token = localStorage.getItem('authToken') if (!token) return try { // Check if verse already has a highlight const existingHighlight = highlights[verse.id] if (existingHighlight) { // Update highlight color const response = await fetch(`/api/highlights/${existingHighlight.id}?locale=${locale}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ color }) }) if (response.ok) { const data = await response.json() setHighlights(prev => ({ ...prev, [verse.id]: data.highlight })) } } else { // Create new highlight const response = await fetch(`/api/highlights?locale=${locale}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ verseId: verse.id, color }) }) if (response.ok) { const data = await response.json() setHighlights(prev => ({ ...prev, [verse.id]: data.highlight })) } } } catch (error) { console.error('Error highlighting verse:', error) } // Close color picker and menu setHighlightColorPickerAnchor({ element: null, verse: null }) handleVerseMenuClose() } const handleRemoveHighlight = async (verse: BibleVerse) => { if (!user) return const token = localStorage.getItem('authToken') if (!token) return try { const highlight = highlights[verse.id] if (!highlight) return const response = await fetch(`/api/highlights/${highlight.id}?locale=${locale}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }) if (response.ok) { setHighlights(prev => { const newHighlights = { ...prev } delete newHighlights[verse.id] return newHighlights }) } } catch (error) { console.error('Error removing highlight:', error) } setHighlightColorPickerAnchor({ element: null, verse: null }) handleVerseMenuClose() } const handleOpenNoteDialog = (verse: BibleVerse) => { const highlight = highlights[verse.id] setNoteDialog({ open: true, verse, note: highlight?.note || '', highlightId: highlight?.id }) setHighlightColorPickerAnchor({ element: null, verse: null }) handleVerseMenuClose() } const handleSaveNote = async () => { if (!noteDialog.verse || !user) return const token = localStorage.getItem('authToken') if (!token) return try { const { verse, note, highlightId } = noteDialog if (highlightId) { // Update existing highlight's note const response = await fetch(`/api/highlights/${highlightId}?locale=${locale}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ note: note.trim() || null }) }) if (response.ok) { const data = await response.json() setHighlights(prev => ({ ...prev, [verse.id]: data.highlight })) setCopyFeedback({ open: true, message: note.trim() ? 'Note saved' : 'Note removed' }) } } setNoteDialog({ open: false, note: '', highlightId: undefined }) } catch (error) { console.error('Error saving note:', error) alert('Failed to save note') } } const handleSetFavoriteVersion = async () => { // If user is not authenticated, show auth modal if (!requireAuth(handleSetFavoriteVersion, 'Please login to set your default Bible version')) { return } const token = localStorage.getItem('authToken') if (!token) return try { const response = await fetch('/api/user/favorite-version', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ versionId: selectedVersion }) }) const data = await response.json() if (data.success) { setCopyFeedback({ open: true, message: 'This version has been set as your default' }) } else { alert('Failed to set favorite version') } } catch (error) { console.error('Error setting favorite version:', error) alert('Failed to set favorite version') } } // Save reading progress const saveReadingProgress = async (versionId: string, bookId: string, chapterNum: number) => { if (!user) return const token = localStorage.getItem('authToken') if (!token) return try { await fetch('/api/user/reading-progress', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ versionId, bookId, chapterNum, verseNum: null }) }) } catch (error) { console.error('Error saving reading progress:', error) } } // Load reading progress for current version const loadReadingProgress = async (versionId: string) => { if (!user) return null const token = localStorage.getItem('authToken') if (!token) return null try { const response = await fetch(`/api/user/reading-progress?versionId=${versionId}`, { headers: { 'Authorization': `Bearer ${token}` } }) const data = await response.json() if (data.success && data.progress) { return data.progress } } catch (error) { console.error('Error loading reading progress:', error) } return null } // Calculate reading progress percentage const calculateProgress = () => { if (!books.length || !selectedBook || !selectedChapter) return 0 // Find current book index and total chapters before current position let totalChaptersBefore = 0 let foundCurrentBook = false let currentBookChapters = 0 for (const book of books) { if (book.id === selectedBook) { foundCurrentBook = true currentBookChapters = book.chaptersCount || 0 // Add chapters from current book up to current chapter totalChaptersBefore += selectedChapter break } if (!foundCurrentBook) { totalChaptersBefore += book.chaptersCount || 0 } } // Calculate total chapters in entire Bible const totalChapters = books.reduce((sum, book) => sum + (book.chaptersCount || 0), 0) if (totalChapters === 0) return 0 const percentage = (totalChaptersBefore / totalChapters) * 100 return Math.min(Math.round(percentage), 100) } const getThemeStyles = () => { switch (preferences.theme) { case 'dark': return { backgroundColor: '#0d0d0d', // Darker for better contrast (WCAG AAA: 15.3:1) color: '#f0f0f0', // Brighter text for 7:1+ contrast borderColor: '#404040' } case 'sepia': return { backgroundColor: '#f5f1e3', // Adjusted sepia background color: '#2b2419', // Darker text for 7:1+ contrast (WCAG AAA) borderColor: '#d4c5a0' } default: return { backgroundColor: '#ffffff', color: '#000000', // Pure black on white = 21:1 (exceeds WCAG AAA) borderColor: '#e0e0e0' } } } const getHighlightColor = (color: TextHighlight['color'], theme: 'light' | 'dark' | 'sepia') => { const colors = { yellow: { light: '#fff9c4', dark: '#7f6000', sepia: '#f5e6b3' }, green: { light: '#c8e6c9', dark: '#2e7d32', sepia: '#d4e8d4' }, blue: { light: '#bbdefb', dark: '#1565c0', sepia: '#c8dce8' }, purple: { light: '#e1bee7', dark: '#6a1b9a', sepia: '#e3d4e8' }, orange: { light: '#ffe0b2', dark: '#e65100', sepia: '#f5ddc8' }, pink: { light: '#f8bbd0', dark: '#c2185b', sepia: '#f5d8e3' }, red: { light: '#ffcdd2', dark: '#c62828', sepia: '#f5d0d4' } } return colors[color][theme] } const renderVerse = (verse: BibleVerse) => { const isBookmarked = !!verseBookmarks[verse.id] const isHighlighted = highlightedVerse === verse.verseNum const highlight = highlights[verse.id] return ( { if (el) verseRefs.current[verse.verseNum] = el }} data-verse-container sx={{ mb: `${preferences.fontSize * preferences.lineHeight * (preferences.paragraphSpacing - 1)}px`, display: 'flex', alignItems: 'flex-start', gap: 1, '&:hover .verse-actions': { opacity: 1 } }} > {preferences.showVerseNumbers && ( {verse.verseNum} )} {verse.text} {/* Display highlight note if it exists */} {highlight?.note && ( Note: {highlight.note} )} 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 && ( )} {/* Reading Progress Bar */} {user && books.length > 0 && ( Reading Progress {calculateProgress()}% )} ) 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" /> Letter Spacing setPreferences(prev => ({ ...prev, letterSpacing: value as number }))} min={0} max={2} step={0.1} marks valueLabelDisplay="auto" /> Word Spacing setPreferences(prev => ({ ...prev, wordSpacing: value as number }))} min={0} max={4} step={0.5} marks valueLabelDisplay="auto" /> Paragraph Spacing setPreferences(prev => ({ ...prev, paragraphSpacing: value as number }))} min={1.0} max={2.5} step={0.1} marks valueLabelDisplay="auto" /> Max Line Length setPreferences(prev => ({ ...prev, maxLineLength: value as number }))} min={50} max={100} step={5} 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')} /> Mobile Navigation setPreferences(prev => ({ ...prev, enableSwipeGestures: e.target.checked }))} /> } label="Enable Swipe Gestures" /> setPreferences(prev => ({ ...prev, enableTapZones: e.target.checked }))} /> } label="Enable Tap Zones" /> setPreferences(prev => ({ ...prev, paginationMode: e.target.checked }))} /> } label="Pagination Mode" /> ) // Always render the UI - loading will be handled within components return ( {/* Skip Navigation Link - WCAG AAA */} Skip to main content {/* ARIA Live Region for Screen Reader Announcements */} {ariaAnnouncement} {/* 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('highlight')}> {verseMenuAnchor.verse && highlights[verseMenuAnchor.verse.id] ? 'Change Highlight' : 'Highlight'} handleVerseMenuAction('copy')}> Copy Verse handleVerseMenuAction('chat')}> Ask AI {/* Highlight Color Picker */} { setHighlightColorPickerAnchor({ element: null, verse: null }) handleVerseMenuClose() }} anchorOrigin={{ vertical: 'bottom', horizontal: 'right', }} transformOrigin={{ vertical: 'top', horizontal: 'right', }} > Select Highlight Color {(['yellow', 'green', 'blue', 'purple', 'orange', 'pink', 'red'] as const).map(color => ( { if (highlightColorPickerAnchor.verse) { handleHighlightVerse(highlightColorPickerAnchor.verse, color) } }} sx={{ width: 40, height: 40, backgroundColor: getHighlightColor(color, preferences.theme), border: highlightColorPickerAnchor.verse && highlights[highlightColorPickerAnchor.verse.id]?.color === color ? '3px solid' : '1px solid', borderColor: highlightColorPickerAnchor.verse && highlights[highlightColorPickerAnchor.verse.id]?.color === color ? 'primary.main' : 'divider', '&:hover': { backgroundColor: getHighlightColor(color, preferences.theme), opacity: 0.8 } }} /> ))} {highlightColorPickerAnchor.verse && highlights[highlightColorPickerAnchor.verse.id] && ( <> )} {/* Note Dialog */} setNoteDialog({ open: false, note: '', highlightId: undefined })} maxWidth="sm" fullWidth > {noteDialog.highlightId ? 'Edit Note' : 'Add Note'} setNoteDialog(prev => ({ ...prev, note: e.target.value }))} sx={{ mt: 2 }} /> {noteDialog.verse && ( {currentBook?.name} {selectedChapter}:{noteDialog.verse.verseNum} )} {/* Copy Feedback */} setCopyFeedback({ open: false, message: '' })} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} > setCopyFeedback({ open: false, message: '' })}> {copyFeedback.message} {/* Auth Modal */} { setAuthModalOpen(false) setAuthModalMessage('') setPendingAction(null) }} onSuccess={handleAuthSuccess} message={authModalMessage} defaultTab="login" /> ) }