Files
biblical-guide.com/docs/plans/2025-01-11-phase-2-rich-annotations-implementation.md

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