38 KiB
Phase 2.1: Rich Annotations & Highlighting - Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task with code reviews.
Goal: Build a complete highlighting and annotation system that works offline-first with seamless sync.
Architecture: Local-first storage in IndexedDB with automatic sync to backend. Highlights appear immediately, sync happens in background every 30 seconds. Uses timestamp-based conflict resolution for cross-device sync.
Tech Stack: TypeScript, React, Material-UI, IndexedDB, Prisma (backend), TDD
Task 1: Extend Types for Highlights
Files:
- Modify:
/root/biblical-guide/types/index.ts - Test:
/root/biblical-guide/__tests__/types/highlights.test.ts(create)
Step 1: Write failing test
Create file /root/biblical-guide/__tests__/types/highlights.test.ts:
import { BibleHighlight } from '@/types'
describe('BibleHighlight types', () => {
it('should create highlight with valid color', () => {
const highlight: BibleHighlight = {
id: 'test-id',
verseId: 'verse-123',
color: 'yellow',
createdAt: Date.now(),
updatedAt: Date.now(),
syncStatus: 'synced'
}
expect(highlight.color).toBe('yellow')
})
it('should reject invalid color', () => {
// This test validates TypeScript type checking
// @ts-expect-error - 'red' is not a valid color
const highlight: BibleHighlight = {
id: 'test-id',
verseId: 'verse-123',
color: 'red',
createdAt: Date.now(),
updatedAt: Date.now(),
syncStatus: 'synced'
}
})
it('should validate syncStatus types', () => {
const highlight: BibleHighlight = {
id: 'test-id',
verseId: 'verse-123',
color: 'blue',
createdAt: Date.now(),
updatedAt: Date.now(),
syncStatus: 'pending'
}
expect(['pending', 'syncing', 'synced', 'error']).toContain(highlight.syncStatus)
})
})
Step 2: Run test to verify it fails
npm test -- __tests__/types/highlights.test.ts
Expected output: FAIL - "BibleHighlight is not defined"
Step 3: Add types to /root/biblical-guide/types/index.ts
Add at the end of the file:
// Highlight system types
export type HighlightColor = 'yellow' | 'orange' | 'pink' | 'blue'
export type SyncStatus = 'pending' | 'syncing' | 'synced' | 'error'
export interface BibleHighlight {
id: string // UUID
verseId: string
userId?: string // Optional, added by backend
color: HighlightColor
createdAt: number // timestamp
updatedAt: number // timestamp
syncStatus: SyncStatus
syncErrorMsg?: string
}
export interface HighlightSyncQueueItem {
highlightId: string
action: 'create' | 'update' | 'delete'
highlight: BibleHighlight
retryCount: number
}
export interface CrossReference {
refVerseId: string
bookName: string
chapter: number
verse: number
preview: string
}
Step 4: Run test to verify it passes
npm test -- __tests__/types/highlights.test.ts
Expected output: PASS - all 3 tests pass
Step 5: Commit
git add types/index.ts __tests__/types/highlights.test.ts
git commit -m "feat: add TypeScript types for highlights and sync system"
Task 2: Create Highlights IndexedDB Store
Files:
- Create:
/root/biblical-guide/lib/highlight-manager.ts - Test:
/root/biblical-guide/__tests__/lib/highlight-manager.test.ts(create)
Step 1: Write failing test
Create file /root/biblical-guide/__tests__/lib/highlight-manager.test.ts:
import { initHighlightsDatabase, addHighlight, getHighlight, getAllHighlights, deleteHighlight } from '@/lib/highlight-manager'
import { BibleHighlight } from '@/types'
describe('HighlightManager', () => {
beforeEach(async () => {
// Clear IndexedDB before each test
const db = await initHighlightsDatabase()
const tx = db.transaction('highlights', 'readwrite')
tx.objectStore('highlights').clear()
})
it('should initialize database with highlights store', async () => {
const db = await initHighlightsDatabase()
expect(db.objectStoreNames.contains('highlights')).toBe(true)
})
it('should add a highlight and retrieve it', async () => {
const highlight: BibleHighlight = {
id: 'h-123',
verseId: 'v-456',
color: 'yellow',
createdAt: Date.now(),
updatedAt: Date.now(),
syncStatus: 'pending'
}
await addHighlight(highlight)
const retrieved = await getHighlight('h-123')
expect(retrieved).toEqual(highlight)
})
it('should get all highlights', async () => {
const highlights: BibleHighlight[] = [
{ id: 'h-1', verseId: 'v-1', color: 'yellow', createdAt: Date.now(), updatedAt: Date.now(), syncStatus: 'pending' },
{ id: 'h-2', verseId: 'v-2', color: 'blue', createdAt: Date.now(), updatedAt: Date.now(), syncStatus: 'synced' }
]
for (const h of highlights) {
await addHighlight(h)
}
const all = await getAllHighlights()
expect(all.length).toBe(2)
})
it('should delete a highlight', async () => {
const highlight: BibleHighlight = {
id: 'h-123',
verseId: 'v-456',
color: 'yellow',
createdAt: Date.now(),
updatedAt: Date.now(),
syncStatus: 'pending'
}
await addHighlight(highlight)
await deleteHighlight('h-123')
const retrieved = await getHighlight('h-123')
expect(retrieved).toBeNull()
})
})
Step 2: Run test to verify it fails
npm test -- __tests__/lib/highlight-manager.test.ts
Expected output: FAIL - "highlight-manager module not found"
Step 3: Create highlight-manager.ts
Create file /root/biblical-guide/lib/highlight-manager.ts:
import { BibleHighlight } from '@/types'
const DB_NAME = 'BiblicalGuide'
const DB_VERSION = 2 // Increment version if schema changes
const HIGHLIGHTS_STORE = 'highlights'
let db: IDBDatabase | null = null
export async function initHighlightsDatabase(): Promise<IDBDatabase> {
if (db) return db
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION)
request.onerror = () => reject(new Error('Failed to open IndexedDB'))
request.onsuccess = () => {
db = request.result
resolve(db)
}
request.onupgradeneeded = (event) => {
const database = (event.target as IDBOpenDBRequest).result
// Create highlights store if it doesn't exist
if (!database.objectStoreNames.contains(HIGHLIGHTS_STORE)) {
const store = database.createObjectStore(HIGHLIGHTS_STORE, { keyPath: 'id' })
// Index for finding highlights by syncStatus for batch operations
store.createIndex('syncStatus', 'syncStatus', { unique: false })
// Index for finding highlights by verse
store.createIndex('verseId', 'verseId', { unique: false })
}
}
})
}
export async function addHighlight(highlight: BibleHighlight): Promise<string> {
const db = await initHighlightsDatabase()
return new Promise((resolve, reject) => {
const tx = db.transaction(HIGHLIGHTS_STORE, 'readwrite')
const store = tx.objectStore(HIGHLIGHTS_STORE)
const request = store.add(highlight)
request.onsuccess = () => resolve(request.result as string)
request.onerror = () => reject(new Error('Failed to add highlight'))
})
}
export async function updateHighlight(highlight: BibleHighlight): Promise<void> {
const db = await initHighlightsDatabase()
return new Promise((resolve, reject) => {
const tx = db.transaction(HIGHLIGHTS_STORE, 'readwrite')
const store = tx.objectStore(HIGHLIGHTS_STORE)
const request = store.put(highlight)
request.onsuccess = () => resolve()
request.onerror = () => reject(new Error('Failed to update highlight'))
})
}
export async function getHighlight(id: string): Promise<BibleHighlight | null> {
const db = await initHighlightsDatabase()
return new Promise((resolve, reject) => {
const tx = db.transaction(HIGHLIGHTS_STORE, 'readonly')
const store = tx.objectStore(HIGHLIGHTS_STORE)
const request = store.get(id)
request.onsuccess = () => resolve(request.result || null)
request.onerror = () => reject(new Error('Failed to get highlight'))
})
}
export async function getHighlightsByVerse(verseId: string): Promise<BibleHighlight[]> {
const db = await initHighlightsDatabase()
return new Promise((resolve, reject) => {
const tx = db.transaction(HIGHLIGHTS_STORE, 'readonly')
const store = tx.objectStore(HIGHLIGHTS_STORE)
const index = store.index('verseId')
const request = index.getAll(verseId)
request.onsuccess = () => resolve(request.result || [])
request.onerror = () => reject(new Error('Failed to get highlights by verse'))
})
}
export async function getAllHighlights(): Promise<BibleHighlight[]> {
const db = await initHighlightsDatabase()
return new Promise((resolve, reject) => {
const tx = db.transaction(HIGHLIGHTS_STORE, 'readonly')
const store = tx.objectStore(HIGHLIGHTS_STORE)
const request = store.getAll()
request.onsuccess = () => resolve(request.result || [])
request.onerror = () => reject(new Error('Failed to get all highlights'))
})
}
export async function getPendingHighlights(): Promise<BibleHighlight[]> {
const db = await initHighlightsDatabase()
return new Promise((resolve, reject) => {
const tx = db.transaction(HIGHLIGHTS_STORE, 'readonly')
const store = tx.objectStore(HIGHLIGHTS_STORE)
const index = store.index('syncStatus')
const request = index.getAll(IDBKeyRange.only('pending'))
request.onsuccess = () => resolve(request.result || [])
request.onerror = () => reject(new Error('Failed to get pending highlights'))
})
}
export async function deleteHighlight(id: string): Promise<void> {
const db = await initHighlightsDatabase()
return new Promise((resolve, reject) => {
const tx = db.transaction(HIGHLIGHTS_STORE, 'readwrite')
const store = tx.objectStore(HIGHLIGHTS_STORE)
const request = store.delete(id)
request.onsuccess = () => resolve()
request.onerror = () => reject(new Error('Failed to delete highlight'))
})
}
export async function clearAllHighlights(): Promise<void> {
const db = await initHighlightsDatabase()
return new Promise((resolve, reject) => {
const tx = db.transaction(HIGHLIGHTS_STORE, 'readwrite')
const store = tx.objectStore(HIGHLIGHTS_STORE)
const request = store.clear()
request.onsuccess = () => resolve()
request.onerror = () => reject(new Error('Failed to clear highlights'))
})
}
Step 4: Run test to verify it passes
npm test -- __tests__/lib/highlight-manager.test.ts
Expected output: PASS - all 5 tests pass
Step 5: Commit
git add lib/highlight-manager.ts __tests__/lib/highlight-manager.test.ts
git commit -m "feat: create highlight manager with IndexedDB storage"
Task 3: Create Highlight Sync Manager Service
Files:
- Create:
/root/biblical-guide/lib/highlight-sync-manager.ts - Test:
/root/biblical-guide/__tests__/lib/highlight-sync-manager.test.ts(create)
Step 1: Write failing test
Create file /root/biblical-guide/__tests__/lib/highlight-sync-manager.test.ts:
import { HighlightSyncManager } from '@/lib/highlight-sync-manager'
import { BibleHighlight } from '@/types'
describe('HighlightSyncManager', () => {
let manager: HighlightSyncManager
beforeEach(() => {
manager = new HighlightSyncManager()
})
it('should add highlight to sync queue', async () => {
const highlight: BibleHighlight = {
id: 'h-1',
verseId: 'v-1',
color: 'yellow',
createdAt: Date.now(),
updatedAt: Date.now(),
syncStatus: 'pending'
}
await manager.queueHighlight(highlight)
const pending = await manager.getPendingSyncItems()
expect(pending.length).toBe(1)
expect(pending[0].highlightId).toBe('h-1')
})
it('should mark highlight as syncing', async () => {
const highlight: BibleHighlight = {
id: 'h-1',
verseId: 'v-1',
color: 'yellow',
createdAt: Date.now(),
updatedAt: Date.now(),
syncStatus: 'pending'
}
await manager.queueHighlight(highlight)
await manager.markSyncing(['h-1'])
const syncing = await manager.getSyncingItems()
expect(syncing.length).toBe(1)
})
it('should mark highlight as synced', async () => {
const highlight: BibleHighlight = {
id: 'h-1',
verseId: 'v-1',
color: 'yellow',
createdAt: Date.now(),
updatedAt: Date.now(),
syncStatus: 'pending'
}
await manager.queueHighlight(highlight)
await manager.markSynced(['h-1'])
const pending = await manager.getPendingSyncItems()
expect(pending.length).toBe(0)
})
it('should retry sync on error', async () => {
const highlight: BibleHighlight = {
id: 'h-1',
verseId: 'v-1',
color: 'yellow',
createdAt: Date.now(),
updatedAt: Date.now(),
syncStatus: 'pending'
}
await manager.queueHighlight(highlight)
await manager.markError(['h-1'], 'Network error')
await manager.markSyncing(['h-1'])
const syncing = await manager.getSyncingItems()
expect(syncing.length).toBe(1)
})
})
Step 2: Run test to verify it fails
npm test -- __tests__/lib/highlight-sync-manager.test.ts
Expected output: FAIL - "HighlightSyncManager is not defined"
Step 3: Create highlight-sync-manager.ts
Create file /root/biblical-guide/lib/highlight-sync-manager.ts:
import { BibleHighlight, HighlightSyncQueueItem } from '@/types'
import {
initHighlightsDatabase,
updateHighlight,
getHighlight
} from './highlight-manager'
const SYNC_QUEUE_STORE = 'highlight_sync_queue'
export class HighlightSyncManager {
private db: IDBDatabase | null = null
private syncInterval: NodeJS.Timeout | null = null
async init() {
this.db = await initHighlightsDatabase()
// Create sync queue store if it doesn't exist
if (!this.db.objectStoreNames.contains(SYNC_QUEUE_STORE)) {
// Note: In real app, this would be done in onupgradeneeded
// For this implementation, assume schema is managed separately
}
}
async queueHighlight(highlight: BibleHighlight): Promise<void> {
if (!this.db) await this.init()
const queueItem: HighlightSyncQueueItem = {
highlightId: highlight.id,
action: highlight.syncStatus === 'synced' ? 'update' : 'create',
highlight,
retryCount: 0
}
await updateHighlight({
...highlight,
syncStatus: 'pending'
})
}
async getPendingSyncItems(): Promise<BibleHighlight[]> {
if (!this.db) await this.init()
return new Promise((resolve, reject) => {
const tx = this.db!.transaction('highlights', 'readonly')
const store = tx.objectStore('highlights')
const index = store.index('syncStatus')
const request = index.getAll('pending')
request.onsuccess = () => resolve(request.result || [])
request.onerror = () => reject(new Error('Failed to get pending items'))
})
}
async getSyncingItems(): Promise<BibleHighlight[]> {
if (!this.db) await this.init()
return new Promise((resolve, reject) => {
const tx = this.db!.transaction('highlights', 'readonly')
const store = tx.objectStore('highlights')
const index = store.index('syncStatus')
const request = index.getAll('syncing')
request.onsuccess = () => resolve(request.result || [])
request.onerror = () => reject(new Error('Failed to get syncing items'))
})
}
async markSyncing(highlightIds: string[]): Promise<void> {
if (!this.db) await this.init()
for (const id of highlightIds) {
const highlight = await getHighlight(id)
if (highlight) {
await updateHighlight({
...highlight,
syncStatus: 'syncing'
})
}
}
}
async markSynced(highlightIds: string[]): Promise<void> {
if (!this.db) await this.init()
for (const id of highlightIds) {
const highlight = await getHighlight(id)
if (highlight) {
await updateHighlight({
...highlight,
syncStatus: 'synced'
})
}
}
}
async markError(highlightIds: string[], errorMsg: string): Promise<void> {
if (!this.db) await this.init()
for (const id of highlightIds) {
const highlight = await getHighlight(id)
if (highlight) {
await updateHighlight({
...highlight,
syncStatus: 'error',
syncErrorMsg: errorMsg
})
}
}
}
startAutoSync(intervalMs: number = 30000, onSyncNeeded?: () => void) {
this.syncInterval = setInterval(async () => {
const pending = await this.getPendingSyncItems()
if (pending.length > 0 && onSyncNeeded) {
onSyncNeeded()
}
}, intervalMs)
}
stopAutoSync() {
if (this.syncInterval) {
clearInterval(this.syncInterval)
this.syncInterval = null
}
}
}
Step 4: Run test to verify it passes
npm test -- __tests__/lib/highlight-sync-manager.test.ts
Expected output: PASS - all 5 tests pass
Step 5: Commit
git add lib/highlight-sync-manager.ts __tests__/lib/highlight-sync-manager.test.ts
git commit -m "feat: create highlight sync manager with queue logic"
Task 4: Create HighlightsTab Component
Files:
- Create:
/root/biblical-guide/components/bible/highlights-tab.tsx - Modify:
/root/biblical-guide/components/bible/verse-details-panel.tsx - Test:
/root/biblical-guide/__tests__/components/highlights-tab.test.tsx(create)
Step 1: Write failing test
Create file /root/biblical-guide/__tests__/components/highlights-tab.test.tsx:
import { render, screen, fireEvent } from '@testing-library/react'
import { HighlightsTab } from '@/components/bible/highlights-tab'
import { BibleVerse } from '@/types'
describe('HighlightsTab', () => {
const mockVerse: BibleVerse = {
id: 'v-1',
verseNum: 1,
text: 'In the beginning God created the heavens and the earth'
}
it('should render highlight button when verse not highlighted', () => {
render(
<HighlightsTab
verse={mockVerse}
isHighlighted={false}
currentColor={null}
onToggleHighlight={() => {}}
onColorChange={() => {}}
/>
)
expect(screen.getByText(/Highlight/i)).toBeInTheDocument()
})
it('should render color picker when verse is highlighted', () => {
render(
<HighlightsTab
verse={mockVerse}
isHighlighted={true}
currentColor="yellow"
onToggleHighlight={() => {}}
onColorChange={() => {}}
/>
)
expect(screen.getByText(/Remove highlight/i)).toBeInTheDocument()
})
it('should call onColorChange when color is selected', () => {
const onColorChange = jest.fn()
render(
<HighlightsTab
verse={mockVerse}
isHighlighted={true}
currentColor="yellow"
onToggleHighlight={() => {}}
onColorChange={onColorChange}
/>
)
const blueButton = screen.getByTestId('color-blue')
fireEvent.click(blueButton)
expect(onColorChange).toHaveBeenCalledWith('blue')
})
})
Step 2: Run test to verify it fails
npm test -- __tests__/components/highlights-tab.test.tsx
Expected output: FAIL - "HighlightsTab is not defined"
Step 3: Create highlights-tab.tsx
Create file /root/biblical-guide/components/bible/highlights-tab.tsx:
'use client'
import { Box, Button, Grid, Typography, Divider } from '@mui/material'
import { BibleVerse, HighlightColor } from '@/types'
const HIGHLIGHT_COLORS: HighlightColor[] = ['yellow', 'orange', 'pink', 'blue']
const COLOR_MAP: Record<HighlightColor, { bg: string; hex: string }> = {
yellow: { bg: 'rgba(255, 193, 7, 0.3)', hex: '#FFC107' },
orange: { bg: 'rgba(255, 152, 0, 0.3)', hex: '#FF9800' },
pink: { bg: 'rgba(233, 30, 99, 0.3)', hex: '#E91E63' },
blue: { bg: 'rgba(33, 150, 243, 0.3)', hex: '#2196F3' }
}
interface HighlightsTabProps {
verse: BibleVerse | null
isHighlighted: boolean
currentColor: HighlightColor | null
onToggleHighlight: () => void
onColorChange: (color: HighlightColor) => void
}
export function HighlightsTab({
verse,
isHighlighted,
currentColor,
onToggleHighlight,
onColorChange
}: HighlightsTabProps) {
if (!verse) return null
return (
<Box sx={{ p: 2 }}>
{!isHighlighted ? (
<Button
fullWidth
variant="contained"
color="primary"
onClick={onToggleHighlight}
>
Highlight this verse
</Button>
) : (
<>
<Button
fullWidth
variant="outlined"
color="error"
onClick={onToggleHighlight}
sx={{ mb: 2 }}
>
Remove highlight
</Button>
<Typography variant="subtitle2" sx={{ mb: 1 }}>
Highlight Color
</Typography>
<Grid container spacing={1} sx={{ mb: 2 }}>
{HIGHLIGHT_COLORS.map((color) => (
<Grid item xs={3} key={color}>
<Button
data-testid={`color-${color}`}
fullWidth
variant={currentColor === color ? 'contained' : 'outlined'}
onClick={() => onColorChange(color)}
sx={{
bgcolor: COLOR_MAP[color].bg,
borderColor: COLOR_MAP[color].hex,
border: currentColor === color ? `2px solid ${COLOR_MAP[color].hex}` : undefined,
minHeight: 50,
textTransform: 'capitalize',
color: currentColor === color ? '#000' : 'inherit'
}}
>
{color}
</Button>
</Grid>
))}
</Grid>
<Divider sx={{ my: 2 }} />
<Typography variant="body2" color="textSecondary">
You can highlight the same verse multiple times with different colors.
</Typography>
</>
)}
</Box>
)
}
Step 4: Run test to verify it passes
npm test -- __tests__/components/highlights-tab.test.tsx
Expected output: PASS - all 3 tests pass
Step 5: Modify VersDetailsPanel to include HighlightsTab
In /root/biblical-guide/components/bible/verse-details-panel.tsx, import and add the HighlightsTab:
import { HighlightsTab } from './highlights-tab'
// In the TabsContainer, add:
<Tab label="Highlights" value="highlights" />
// In the TabPanel area, add:
<TabPanel value="highlights">
<HighlightsTab
verse={verse}
isHighlighted={false} // TODO: get from state
currentColor={null} // TODO: get from state
onToggleHighlight={() => {}} // TODO: implement
onColorChange={() => {}} // TODO: implement
/>
</TabPanel>
Step 6: Commit
git add components/bible/highlights-tab.tsx __tests__/components/highlights-tab.test.tsx
git commit -m "feat: create HighlightsTab component with color picker"
Task 5: Enhance VerseRenderer with Highlight Background
Files:
- Modify:
/root/biblical-guide/components/bible/reading-view.tsx - Test: (add to existing reading-view tests)
Step 1: Update VerseRenderer to show highlight
In /root/biblical-guide/components/bible/reading-view.tsx, find the verse rendering code and update:
// Add highlight background color support
const COLOR_MAP: Record<HighlightColor, string> = {
yellow: 'rgba(255, 193, 7, 0.3)',
orange: 'rgba(255, 152, 0, 0.3)',
pink: 'rgba(233, 30, 99, 0.3)',
blue: 'rgba(33, 150, 243, 0.3)'
}
// In the verse rendering, update the span to:
<span
key={verse.id}
role="button"
tabIndex={0}
aria-label={`Verse ${verse.verseNum}: ${verse.text}`}
onClick={(e) => {
e.stopPropagation()
onVerseClick(verse.id)
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
onVerseClick(verse.id)
}
}}
onMouseEnter={() => setHoveredVerseNum(verse.verseNum)}
onMouseLeave={() => setHoveredVerseNum(null)}
style={{
backgroundColor: verse.highlight ? COLOR_MAP[verse.highlight.color] : 'transparent',
padding: '0.25rem 0.5rem',
borderRadius: '4px',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
>
{verse.text}
</span>
Note: This assumes verse has been extended with an optional highlight property. For now, this will be a visual placeholder for Task 6 integration.
Step 2: Commit
git add components/bible/reading-view.tsx
git commit -m "feat: add highlight background color support to verse renderer"
Task 6: Integrate Highlights into BibleReaderApp
Files:
- Modify:
/root/biblical-guide/components/bible/bible-reader-app.tsx - Modify:
/root/biblical-guide/components/bible/verse-details-panel.tsx
Step 1: Update BibleReaderApp to manage highlights
Add highlight state and sync manager to /root/biblical-guide/components/bible/bible-reader-app.tsx:
import { HighlightSyncManager } from '@/lib/highlight-sync-manager'
import { addHighlight, updateHighlight, getHighlightsByVerse, deleteHighlight } from '@/lib/highlight-manager'
import { BibleHighlight, HighlightColor } from '@/types'
// In component state:
const [highlights, setHighlights] = useState<Map<string, BibleHighlight>>(new Map())
const syncManager = useRef<HighlightSyncManager | null>(null)
// 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()
}, [])
// Functions to add to component:
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 {
await deleteHighlight(`h-${selectedVerse.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
}
}
Step 2: Update VersDetailsPanel to use highlight functions
In /root/biblical-guide/components/bible/verse-details-panel.tsx:
interface VersDetailsPanelProps {
// ... existing props
isHighlighted?: boolean
currentHighlightColor?: HighlightColor | null
onHighlightVerse?: (color: HighlightColor) => void
onChangeHighlightColor?: (color: HighlightColor) => void
onRemoveHighlight?: () => void
}
// Pass to HighlightsTab:
<HighlightsTab
verse={verse}
isHighlighted={isHighlighted || false}
currentColor={currentHighlightColor || null}
onToggleHighlight={() => {
if (isHighlighted) {
onRemoveHighlight?.()
} else {
onHighlightVerse?.('yellow')
}
}}
onColorChange={(color) => onChangeHighlightColor?.(color)}
/>
Step 3: Pass highlight state from BibleReaderApp to VersDetailsPanel
<VersDetailsPanel
// ... existing props
isHighlighted={highlights.has(selectedVerse?.id || '')}
currentHighlightColor={highlights.get(selectedVerse?.id || '')?.color}
onHighlightVerse={handleHighlightVerse}
onChangeHighlightColor={handleChangeHighlightColor}
onRemoveHighlight={handleRemoveHighlight}
/>
Step 4: Commit
git add components/bible/bible-reader-app.tsx components/bible/verse-details-panel.tsx
git commit -m "feat: integrate highlight management into reader app"
Task 7: Backend API Endpoints for Highlights
Files:
- Create:
/root/biblical-guide/app/api/highlights/route.ts - Create:
/root/biblical-guide/app/api/highlights/bulk/route.ts - Create:
/root/biblical-guide/app/api/highlights/all/route.ts - Create:
/root/biblical-guide/app/api/bible/cross-references/route.ts
Step 1: Create POST /api/highlights
Create /root/biblical-guide/app/api/highlights/route.ts:
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/db'
import { getAuth } from '@clerk/nextjs/server'
export const runtime = 'nodejs'
export async function POST(request: Request) {
try {
const { userId } = await getAuth(request)
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const { verseId, color } = body
if (!verseId || !['yellow', 'orange', 'pink', 'blue'].includes(color)) {
return NextResponse.json({ error: 'Invalid input' }, { status: 400 })
}
const highlight = await prisma.userHighlight.create({
data: {
userId,
verseId,
color,
createdAt: new Date(),
updatedAt: new Date()
}
})
return NextResponse.json({
id: highlight.id,
verseId: highlight.verseId,
color: highlight.color,
createdAt: highlight.createdAt.getTime(),
updatedAt: highlight.updatedAt.getTime(),
syncStatus: 'synced'
})
} catch (error) {
console.error('Error creating highlight:', error)
return NextResponse.json(
{ error: 'Failed to create highlight' },
{ status: 500 }
)
}
}
Step 2: Create POST /api/highlights/bulk
Create /root/biblical-guide/app/api/highlights/bulk/route.ts:
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/db'
import { getAuth } from '@clerk/nextjs/server'
export const runtime = 'nodejs'
export async function POST(request: Request) {
try {
const { userId } = await getAuth(request)
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const { highlights } = body
if (!Array.isArray(highlights)) {
return NextResponse.json({ error: 'Invalid input' }, { status: 400 })
}
const synced = []
const errors = []
for (const item of highlights) {
try {
const existing = await prisma.userHighlight.findFirst({
where: {
userId,
verseId: item.verseId
}
})
if (existing) {
await prisma.userHighlight.update({
where: { id: existing.id },
data: {
color: item.color,
updatedAt: new Date()
}
})
} else {
await prisma.userHighlight.create({
data: {
userId,
verseId: item.verseId,
color: item.color,
createdAt: new Date(),
updatedAt: new Date()
}
})
}
synced.push(item.verseId)
} catch (e) {
errors.push({
verseId: item.verseId,
error: 'Failed to sync'
})
}
}
return NextResponse.json({
synced: synced.length,
errors,
serverTime: Date.now()
})
} catch (error) {
console.error('Error bulk syncing highlights:', error)
return NextResponse.json(
{ error: 'Failed to sync highlights' },
{ status: 500 }
)
}
}
Step 3: Create GET /api/highlights/all
Create /root/biblical-guide/app/api/highlights/all/route.ts:
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/db'
import { getAuth } from '@clerk/nextjs/server'
export const runtime = 'nodejs'
export async function GET(request: Request) {
try {
const { userId } = await getAuth(request)
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const highlights = await prisma.userHighlight.findMany({
where: { userId },
select: {
id: true,
verseId: true,
color: true,
createdAt: true,
updatedAt: true
}
})
return NextResponse.json({
highlights: highlights.map(h => ({
id: h.id,
verseId: h.verseId,
color: h.color,
createdAt: h.createdAt.getTime(),
updatedAt: h.updatedAt.getTime()
})),
serverTime: Date.now()
})
} catch (error) {
console.error('Error fetching highlights:', error)
return NextResponse.json(
{ error: 'Failed to fetch highlights' },
{ status: 500 }
)
}
}
Step 4: Create GET /api/bible/cross-references
Create /root/biblical-guide/app/api/bible/cross-references/route.ts:
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/db'
export const runtime = 'nodejs'
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url)
const verseId = searchParams.get('verseId')
if (!verseId) {
return NextResponse.json(
{ error: 'verseId parameter required' },
{ status: 400 }
)
}
// For now, return empty cross-references
// TODO: Implement actual cross-reference lookup in Phase 2.1B
// This would require a cross_references table mapping verses to related verses
return NextResponse.json({
verseId,
references: []
})
} catch (error) {
console.error('Error fetching cross-references:', error)
return NextResponse.json(
{ error: 'Failed to fetch cross-references' },
{ status: 500 }
)
}
}
Step 5: Commit
git add app/api/highlights/ app/api/bible/cross-references/route.ts
git commit -m "feat: add backend API endpoints for highlights and cross-references"
Task 8: Add Database Schema for Highlights
Files:
- Create/Modify:
prisma/schema.prisma
Step 1: Add UserHighlight model to schema
Add to /root/biblical-guide/prisma/schema.prisma:
model UserHighlight {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
verseId String
color String @default("yellow") // yellow, orange, pink, blue
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, verseId])
@@index([userId])
@@index([verseId])
}
Also add relation to User model:
model User {
// ... existing fields
highlights UserHighlight[]
}
Step 2: Create and run migration
npx prisma migrate dev --name add_highlights
Expected output: Migration created and applied successfully
Step 3: Commit
git add prisma/schema.prisma prisma/migrations/
git commit -m "feat: add UserHighlight model to database schema"
Task 9: Update Imports and Build Verification
Files:
- Modify:
/root/biblical-guide/lib/highlight-manager.ts- add getAllHighlights export if missing - Run full build
Step 1: Verify all imports are correct
Ensure the following are exported from each module:
From /root/biblical-guide/lib/highlight-manager.ts:
export {
initHighlightsDatabase,
addHighlight,
updateHighlight,
getHighlight,
getHighlightsByVerse,
getAllHighlights,
getPendingHighlights,
deleteHighlight,
clearAllHighlights
}
Step 2: Run build
npm run build 2>&1 | tail -50
Expected output: Build completed successfully with no type errors
Step 3: Commit
git add -A
git commit -m "build: complete Phase 2.1 implementation and verify build"
Summary of Implementation
This plan implements:
✅ Types - TypeScript definitions for highlights, colors, sync status ✅ Storage - IndexedDB-based storage with sync queue ✅ Sync Manager - Background sync service with queue management ✅ UI Components - HighlightsTab for color selection and management ✅ Reader Integration - Highlight state management in BibleReaderApp ✅ Visual Feedback - Colored backgrounds on highlighted verses ✅ Backend APIs - Four new endpoints for highlight CRUD and cross-references ✅ Database - UserHighlight model with proper relations ✅ Testing - Unit tests for all components and services
Total effort: ~8-10 hours for experienced developer with zero context
Next Phase (2.1B):
- Implement actual sync POST calls in performSync()
- Add cross-device highlight merge logic
- Implement cross-references lookup in backend
- Add sync status indicator UI
- Full E2E testing