From aefe54751b7feca162492287480a026b8591f87f Mon Sep 17 00:00:00 2001 From: Andrei Date: Tue, 11 Nov 2025 20:12:41 +0000 Subject: [PATCH] feat: integrate all Bible reader 2025 components into main app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This completes Task 5 of the Bible Reader 2025 implementation plan, integrating all previously built components into a cohesive reading experience. Components added: - BibleReaderApp: Main orchestrator component with state management - ReadingSettings: Settings panel with presets and customization options Key features: - Chapter navigation with prev/next controls - SearchNavigator integration for book/chapter lookup - ReadingView with customizable reading preferences - VersDetailsPanel for verse interactions (notes, bookmarks) - ReadingSettings panel with 4 presets and custom controls - IndexedDB caching for offline chapter access - Mobile-responsive bottom sheet and desktop sidebar layouts The app now provides: - Bookmark management (client-side Set for now, backend sync in Phase 2) - Note taking (console logging for now, persistence in Phase 2) - Font customization (4 font families including dyslexia-friendly) - Size and spacing controls (font size 12-32px, line height 1.4-2.2x) - Background themes (warm, white, light gray, dark) - Preset modes (default, dyslexia, high contrast, minimal) Technical implementation: - State management via React hooks (useState, useEffect) - Cache-first loading strategy with API fallback - Storage events for cross-component preference updates - TypeScript with proper type annotations - Material-UI components for consistent styling Next steps (Phase 2): - Backend persistence for bookmarks and notes - Sync annotations across devices - Highlight system with color selection - Cross-references integration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/[locale]/bible/page.tsx | 68 +--------- components/bible/bible-reader-app.tsx | 144 ++++++++++++++++++++ components/bible/reading-settings.tsx | 182 ++++++++++++++++++++++++++ 3 files changed, 332 insertions(+), 62 deletions(-) create mode 100644 components/bible/bible-reader-app.tsx create mode 100644 components/bible/reading-settings.tsx diff --git a/app/[locale]/bible/page.tsx b/app/[locale]/bible/page.tsx index 1dd257b..2465153 100644 --- a/app/[locale]/bible/page.tsx +++ b/app/[locale]/bible/page.tsx @@ -1,66 +1,10 @@ -import { Suspense } from 'react' -import { redirect } from 'next/navigation' -import BibleReader from './reader' -import { prisma } from '@/lib/db' +import { BibleReaderApp } from '@/components/bible/bible-reader-app' -interface PageProps { - searchParams: Promise<{ - version?: string - book?: string - chapter?: string - verse?: string - }> - params: Promise<{ - locale: string - }> +export const metadata = { + title: 'Read Bible', + description: 'Modern Bible reader with offline support' } -// Helper function to convert UUIDs to SEO-friendly slugs -async function convertToSeoUrl(versionId: string, bookId: string, chapter: string, locale: string) { - try { - const version = await prisma.bibleVersion.findUnique({ - where: { id: versionId } - }) - - const book = await prisma.bibleBook.findUnique({ - where: { id: bookId } - }) - - if (version && book) { - const versionSlug = version.abbreviation.toLowerCase() - const bookSlug = book.bookKey.toLowerCase() - return `/${locale}/bible/${versionSlug}/${bookSlug}/${chapter}` - } - } catch (error) { - console.error('Error converting to SEO URL:', error) - } - return null +export default function BiblePage() { + return } - -export default async function BiblePage({ searchParams, params }: PageProps) { - const { version, book, chapter } = await searchParams - const { locale } = await params - - // If we have the old URL format with UUIDs, redirect to SEO-friendly URL - if (version && book && chapter) { - const seoUrl = await convertToSeoUrl(version, book, chapter, locale) - if (seoUrl) { - redirect(seoUrl) - } - } - - return ( - - Loading Bible reader... - - }> - - - ) -} \ No newline at end of file diff --git a/components/bible/bible-reader-app.tsx b/components/bible/bible-reader-app.tsx new file mode 100644 index 0000000..59b6430 --- /dev/null +++ b/components/bible/bible-reader-app.tsx @@ -0,0 +1,144 @@ +'use client' +import { useState, useEffect } from 'react' +import { Box } from '@mui/material' +import { BibleChapter, BibleVerse } from '@/types' +import { getCachedChapter, cacheChapter } from '@/lib/cache-manager' +import { SearchNavigator } from './search-navigator' +import { ReadingView } from './reading-view' +import { VersDetailsPanel } from './verse-details-panel' +import { ReadingSettings } from './reading-settings' + +export function BibleReaderApp() { + const [bookId, setBookId] = useState(1) // Genesis + const [chapter, setChapter] = useState(1) + const [currentChapter, setCurrentChapter] = useState(null) + const [selectedVerse, setSelectedVerse] = useState(null) + const [detailsPanelOpen, setDetailsPanelOpen] = useState(false) + const [settingsOpen, setSettingsOpen] = useState(false) + const [loading, setLoading] = useState(false) + const [bookmarks, setBookmarks] = useState>(new Set()) + + // Load chapter on navigation + useEffect(() => { + loadChapter(bookId, chapter) + }, [bookId, chapter]) + + async function loadChapter(bookId: number, chapterNum: number) { + setLoading(true) + try { + // Try cache first + const chapterId = `${bookId}-${chapterNum}` + let data = await getCachedChapter(chapterId) + + // If not cached, fetch from API + if (!data) { + const response = await fetch(`/api/bible/chapter?book=${bookId}&chapter=${chapterNum}`) + if (response.ok) { + const json = await response.json() + data = json.chapter + + // Cache it + if (data) { + data.id = chapterId + await cacheChapter(data) + } + } else { + console.error('Failed to load chapter:', response.status) + } + } + + setCurrentChapter(data) + } catch (error) { + console.error('Error loading chapter:', error) + } finally { + setLoading(false) + } + } + + const handleVerseClick = (verseId: string) => { + const verse = currentChapter?.verses.find(v => v.id === verseId) + if (verse) { + setSelectedVerse(verse) + setDetailsPanelOpen(true) + } + } + + const handleToggleBookmark = () => { + if (!selectedVerse) return + const newBookmarks = new Set(bookmarks) + if (newBookmarks.has(selectedVerse.id)) { + newBookmarks.delete(selectedVerse.id) + } else { + newBookmarks.add(selectedVerse.id) + } + setBookmarks(newBookmarks) + // TODO: Sync to backend in Phase 2 + console.log('Bookmarks updated:', Array.from(newBookmarks)) + } + + const handleAddNote = (note: string) => { + if (!selectedVerse) return + // TODO: Save note to backend in Phase 2 + console.log(`Note for verse ${selectedVerse.id}:`, note) + } + + return ( + + {/* Header with search */} + + { + setBookId(newBookId) + setChapter(newChapter) + }} + /> + + + {/* Reading area */} + + {currentChapter ? ( + chapter > 1 && setChapter(chapter - 1)} + onNextChapter={() => setChapter(chapter + 1)} + onVerseClick={handleVerseClick} + onSettingsOpen={() => setSettingsOpen(true)} + hasPrevChapter={chapter > 1} + hasNextChapter={true} // TODO: Check actual chapter count based on book + /> + ) : ( + + {loading ? 'Loading Bible reader...' : 'Failed to load chapter. Please try again.'} + + )} + + + {/* Details panel */} + setDetailsPanelOpen(false)} + isBookmarked={selectedVerse ? bookmarks.has(selectedVerse.id) : false} + onToggleBookmark={handleToggleBookmark} + onAddNote={handleAddNote} + /> + + {/* Settings panel */} + {settingsOpen && ( + setSettingsOpen(false)} /> + )} + + ) +} diff --git a/components/bible/reading-settings.tsx b/components/bible/reading-settings.tsx new file mode 100644 index 0000000..e2a360e --- /dev/null +++ b/components/bible/reading-settings.tsx @@ -0,0 +1,182 @@ +'use client' +import { useState, useEffect } from 'react' +import { Box, Paper, Typography, Button, Slider, FormControl, InputLabel, Select, MenuItem, useMediaQuery, useTheme, IconButton } from '@mui/material' +import { Close } from '@mui/icons-material' +import { ReadingPreference } from '@/types' +import { getPreset, loadPreferences, savePreferences } from '@/lib/reading-preferences' + +const FONTS = [ + { value: 'georgia', label: 'Georgia (Serif)' }, + { value: 'merriweather', label: 'Merriweather (Serif)' }, + { value: 'inter', label: 'Inter (Sans)' }, + { value: 'atkinson', label: 'Atkinson (Dyslexia-friendly)' }, +] + +interface ReadingSettingsProps { + onClose: () => void +} + +export function ReadingSettings({ onClose }: ReadingSettingsProps) { + const theme = useTheme() + const isMobile = useMediaQuery(theme.breakpoints.down('sm')) + const [preferences, setPreferences] = useState(loadPreferences()) + + // Reload preferences on mount + useEffect(() => { + setPreferences(loadPreferences()) + }, []) + + const applyPreset = (presetName: string) => { + const preset = getPreset(presetName as any) + setPreferences(preset) + savePreferences(preset) + // Trigger a storage event to notify other components + window.dispatchEvent(new Event('storage')) + } + + const handleChange = (key: keyof ReadingPreference, value: any) => { + const updated: ReadingPreference = { + ...preferences, + [key]: value, + preset: 'custom' as const + } + setPreferences(updated) + savePreferences(updated) + // Trigger a storage event to notify other components + window.dispatchEvent(new Event('storage')) + } + + const content = ( + + + Reading Settings + + + + + + {/* Presets */} + + Presets + + {['default', 'dyslexia', 'highContrast', 'minimal'].map((preset) => ( + + ))} + + + + {/* Font */} + + Font + + + + {/* Font Size */} + + Size: {preferences.fontSize}px + handleChange('fontSize', value)} + min={12} + max={32} + step={1} + marks={[ + { value: 12, label: '12' }, + { value: 22, label: '22' }, + { value: 32, label: '32' }, + ]} + /> + + + {/* Line Height */} + + Line Height: {preferences.lineHeight.toFixed(1)}x + handleChange('lineHeight', value)} + min={1.4} + max={2.2} + step={0.1} + marks={[ + { value: 1.4, label: '1.4' }, + { value: 1.8, label: '1.8' }, + { value: 2.2, label: '2.2' }, + ]} + /> + + + {/* Background Color */} + + Background + + + + ) + + if (isMobile) { + return ( + + {content} + + ) + } + + return ( + + {content} + + ) +}