# Parallel Bible View - Implementation Plan ## 📋 Overview Implement a side-by-side Bible reading experience allowing users to compare multiple translations simultaneously, perfect for Bible study, translation verification, and deep Scripture analysis. **Status:** Planning Phase **Priority:** 🔴 High **Estimated Time:** 2 weeks (80 hours) **Target Completion:** TBD --- ## 🎯 Goals & Objectives ### Primary Goals 1. Enable simultaneous viewing of 2-3 Bible translations 2. Provide synchronized scrolling across all panes 3. Allow easy switching between versions 4. Maintain responsive design for mobile devices 5. Support independent highlighting per version ### User Value Proposition - **For Bible students**: Compare translations to understand nuances - **For scholars**: Analyze textual differences - **For language learners**: See original and translated text - **For teachers**: Prepare lessons with multiple versions - **For translators**: Verify accuracy against source texts --- ## ✨ Feature Specifications ### 1. Layout Configurations ```typescript type PaneLayout = '1-pane' | '2-pane-horizontal' | '2-pane-vertical' | '3-pane' | '4-pane' interface LayoutConfig { layout: PaneLayout panes: PaneConfig[] syncScroll: boolean syncChapter: boolean // All panes show same chapter equalWidths: boolean showDividers: boolean compactMode: boolean // Reduce padding on mobile } interface PaneConfig { id: string versionId: string visible: boolean width: number // percentage (for horizontal layouts) locked: boolean // Prevent accidental changes customSettings?: { fontSize?: number theme?: 'light' | 'dark' | 'sepia' showVerseNumbers?: boolean } } ``` ### 2. Visual Layouts #### Desktop Layouts ``` 2-Pane Horizontal: ┌─────────────────┬─────────────────┐ │ KJV │ ESV │ │ │ │ │ Genesis 1:1 │ Genesis 1:1 │ │ In the │ In the │ │ beginning... │ beginning... │ │ │ │ └─────────────────┴─────────────────┘ 3-Pane: ┌───────┬───────┬───────┐ │ KJV │ ESV │ NIV │ │ │ │ │ │ Gen 1 │ Gen 1 │ Gen 1 │ │ │ │ │ └───────┴───────┴───────┘ 2-Pane Vertical (Stacked): ┌─────────────────────────┐ │ KJV - Genesis 1 │ │ │ │ 1 In the beginning... │ └─────────────────────────┘ ┌─────────────────────────┐ │ ESV - Genesis 1 │ │ │ │ 1 In the beginning... │ └─────────────────────────┘ ``` #### Mobile Layout ``` Mobile (Stacked with Tabs): ┌─────────────────────────┐ │ [KJV] [ESV] [NIV] [+] │ ← Tab bar ├─────────────────────────┤ │ Genesis 1:1-31 │ │ │ │ 1 In the beginning... │ │ 2 And the earth... │ │ │ │ ▼ Swipe to compare ▼ │ └─────────────────────────┘ ``` ### 3. Synchronized Scrolling ```typescript interface ScrollSyncConfig { enabled: boolean mode: 'verse' | 'pixel' | 'paragraph' leadPane: string | 'any' // Which pane controls scroll smoothness: number // 0-1, animation easing threshold: number // Minimum scroll delta to trigger sync } class ScrollSynchronizer { private panes: HTMLElement[] private isScrolling: boolean = false private scrollTimeout: NodeJS.Timeout | null = null constructor(private config: ScrollSyncConfig) {} syncScroll(sourcePane: HTMLElement, scrollTop: number): void { if (this.isScrolling) return this.isScrolling = true switch (this.config.mode) { case 'verse': this.syncByVerse(sourcePane, scrollTop) break case 'pixel': this.syncByPixel(sourcePane, scrollTop) break case 'paragraph': this.syncByParagraph(sourcePane, scrollTop) break } // Reset scrolling flag after brief delay clearTimeout(this.scrollTimeout) this.scrollTimeout = setTimeout(() => { this.isScrolling = false }, 100) } private syncByVerse(sourcePane: HTMLElement, scrollTop: number): void { // Find which verse is at the top of source pane const visibleVerse = this.getVisibleVerseNumber(sourcePane, scrollTop) // Scroll other panes to show the same verse at top this.panes.forEach(pane => { if (pane === sourcePane) return const targetVerse = pane.querySelector(`[data-verse="${visibleVerse}"]`) if (targetVerse) { pane.scrollTo({ top: (targetVerse as HTMLElement).offsetTop - 100, behavior: 'smooth' }) } }) } private syncByPixel(sourcePane: HTMLElement, scrollTop: number): void { // Calculate scroll percentage const scrollHeight = sourcePane.scrollHeight - sourcePane.clientHeight const scrollPercent = scrollTop / scrollHeight // Apply same percentage to other panes this.panes.forEach(pane => { if (pane === sourcePane) return const targetScrollHeight = pane.scrollHeight - pane.clientHeight const targetScrollTop = targetScrollHeight * scrollPercent pane.scrollTo({ top: targetScrollTop, behavior: 'smooth' }) }) } private getVisibleVerseNumber(pane: HTMLElement, scrollTop: number): number { const verses = Array.from(pane.querySelectorAll('[data-verse]')) const viewportTop = scrollTop + 100 // Offset for header for (const verse of verses) { const verseTop = (verse as HTMLElement).offsetTop if (verseTop >= viewportTop) { return parseInt(verse.getAttribute('data-verse') || '1') } } return 1 } } ``` ### 4. Version Selector Per Pane ```typescript interface VersionSelectorProps { paneId: string currentVersionId: string onVersionChange: (versionId: string) => void position: 'top' | 'bottom' compact?: boolean } const VersionSelector: React.FC = ({ paneId, currentVersionId, onVersionChange, position, compact = false }) => { const [versions, setVersions] = useState([]) const [search, setSearch] = useState('') useEffect(() => { // Load available versions fetch('/api/bible/versions') .then(r => r.json()) .then(data => setVersions(data.versions)) }, []) const filteredVersions = versions.filter(v => v.name.toLowerCase().includes(search.toLowerCase()) || v.abbreviation.toLowerCase().includes(search.toLowerCase()) ) return ( ) } ``` ### 5. Verse Alignment Highlighting ```typescript interface AlignmentConfig { enabled: boolean highlightMode: 'hover' | 'focus' | 'always' | 'none' color: string showConnectors: boolean // Lines between aligned verses } // Highlight same verse across all panes const VerseAlignmentHighlighter: React.FC = () => { const { panes, alignmentConfig } = useParallelView() const [hoveredVerse, setHoveredVerse] = useState(null) useEffect(() => { if (!alignmentConfig.enabled || alignmentConfig.highlightMode === 'none') { return } const handleVerseHover = (e: MouseEvent) => { const verseElement = (e.target as HTMLElement).closest('[data-verse]') if (verseElement) { const verseNum = parseInt(verseElement.getAttribute('data-verse') || '0') setHoveredVerse(verseNum) } else { setHoveredVerse(null) } } document.addEventListener('mouseover', handleVerseHover) return () => document.removeEventListener('mouseover', handleVerseHover) }, [alignmentConfig]) useEffect(() => { if (hoveredVerse === null) { // Remove all highlights document.querySelectorAll('.verse-aligned').forEach(el => { el.classList.remove('verse-aligned') }) return } // Highlight verse in all panes panes.forEach(pane => { const verseElements = document.querySelectorAll( `#pane-${pane.id} [data-verse="${hoveredVerse}"]` ) verseElements.forEach(el => el.classList.add('verse-aligned')) }) }, [hoveredVerse, panes]) return null } // CSS .verse-aligned { background-color: rgba(var(--primary-rgb), 0.1); border-left: 3px solid var(--primary-color); padding-left: 8px; margin-left: -11px; transition: all 0.2s ease; } ``` ### 6. Diff View for Text Differences ```typescript interface DiffConfig { enabled: boolean compareAgainst: string // Pane ID to use as reference diffMode: 'word' | 'phrase' | 'verse' highlightStyle: 'color' | 'underline' | 'background' | 'strikethrough' showSimilarity: boolean // Show % similarity score } // Simple word-level diff function calculateDiff(text1: string, text2: string): DiffResult[] { const words1 = text1.split(/\s+/) const words2 = text2.split(/\s+/) const diff: DiffResult[] = [] // Simple longest common subsequence approach let i = 0, j = 0 while (i < words1.length || j < words2.length) { if (words1[i] === words2[j]) { diff.push({ type: 'same', text: words1[i] }) i++ j++ } else { // Check if word exists ahead const indexInText2 = words2.slice(j).indexOf(words1[i]) const indexInText1 = words1.slice(i).indexOf(words2[j]) if (indexInText2 !== -1 && (indexInText1 === -1 || indexInText2 < indexInText1)) { // Word missing in text1 diff.push({ type: 'added', text: words2[j] }) j++ } else if (indexInText1 !== -1) { // Word missing in text2 diff.push({ type: 'removed', text: words1[i] }) i++ } else { // Different words diff.push({ type: 'changed', text1: words1[i], text2: words2[j] }) i++ j++ } } } return diff } interface DiffResult { type: 'same' | 'added' | 'removed' | 'changed' text?: string text1?: string text2?: string } // Component to render diff const DiffHighlightedVerse: React.FC<{ verseText: string referenceText: string config: DiffConfig }> = ({ verseText, referenceText, config }) => { if (!config.enabled) { return {verseText} } const diff = calculateDiff(referenceText, verseText) return ( {diff.map((part, index) => { if (part.type === 'same') { return {part.text} } else if (part.type === 'added') { return ( {part.text}{' '} ) } else if (part.type === 'removed') { return ( {part.text}{' '} ) } else if (part.type === 'changed') { return ( {part.text2}{' '} ) } })} ) } ``` ### 7. Quick Swap Versions ```typescript // Allow swapping versions between panes const SwapVersionsButton: React.FC<{ pane1Id: string pane2Id: string }> = ({ pane1Id, pane2Id }) => { const { panes, updatePane } = useParallelView() const handleSwap = () => { const pane1 = panes.find(p => p.id === pane1Id) const pane2 = panes.find(p => p.id === pane2Id) if (pane1 && pane2) { updatePane(pane1Id, { versionId: pane2.versionId }) updatePane(pane2Id, { versionId: pane1.versionId }) } } return ( ) } ``` ### 8. Column Width Adjustment ```typescript interface ResizablePane { id: string minWidth: number // percentage maxWidth: number currentWidth: number } // Draggable divider between panes const PaneDivider: React.FC<{ leftPaneId: string rightPaneId: string }> = ({ leftPaneId, rightPaneId }) => { const { updatePane } = useParallelView() const [isDragging, setIsDragging] = useState(false) const [startX, setStartX] = useState(0) const [startWidths, setStartWidths] = useState<[number, number]>([50, 50]) const handleMouseDown = (e: React.MouseEvent) => { setIsDragging(true) setStartX(e.clientX) const leftPane = document.getElementById(`pane-${leftPaneId}`) const rightPane = document.getElementById(`pane-${rightPaneId}`) if (leftPane && rightPane) { const leftWidth = (leftPane.offsetWidth / leftPane.parentElement!.offsetWidth) * 100 const rightWidth = (rightPane.offsetWidth / rightPane.parentElement!.offsetWidth) * 100 setStartWidths([leftWidth, rightWidth]) } } useEffect(() => { if (!isDragging) return const handleMouseMove = (e: MouseEvent) => { const deltaX = e.clientX - startX const container = document.querySelector('.parallel-view-container') if (!container) return const deltaPercent = (deltaX / container.clientWidth) * 100 const newLeftWidth = Math.max(20, Math.min(80, startWidths[0] + deltaPercent)) const newRightWidth = Math.max(20, Math.min(80, startWidths[1] - deltaPercent)) updatePane(leftPaneId, { width: newLeftWidth }) updatePane(rightPaneId, { width: newRightWidth }) } const handleMouseUp = () => { setIsDragging(false) } document.addEventListener('mousemove', handleMouseMove) document.addEventListener('mouseup', handleMouseUp) return () => { document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mouseup', handleMouseUp) } }, [isDragging, startX, startWidths]) return ( ) } ``` ### 9. Independent Highlighting Per Version ```typescript // Each pane maintains its own highlights interface PaneHighlights { paneId: string highlights: Highlight[] } // Store highlights per version in database model Highlight { id String @id @default(cuid()) userId String versionId String // Link to specific Bible version book String chapter Int verse Int color String note String? createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) version BibleVersion @relation(fields: [versionId], references: [id]) @@index([userId, versionId, book, chapter]) } // Load highlights per pane const loadPaneHighlights = async ( paneId: string, versionId: string, book: string, chapter: number ): Promise => { const response = await fetch( `/api/highlights?versionId=${versionId}&book=${book}&chapter=${chapter}` ) return response.json() } ``` --- ## 🏗️ Technical Implementation ### File Structure ``` /components/bible-reader/ ├── parallel-view/ │ ├── ParallelViewProvider.tsx # Context provider │ ├── ParallelViewContainer.tsx # Main container │ ├── Pane.tsx # Individual pane │ ├── PaneDivider.tsx # Resizable divider │ ├── VersionSelector.tsx # Version picker per pane │ ├── LayoutSelector.tsx # Layout switcher │ ├── ScrollSynchronizer.tsx # Scroll sync logic │ ├── VerseAlignmentHighlighter.tsx # Verse highlighting │ ├── DiffView.tsx # Text difference view │ ├── SwapControl.tsx # Version swapping │ └── hooks/ │ ├── useParallelView.ts # Main hook │ ├── useScrollSync.ts # Scroll synchronization │ ├── usePaneResize.ts # Resize logic │ └── useVerseAlignment.ts # Alignment logic └── reader.tsx # Updated main reader ``` ### Context Provider ```typescript // ParallelViewProvider.tsx interface ParallelViewContextType { // State enabled: boolean layout: LayoutConfig panes: PaneConfig[] scrollSync: ScrollSyncConfig alignmentConfig: AlignmentConfig diffConfig: DiffConfig // Actions toggleParallelView: () => void addPane: (config: Partial) => void removePane: (paneId: string) => void updatePane: (paneId: string, updates: Partial) => void setLayout: (layout: PaneLayout) => void updateScrollSync: (config: Partial) => void swapVersions: (paneId1: string, paneId2: string) => void } export const ParallelViewProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [enabled, setEnabled] = useState(false) const [layout, setLayoutState] = useState(defaultLayout) const [panes, setPanes] = useState([]) const [scrollSync, setScrollSync] = useState(defaultScrollSync) // Load from localStorage useEffect(() => { const saved = localStorage.getItem('parallel-view-config') if (saved) { const config = JSON.parse(saved) setEnabled(config.enabled) setLayoutState(config.layout) setPanes(config.panes) setScrollSync(config.scrollSync) } else { // Initialize with default 2-pane view const defaultPanes = [ { id: 'pane-1', versionId: 'kjv', visible: true, width: 50, locked: false }, { id: 'pane-2', versionId: 'esv', visible: true, width: 50, locked: false } ] setPanes(defaultPanes) } }, []) // Save to localStorage useEffect(() => { localStorage.setItem('parallel-view-config', JSON.stringify({ enabled, layout, panes, scrollSync })) }, [enabled, layout, panes, scrollSync]) const addPane = (config: Partial) => { const newPane: PaneConfig = { id: `pane-${Date.now()}`, versionId: config.versionId || 'kjv', visible: true, width: 100 / (panes.length + 1), locked: false, ...config } // Adjust existing pane widths const adjustedPanes = panes.map(p => ({ ...p, width: p.width * (panes.length / (panes.length + 1)) })) setPanes([...adjustedPanes, newPane]) } const removePane = (paneId: string) => { const updatedPanes = panes.filter(p => p.id !== paneId) // Redistribute widths const equalWidth = 100 / updatedPanes.length setPanes(updatedPanes.map(p => ({ ...p, width: equalWidth }))) } const updatePane = (paneId: string, updates: Partial) => { setPanes(panes.map(p => p.id === paneId ? { ...p, ...updates } : p )) } const swapVersions = (paneId1: string, paneId2: string) => { const pane1 = panes.find(p => p.id === paneId1) const pane2 = panes.find(p => p.id === paneId2) if (pane1 && pane2) { updatePane(paneId1, { versionId: pane2.versionId }) updatePane(paneId2, { versionId: pane1.versionId }) } } return ( setEnabled(!enabled), addPane, removePane, updatePane, setLayout: setLayoutState, updateScrollSync: (config) => setScrollSync({ ...scrollSync, ...config }), swapVersions }}> {children} ) } ``` ### Main Container Component ```typescript // ParallelViewContainer.tsx export const ParallelViewContainer: React.FC = () => { const { enabled, layout, panes, scrollSync } = useParallelView() const scrollSynchronizer = useRef(new ScrollSynchronizer(scrollSync)) if (!enabled || panes.length === 0) { return null } const visiblePanes = panes.filter(p => p.visible) const getGridTemplate = () => { switch (layout.layout) { case '2-pane-horizontal': return 'repeat(2, 1fr)' case '3-pane': return 'repeat(3, 1fr)' case '4-pane': return 'repeat(2, 1fr)' default: return '1fr' } } return ( {visiblePanes.map((pane, index) => ( { if (scrollSync.enabled) { scrollSynchronizer.current.syncScroll( document.getElementById(`pane-${pane.id}`)!, scrollTop ) } }} /> {layout.showDividers && index < visiblePanes.length - 1 && ( )} ))} ) } ``` --- ## 💾 Data Persistence ### LocalStorage Schema ```typescript interface ParallelViewStorage { version: number enabled: boolean layout: LayoutConfig panes: PaneConfig[] scrollSync: ScrollSyncConfig alignmentConfig: AlignmentConfig diffConfig: DiffConfig recentVersionCombinations: string[][] // Track popular combos } // Key: 'bible-reader:parallel-view' ``` ### User Preferences API ```typescript // Add to UserPreference model model UserPreference { // ... existing fields parallelViewConfig Json? favoriteVersionCombinations Json? // [["kjv", "esv"], ["niv", "msg"]] } // API endpoint POST /api/user/preferences/parallel-view Body: ParallelViewStorage ``` --- ## 📅 Implementation Timeline ### Week 1: Core Functionality **Day 1-2: Foundation** - [ ] Create context provider - [ ] Build basic 2-pane layout - [ ] Implement version selector per pane - [ ] Add layout switcher (1/2/3 panes) **Day 3-4: Scroll Sync** - [ ] Implement scroll synchronizer - [ ] Add verse-based sync - [ ] Add pixel-based sync - [ ] Test smooth scrolling **Day 5: Resizing & Controls** - [ ] Build resizable dividers - [ ] Add width adjustment - [ ] Implement swap versions - [ ] Test on different screen sizes **Deliverable:** Working parallel view with basic features ### Week 2: Advanced Features & Polish **Day 1-2: Alignment & Diff** - [ ] Implement verse alignment highlighting - [ ] Build diff view - [ ] Add similarity calculations - [ ] Test with various translations **Day 3-4: Mobile & Responsive** - [ ] Design mobile layout (tabs) - [ ] Implement swipe navigation - [ ] Optimize for tablets - [ ] Test touch gestures **Day 5: Polish & Testing** - [ ] Independent highlighting per pane - [ ] Performance optimization - [ ] Bug fixes - [ ] Documentation **Deliverable:** Production-ready parallel Bible view --- ## 🚀 Deployment Plan ### Pre-Launch Checklist - [ ] All layouts tested (2/3/4 pane) - [ ] Scroll sync working smoothly - [ ] Mobile responsive design complete - [ ] Performance benchmarks met (<100ms lag) - [ ] Accessibility audit passed - [ ] Cross-browser testing complete - [ ] User documentation created ### Rollout Strategy 1. **Beta (Week 1)**: 10% of users, 2-pane only 2. **Staged (Week 2)**: 50% of users, all layouts 3. **Full (Week 3)**: 100% of users --- ## 📝 Notes & Considerations ### Performance - Use virtual scrolling for long chapters - Debounce scroll sync (avoid jank) - Lazy load panes not in viewport - Cache rendered verses - Monitor memory usage with multiple panes ### Accessibility - Maintain keyboard navigation across panes - Screen reader support for pane switching - Focus management between panes - ARIA labels for all controls --- **Document Version:** 1.0 **Last Updated:** 2025-10-13 **Owner:** Development Team **Status:** Ready for Implementation