feat: integrate highlight management into reader app
- Added HighlightSyncManager and highlight state management to BibleReaderApp - Implemented highlight handlers: add, update color, remove, and sync - Connected highlight state from BibleReaderApp to VersDetailsPanel - Updated VersDetailsPanel to pass highlight props to HighlightsTab - Added auto-sync initialization with 30-second interval - Prepared for Phase 2.1B API integration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,15 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { useLocale } from 'next-intl'
|
import { useLocale } from 'next-intl'
|
||||||
import { Box, Typography, Button } from '@mui/material'
|
import { Box, Typography, Button } from '@mui/material'
|
||||||
import { BibleChapter, BibleVerse } from '@/types'
|
import { BibleChapter, BibleVerse, BibleHighlight, HighlightColor } from '@/types'
|
||||||
import { getCachedChapter, cacheChapter } from '@/lib/cache-manager'
|
import { getCachedChapter, cacheChapter } from '@/lib/cache-manager'
|
||||||
import { SearchNavigator } from './search-navigator'
|
import { SearchNavigator } from './search-navigator'
|
||||||
import { ReadingView } from './reading-view'
|
import { ReadingView } from './reading-view'
|
||||||
import { VersDetailsPanel } from './verse-details-panel'
|
import { VersDetailsPanel } from './verse-details-panel'
|
||||||
import { ReadingSettings } from './reading-settings'
|
import { ReadingSettings } from './reading-settings'
|
||||||
|
import { HighlightSyncManager } from '@/lib/highlight-sync-manager'
|
||||||
|
import { addHighlight, updateHighlight, getHighlightsByVerse, deleteHighlight, getAllHighlights } from '@/lib/highlight-manager'
|
||||||
|
|
||||||
interface BookInfo {
|
interface BookInfo {
|
||||||
id: string // UUID
|
id: string // UUID
|
||||||
@@ -31,6 +33,8 @@ export function BibleReaderApp() {
|
|||||||
const [versionId, setVersionId] = useState<string>('')
|
const [versionId, setVersionId] = useState<string>('')
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [booksLoading, setBooksLoading] = useState(true)
|
const [booksLoading, setBooksLoading] = useState(true)
|
||||||
|
const [highlights, setHighlights] = useState<Map<string, BibleHighlight>>(new Map())
|
||||||
|
const syncManager = useRef<HighlightSyncManager | null>(null)
|
||||||
|
|
||||||
// Load books on mount or when locale changes
|
// Load books on mount or when locale changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -44,6 +48,24 @@ export function BibleReaderApp() {
|
|||||||
}
|
}
|
||||||
}, [bookId, chapter, booksLoading, books.length])
|
}, [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()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Load all highlights on mount
|
||||||
|
useEffect(() => {
|
||||||
|
loadAllHighlights()
|
||||||
|
}, [])
|
||||||
|
|
||||||
async function loadBooks() {
|
async function loadBooks() {
|
||||||
setBooksLoading(true)
|
setBooksLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
@@ -168,6 +190,95 @@ export function BibleReaderApp() {
|
|||||||
console.log(`Note for verse ${selectedVerse.id}:`, note)
|
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 {
|
||||||
|
const pending = await syncManager.current.getPendingSyncItems()
|
||||||
|
if (pending.length === 0) return
|
||||||
|
|
||||||
|
await syncManager.current.markSyncing(pending.map(h => h.id))
|
||||||
|
|
||||||
|
// TODO: POST to /api/highlights/bulk in Phase 2.1B
|
||||||
|
|
||||||
|
await syncManager.current.markSynced(pending.map(h => h.id))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Sync failed:', error)
|
||||||
|
// Mark items with error status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: 'auto', overflow: 'hidden' }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', height: 'auto', overflow: 'hidden' }}>
|
||||||
{/* Header with search */}
|
{/* Header with search */}
|
||||||
@@ -238,6 +349,11 @@ export function BibleReaderApp() {
|
|||||||
isBookmarked={selectedVerse ? bookmarks.has(selectedVerse.id) : false}
|
isBookmarked={selectedVerse ? bookmarks.has(selectedVerse.id) : false}
|
||||||
onToggleBookmark={handleToggleBookmark}
|
onToggleBookmark={handleToggleBookmark}
|
||||||
onAddNote={handleAddNote}
|
onAddNote={handleAddNote}
|
||||||
|
isHighlighted={highlights.has(selectedVerse?.id || '')}
|
||||||
|
currentHighlightColor={highlights.get(selectedVerse?.id || '')?.color}
|
||||||
|
onHighlightVerse={handleHighlightVerse}
|
||||||
|
onChangeHighlightColor={handleChangeHighlightColor}
|
||||||
|
onRemoveHighlight={handleRemoveHighlight}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Settings panel */}
|
{/* Settings panel */}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Box, Paper, Typography, Tabs, Tab, IconButton, useMediaQuery, useTheme, TextField, Button } from '@mui/material'
|
import { Box, Paper, Typography, Tabs, Tab, IconButton, useMediaQuery, useTheme, TextField, Button } from '@mui/material'
|
||||||
import { Close, Bookmark, BookmarkBorder } from '@mui/icons-material'
|
import { Close, Bookmark, BookmarkBorder } from '@mui/icons-material'
|
||||||
import { BibleVerse } from '@/types'
|
import { BibleVerse, HighlightColor } from '@/types'
|
||||||
import { HighlightsTab } from './highlights-tab'
|
import { HighlightsTab } from './highlights-tab'
|
||||||
|
|
||||||
interface VersDetailsPanelProps {
|
interface VersDetailsPanelProps {
|
||||||
@@ -12,6 +12,11 @@ interface VersDetailsPanelProps {
|
|||||||
isBookmarked: boolean
|
isBookmarked: boolean
|
||||||
onToggleBookmark: () => void
|
onToggleBookmark: () => void
|
||||||
onAddNote: (note: string) => void
|
onAddNote: (note: string) => void
|
||||||
|
isHighlighted?: boolean
|
||||||
|
currentHighlightColor?: HighlightColor | null
|
||||||
|
onHighlightVerse?: (color: HighlightColor) => void
|
||||||
|
onChangeHighlightColor?: (color: HighlightColor) => void
|
||||||
|
onRemoveHighlight?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VersDetailsPanel({
|
export function VersDetailsPanel({
|
||||||
@@ -21,6 +26,11 @@ export function VersDetailsPanel({
|
|||||||
isBookmarked,
|
isBookmarked,
|
||||||
onToggleBookmark,
|
onToggleBookmark,
|
||||||
onAddNote,
|
onAddNote,
|
||||||
|
isHighlighted,
|
||||||
|
currentHighlightColor,
|
||||||
|
onHighlightVerse,
|
||||||
|
onChangeHighlightColor,
|
||||||
|
onRemoveHighlight,
|
||||||
}: VersDetailsPanelProps) {
|
}: VersDetailsPanelProps) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
|
||||||
@@ -121,10 +131,16 @@ export function VersDetailsPanel({
|
|||||||
{tabValue === 1 && (
|
{tabValue === 1 && (
|
||||||
<HighlightsTab
|
<HighlightsTab
|
||||||
verse={verse}
|
verse={verse}
|
||||||
isHighlighted={false} // TODO: get from state
|
isHighlighted={isHighlighted || false}
|
||||||
currentColor={null} // TODO: get from state
|
currentColor={currentHighlightColor || null}
|
||||||
onToggleHighlight={() => {}} // TODO: implement
|
onToggleHighlight={() => {
|
||||||
onColorChange={() => {}} // TODO: implement
|
if (isHighlighted) {
|
||||||
|
onRemoveHighlight?.()
|
||||||
|
} else {
|
||||||
|
onHighlightVerse?.('yellow')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onColorChange={(color) => onChangeHighlightColor?.(color)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user