1426 lines
38 KiB
Markdown
1426 lines
38 KiB
Markdown
# 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`:
|
|
|
|
```typescript
|
|
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**
|
|
|
|
```bash
|
|
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:
|
|
|
|
```typescript
|
|
// 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**
|
|
|
|
```bash
|
|
npm test -- __tests__/types/highlights.test.ts
|
|
```
|
|
|
|
Expected output: PASS - all 3 tests pass
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```typescript
|
|
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**
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```typescript
|
|
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**
|
|
|
|
```bash
|
|
npm test -- __tests__/lib/highlight-manager.test.ts
|
|
```
|
|
|
|
Expected output: PASS - all 5 tests pass
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```typescript
|
|
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**
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```typescript
|
|
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**
|
|
|
|
```bash
|
|
npm test -- __tests__/lib/highlight-sync-manager.test.ts
|
|
```
|
|
|
|
Expected output: PASS - all 5 tests pass
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```typescript
|
|
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**
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```typescript
|
|
'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**
|
|
|
|
```bash
|
|
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:
|
|
|
|
```typescript
|
|
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**
|
|
|
|
```bash
|
|
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:
|
|
|
|
```typescript
|
|
// 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**
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```typescript
|
|
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`:
|
|
|
|
```typescript
|
|
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**
|
|
|
|
```typescript
|
|
<VersDetailsPanel
|
|
// ... existing props
|
|
isHighlighted={highlights.has(selectedVerse?.id || '')}
|
|
currentHighlightColor={highlights.get(selectedVerse?.id || '')?.color}
|
|
onHighlightVerse={handleHighlightVerse}
|
|
onChangeHighlightColor={handleChangeHighlightColor}
|
|
onRemoveHighlight={handleRemoveHighlight}
|
|
/>
|
|
```
|
|
|
|
**Step 4: Commit**
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```typescript
|
|
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`:
|
|
|
|
```typescript
|
|
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`:
|
|
|
|
```typescript
|
|
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`:
|
|
|
|
```typescript
|
|
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**
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```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:
|
|
|
|
```prisma
|
|
model User {
|
|
// ... existing fields
|
|
highlights UserHighlight[]
|
|
}
|
|
```
|
|
|
|
**Step 2: Create and run migration**
|
|
|
|
```bash
|
|
npx prisma migrate dev --name add_highlights
|
|
```
|
|
|
|
Expected output: Migration created and applied successfully
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
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`:
|
|
```typescript
|
|
export {
|
|
initHighlightsDatabase,
|
|
addHighlight,
|
|
updateHighlight,
|
|
getHighlight,
|
|
getHighlightsByVerse,
|
|
getAllHighlights,
|
|
getPendingHighlights,
|
|
deleteHighlight,
|
|
clearAllHighlights
|
|
}
|
|
```
|
|
|
|
**Step 2: Run build**
|
|
|
|
```bash
|
|
npm run build 2>&1 | tail -50
|
|
```
|
|
|
|
Expected output: Build completed successfully with no type errors
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
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
|
|
|
|
---
|