Files
biblical-guide.com/components/bible/offline-bible-reader.tsx
Andrei a01b2490dc Implement comprehensive PWA with offline Bible reading capabilities
- Add Web App Manifest with app metadata, icons, and installation support
- Create Service Worker with intelligent caching strategies for Bible content, static assets, and dynamic content
- Implement IndexedDB-based offline storage system for Bible versions, books, chapters, and verses
- Add offline download manager component for browsing and downloading Bible versions
- Create offline Bible reader component for seamless offline reading experience
- Integrate PWA install prompt with platform-specific instructions
- Add offline reading interface to existing Bible reader with download buttons
- Create dedicated offline page with tabbed interface for reading and downloading
- Add PWA and offline-related translations for English and Romanian locales
- Implement background sync for Bible downloads and cache management
- Add storage usage monitoring and management utilities
- Ensure SSR-safe implementation with dynamic imports for client-side components

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-28 22:20:44 +00:00

371 lines
11 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
// import { useTranslations } from 'next-intl'
import {
Box,
Card,
CardContent,
Typography,
Select,
MenuItem,
FormControl,
InputLabel,
Button,
Alert,
CircularProgress,
Chip,
Paper,
Divider,
IconButton,
Tooltip
} from '@mui/material'
import {
CloudOff,
Wifi,
WifiOff,
NavigateBefore,
NavigateNext,
MenuBook,
Download
} from '@mui/icons-material'
import {
offlineStorage,
type BibleVersion,
type BibleBook,
type BibleChapter
} from '@/lib/offline-storage'
interface OfflineBibleReaderProps {
onRequestDownload?: () => void
}
export function OfflineBibleReader({ onRequestDownload }: OfflineBibleReaderProps) {
// const t = useTranslations('bible')
const [isOnline, setIsOnline] = useState(true)
const [versions, setVersions] = useState<BibleVersion[]>([])
const [selectedVersion, setSelectedVersion] = useState<string>('')
const [books, setBooks] = useState<BibleBook[]>([])
const [selectedBook, setSelectedBook] = useState<string>('')
const [currentChapter, setCurrentChapter] = useState<BibleChapter | null>(null)
const [selectedChapterNum, setSelectedChapterNum] = useState<number>(1)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
checkOnlineStatus()
loadOfflineVersions()
// Listen for online/offline events
const handleOnline = () => setIsOnline(true)
const handleOffline = () => setIsOnline(false)
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
return () => {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
}, [])
useEffect(() => {
if (selectedVersion) {
loadBooksForVersion(selectedVersion)
}
}, [selectedVersion])
useEffect(() => {
if (selectedBook && selectedVersion) {
loadChapter(selectedVersion, selectedBook, selectedChapterNum)
}
}, [selectedBook, selectedChapterNum, selectedVersion])
const checkOnlineStatus = () => {
setIsOnline(navigator.onLine)
}
const loadOfflineVersions = async () => {
try {
setLoading(true)
const offlineVersions = await offlineStorage.getAllVersions()
setVersions(offlineVersions)
if (offlineVersions.length > 0 && !selectedVersion) {
setSelectedVersion(offlineVersions[0].id)
}
} catch (error) {
console.error('Failed to load offline versions:', error)
setError('Failed to load offline Bible versions')
} finally {
setLoading(false)
}
}
const loadBooksForVersion = async (versionId: string) => {
try {
const versionBooks = await offlineStorage.getBooksForVersion(versionId)
setBooks(versionBooks.sort((a, b) => a.orderNum - b.orderNum))
if (versionBooks.length > 0 && !selectedBook) {
setSelectedBook(versionBooks[0].id)
setSelectedChapterNum(1)
}
} catch (error) {
console.error('Failed to load books:', error)
setError('Failed to load books for this version')
}
}
const loadChapter = async (versionId: string, bookId: string, chapterNum: number) => {
try {
setError(null)
const chapter = await offlineStorage.getChapter(versionId, bookId, chapterNum)
if (chapter) {
setCurrentChapter(chapter)
} else {
setError(`Chapter ${chapterNum} not found in offline storage`)
}
} catch (error) {
console.error('Failed to load chapter:', error)
setError('Failed to load chapter from offline storage')
}
}
const navigateChapter = (direction: 'prev' | 'next') => {
if (!currentChapter || !selectedBook) return
const currentBook = books.find(b => b.id === selectedBook)
if (!currentBook) return
let newChapterNum = selectedChapterNum
let newBookId = selectedBook
if (direction === 'next') {
if (selectedChapterNum < currentBook.chaptersCount) {
newChapterNum = selectedChapterNum + 1
} else {
// Move to next book
const currentBookIndex = books.findIndex(b => b.id === selectedBook)
if (currentBookIndex < books.length - 1) {
newBookId = books[currentBookIndex + 1].id
newChapterNum = 1
}
}
} else {
if (selectedChapterNum > 1) {
newChapterNum = selectedChapterNum - 1
} else {
// Move to previous book
const currentBookIndex = books.findIndex(b => b.id === selectedBook)
if (currentBookIndex > 0) {
const prevBook = books[currentBookIndex - 1]
newBookId = prevBook.id
newChapterNum = prevBook.chaptersCount
}
}
}
if (newBookId !== selectedBook) {
setSelectedBook(newBookId)
}
setSelectedChapterNum(newChapterNum)
}
const getCurrentBookName = () => {
return books.find(b => b.id === selectedBook)?.name || ''
}
const getVersionName = () => {
return versions.find(v => v.id === selectedVersion)?.name || ''
}
if (loading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
<CircularProgress />
</Box>
)
}
if (versions.length === 0) {
return (
<Card sx={{ m: 2 }}>
<CardContent sx={{ textAlign: 'center', py: 6 }}>
<CloudOff sx={{ fontSize: 60, color: 'text.secondary', mb: 2 }} />
<Typography variant="h5" gutterBottom>
No Offline Bible Versions
</Typography>
<Typography color="text.secondary" paragraph>
You haven't downloaded any Bible versions for offline reading yet.
</Typography>
<Button
variant="contained"
startIcon={<Download />}
onClick={onRequestDownload}
sx={{ mt: 2 }}
>
Download Bible Versions
</Button>
</CardContent>
</Card>
)
}
return (
<Box sx={{ p: 2 }}>
{/* Header with status */}
<Box sx={{ mb: 3, display: 'flex', alignItems: 'center', gap: 2 }}>
<MenuBook color="primary" />
<Typography variant="h5">Offline Bible Reader</Typography>
<Chip
icon={isOnline ? <Wifi /> : <WifiOff />}
label={isOnline ? 'Online' : 'Offline'}
color={isOnline ? 'success' : 'warning'}
size="small"
/>
</Box>
{/* Controls */}
<Paper sx={{ p: 2, mb: 3 }}>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'center' }}>
<FormControl size="small" sx={{ minWidth: 200 }}>
<InputLabel>Bible Version</InputLabel>
<Select
value={selectedVersion}
label="Bible Version"
onChange={(e) => setSelectedVersion(e.target.value)}
>
{versions.map((version) => (
<MenuItem key={version.id} value={version.id}>
{version.name} ({version.abbreviation})
</MenuItem>
))}
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 200 }}>
<InputLabel>Book</InputLabel>
<Select
value={selectedBook}
label="Book"
onChange={(e) => {
setSelectedBook(e.target.value)
setSelectedChapterNum(1)
}}
>
{books.map((book) => (
<MenuItem key={book.id} value={book.id}>
{book.name}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 120 }}>
<InputLabel>Chapter</InputLabel>
<Select
value={selectedChapterNum}
label="Chapter"
onChange={(e) => setSelectedChapterNum(Number(e.target.value))}
>
{Array.from(
{ length: books.find(b => b.id === selectedBook)?.chaptersCount || 1 },
(_, i) => (
<MenuItem key={i + 1} value={i + 1}>
{i + 1}
</MenuItem>
)
)}
</Select>
</FormControl>
</Box>
</Paper>
{/* Error Alert */}
{error && (
<Alert severity="error" sx={{ mb: 3 }}>
{error}
</Alert>
)}
{/* Chapter Content */}
{currentChapter && (
<Card>
<CardContent>
{/* Chapter Header */}
<Box sx={{ mb: 3, textAlign: 'center' }}>
<Typography variant="h4" gutterBottom>
{getCurrentBookName()} {selectedChapterNum}
</Typography>
<Typography variant="subtitle1" color="text.secondary">
{getVersionName()}
</Typography>
</Box>
<Divider sx={{ mb: 3 }} />
{/* Verses */}
<Box sx={{ lineHeight: 2, fontSize: '1.1rem' }}>
{currentChapter.verses.map((verse) => (
<Box key={verse.id} sx={{ mb: 1 }}>
<Typography
component="span"
sx={{
fontWeight: 'bold',
color: 'primary.main',
fontSize: '0.9em',
mr: 1
}}
>
{verse.verseNum}
</Typography>
<Typography component="span">
{verse.text}
</Typography>
</Box>
))}
</Box>
{/* Navigation */}
<Box sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mt: 4,
pt: 2,
borderTop: 1,
borderColor: 'divider'
}}>
<Tooltip title="Previous Chapter">
<IconButton
onClick={() => navigateChapter('prev')}
disabled={selectedChapterNum === 1 && books.findIndex(b => b.id === selectedBook) === 0}
>
<NavigateBefore />
</IconButton>
</Tooltip>
<Typography variant="body2" color="text.secondary">
{currentChapter.verses.length} verses
</Typography>
<Tooltip title="Next Chapter">
<IconButton
onClick={() => navigateChapter('next')}
disabled={
selectedChapterNum === (books.find(b => b.id === selectedBook)?.chaptersCount || 1) &&
books.findIndex(b => b.id === selectedBook) === books.length - 1
}
>
<NavigateNext />
</IconButton>
</Tooltip>
</Box>
</CardContent>
</Card>
)}
</Box>
)
}