'use client' import { useState, useEffect, useRef } from 'react' import { useLocale } from 'next-intl' import { Box, Typography, Button } from '@mui/material' import { BibleChapter, BibleVerse, BibleHighlight, HighlightColor } from '@/types' import { getCachedChapter, cacheChapter } from '@/lib/cache-manager' import { SearchNavigator } from './search-navigator' import { ReadingView } from './reading-view' import { VersDetailsPanel } from './verse-details-panel' import { ReadingSettings } from './reading-settings' import { HighlightSyncManager } from '@/lib/highlight-sync-manager' import { addHighlight, updateHighlight, getHighlightsByVerse, deleteHighlight, getAllHighlights } from '@/lib/highlight-manager' import { pullAndMergeHighlights } from '@/lib/highlight-pull-sync' interface BookInfo { id: string // UUID orderNum: number bookKey: string name: string chapterCount: number } export function BibleReaderApp() { const locale = useLocale() const [bookId, setBookId] = useState(1) // Genesis (numeric ID from search) const [chapter, setChapter] = useState(1) const [currentChapter, setCurrentChapter] = useState(null) const [selectedVerse, setSelectedVerse] = useState(null) const [detailsPanelOpen, setDetailsPanelOpen] = useState(false) const [settingsOpen, setSettingsOpen] = useState(false) const [loading, setLoading] = useState(true) const [bookmarks, setBookmarks] = useState>(new Set()) const [books, setBooks] = useState([]) const [versionId, setVersionId] = useState('') const [error, setError] = useState(null) const [booksLoading, setBooksLoading] = useState(true) const [highlights, setHighlights] = useState>(new Map()) const syncManager = useRef(null) const [syncStatus, setSyncStatus] = useState<'synced' | 'syncing' | 'pending' | 'error'>('synced') const [syncError, setSyncError] = useState(null) // Load books on mount or when locale changes useEffect(() => { loadBooks() }, [locale]) // Load chapter when bookId or chapter changes useEffect(() => { if (!booksLoading && books.length > 0) { loadChapter(bookId, chapter) } }, [bookId, chapter, booksLoading, books.length]) // Initialize sync manager on mount useEffect(() => { syncManager.current = new HighlightSyncManager() syncManager.current.init() syncManager.current.startAutoSync(30000, () => { performSync() }) return () => { syncManager.current?.stopAutoSync() } }, []) // Pull highlights from server when component mounts (user logged in) useEffect(() => { const pullHighlights = async () => { try { const merged = await pullAndMergeHighlights() const map = new Map(merged.map(h => [h.verseId, h])) setHighlights(map) } catch (error) { console.error('Failed to pull highlights:', error) } } pullHighlights() }, []) // Load all highlights on mount useEffect(() => { loadAllHighlights() }, []) async function loadBooks() { setBooksLoading(true) setError(null) try { const response = await fetch(`/api/bible/books?locale=${locale}`) if (!response.ok) { throw new Error(`Failed to load books: ${response.status}`) } const data = await response.json() if (data.books && Array.isArray(data.books)) { const bookMap: BookInfo[] = data.books.map((book: any) => ({ id: book.id, orderNum: book.orderNum, bookKey: book.bookKey, name: book.name, chapterCount: book.chapters.length })) setBooks(bookMap) setVersionId(data.version?.id || 'unknown') } else { throw new Error('Invalid books response format') } } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Unknown error loading books' setError(errorMsg) console.error('Error loading books:', error) } finally { setBooksLoading(false) } } async function loadChapter(numericBookId: number, chapterNum: number) { setLoading(true) setError(null) try { const book = books.find(b => b.orderNum === numericBookId) if (!book) { setError(`Book not found (ID: ${numericBookId})`) setCurrentChapter(null) return } // Try cache first const chapterId = `${book.id}-${chapterNum}` let data = await getCachedChapter(chapterId) // If not cached, fetch from API if (!data) { const response = await fetch(`/api/bible/chapter?book=${book.id}&chapter=${chapterNum}`) if (!response.ok) { throw new Error(`Failed to load chapter: ${response.status} ${response.statusText}`) } const json = await response.json() data = json.chapter // Cache it if (data) { data.id = chapterId await cacheChapter(data).catch(e => console.error('Cache error:', e)) } } setCurrentChapter(data) } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Unknown error loading chapter' setError(errorMsg) setCurrentChapter(null) console.error('Error loading chapter:', error) } finally { setLoading(false) } } const handleVerseClick = (verseId: string) => { const verse = currentChapter?.verses.find(v => v.id === verseId) if (verse) { setSelectedVerse(verse) setDetailsPanelOpen(true) } } const handleToggleBookmark = () => { if (!selectedVerse) return const newBookmarks = new Set(bookmarks) if (newBookmarks.has(selectedVerse.id)) { newBookmarks.delete(selectedVerse.id) } else { newBookmarks.add(selectedVerse.id) } setBookmarks(newBookmarks) // TODO: Sync to backend in Phase 2 console.log('Bookmarks updated:', Array.from(newBookmarks)) } useEffect(() => { // Persist bookmarks to localStorage const bookmarkArray = Array.from(bookmarks) localStorage.setItem('bible-reader-bookmarks', JSON.stringify(bookmarkArray)) }, [bookmarks]) // On mount, load bookmarks from localStorage useEffect(() => { const stored = localStorage.getItem('bible-reader-bookmarks') if (stored) { try { const bookmarkArray = JSON.parse(stored) as string[] setBookmarks(new Set(bookmarkArray)) } catch (e) { console.error('Failed to load bookmarks:', e) } } }, []) const handleAddNote = (note: string) => { if (!selectedVerse) return // TODO: Save note to backend in Phase 2 console.log(`Note for verse ${selectedVerse.id}:`, note) } async function loadAllHighlights() { try { const highlightList = await getAllHighlights() const map = new Map(highlightList.map(h => [h.verseId, h])) setHighlights(map) } catch (error) { console.error('Failed to load highlights:', error) } } async function handleHighlightVerse(color: HighlightColor = 'yellow') { if (!selectedVerse) return const highlight: BibleHighlight = { id: `h-${selectedVerse.id}-${Date.now()}`, verseId: selectedVerse.id, color, createdAt: Date.now(), updatedAt: Date.now(), syncStatus: 'pending' } try { await addHighlight(highlight) const newMap = new Map(highlights) newMap.set(selectedVerse.id, highlight) setHighlights(newMap) } catch (error) { console.error('Failed to highlight verse:', error) } } async function handleChangeHighlightColor(color: HighlightColor) { if (!selectedVerse) return const existing = highlights.get(selectedVerse.id) if (existing) { const updated = { ...existing, color, updatedAt: Date.now(), syncStatus: 'pending' as const } try { await updateHighlight(updated) const newMap = new Map(highlights) newMap.set(selectedVerse.id, updated) setHighlights(newMap) } catch (error) { console.error('Failed to update highlight color:', error) } } } async function handleRemoveHighlight() { if (!selectedVerse) return try { // Find and delete all highlights for this verse const existing = highlights.get(selectedVerse.id) if (existing) { await deleteHighlight(existing.id) const newMap = new Map(highlights) newMap.delete(selectedVerse.id) setHighlights(newMap) } } catch (error) { console.error('Failed to remove highlight:', error) } } async function performSync() { if (!syncManager.current) return try { setSyncStatus('syncing') const result = await syncManager.current.performSync() if (result.errors > 0) { setSyncStatus('error') setSyncError(`Failed to sync ${result.errors} highlights`) } else { setSyncStatus('synced') setSyncError(null) } } catch (error) { setSyncStatus('error') setSyncError(error instanceof Error ? error.message : 'Unknown error') } } return ( {/* Header with search */} { setBookId(newBookId) setChapter(newChapter) }} /> {/* Reading area */} {!booksLoading && error ? ( {error} ) : booksLoading ? ( Initializing Bible reader... ) : loading ? ( Loading chapter... ) : currentChapter ? ( chapter > 1 && setChapter(chapter - 1)} onNextChapter={() => { const book = books.find(b => b.orderNum === bookId) if (book && chapter < book.chapterCount) { setChapter(chapter + 1) } }} onVerseClick={handleVerseClick} onSettingsOpen={() => setSettingsOpen(true)} hasPrevChapter={chapter > 1} hasNextChapter={(() => { const book = books.find(b => b.orderNum === bookId) return book ? chapter < book.chapterCount : false })()} /> ) : ( Failed to load chapter. Please try again. )} {/* Details panel */} setDetailsPanelOpen(false)} isBookmarked={selectedVerse ? bookmarks.has(selectedVerse.id) : false} onToggleBookmark={handleToggleBookmark} onAddNote={handleAddNote} isHighlighted={highlights.has(selectedVerse?.id || '')} currentHighlightColor={highlights.get(selectedVerse?.id || '')?.color} onHighlightVerse={handleHighlightVerse} onChangeHighlightColor={handleChangeHighlightColor} onRemoveHighlight={handleRemoveHighlight} syncStatus={syncStatus} syncErrorMessage={syncError || undefined} /> {/* Settings panel */} {settingsOpen && ( setSettingsOpen(false)} /> )} ) }