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>
This commit is contained in:
2025-09-28 22:20:44 +00:00
parent 83a981cabc
commit a01b2490dc
15 changed files with 2730 additions and 7 deletions

View File

@@ -4,6 +4,9 @@ import { useState, useEffect, useRef, useCallback } from 'react'
import { useTranslations, useLocale } from 'next-intl'
import { useAuth } from '@/hooks/use-auth'
import { useSearchParams, useRouter } from 'next/navigation'
import { OfflineDownloadManager } from '@/components/bible/offline-download-manager'
import { OfflineBibleReader } from '@/components/bible/offline-bible-reader'
import { InstallPrompt, useInstallPrompt } from '@/components/pwa/install-prompt'
import {
Box,
Typography,
@@ -69,7 +72,10 @@ import {
MenuBook,
Visibility,
Speed,
Chat
Chat,
CloudDownload,
WifiOff,
Storage
} from '@mui/icons-material'
interface BibleVerse {
@@ -149,6 +155,11 @@ export default function BibleReaderNew() {
const [showScrollTop, setShowScrollTop] = useState(false)
const [previousVerses, setPreviousVerses] = useState<BibleVerse[]>([]) // Keep previous content during loading
// Offline/PWA state
const [isOnline, setIsOnline] = useState(true)
const [isOfflineMode, setIsOfflineMode] = useState(false)
const [offlineDialogOpen, setOfflineDialogOpen] = useState(false)
// Bookmark state
const [isChapterBookmarked, setIsChapterBookmarked] = useState(false)
const [verseBookmarks, setVerseBookmarks] = useState<{[key: string]: any}>({})
@@ -177,6 +188,9 @@ export default function BibleReaderNew() {
const contentRef = useRef<HTMLDivElement>(null)
const verseRefs = useRef<{[key: number]: HTMLDivElement}>({})
// PWA install prompt
const { canInstall, isInstalled, showInstallPrompt } = useInstallPrompt()
// Load user preferences from localStorage
useEffect(() => {
const savedPrefs = localStorage.getItem('bibleReaderPreferences')
@@ -235,6 +249,39 @@ export default function BibleReaderNew() {
return () => window.removeEventListener('scroll', handleScroll)
}, [])
// Online/offline detection
useEffect(() => {
const handleOnline = () => {
setIsOnline(true)
if (isOfflineMode) {
// Show notification that connection is restored
console.log('Connection restored, you can now access all features')
}
}
const handleOffline = () => {
setIsOnline(false)
console.log('You are now offline. Only downloaded content is available.')
}
// Set initial state
setIsOnline(navigator.onLine)
// Check for offline mode preference
const offlineParam = new URLSearchParams(window.location.search).get('offline')
if (offlineParam === 'true') {
setIsOfflineMode(true)
}
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
return () => {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
}, [isOfflineMode])
// Fetch versions based on showAllVersions state and locale
useEffect(() => {
setVersionsLoading(true)
@@ -1093,6 +1140,24 @@ export default function BibleReaderNew() {
<Share />
</IconButton>
</Tooltip>
<Tooltip title="Offline Downloads">
<IconButton
size="small"
onClick={() => setOfflineDialogOpen(true)}
sx={{ color: !isOnline ? 'warning.main' : 'inherit' }}
>
{isOnline ? <CloudDownload /> : <WifiOff />}
</IconButton>
</Tooltip>
{canInstall && !isInstalled && (
<Tooltip title="Install App">
<IconButton size="small" onClick={showInstallPrompt}>
<Storage />
</IconButton>
</Tooltip>
)}
</Box>
</Paper>
)
@@ -1386,6 +1451,38 @@ export default function BibleReaderNew() {
{/* Settings Dialog */}
{renderSettings()}
{/* Offline Downloads Dialog */}
<Dialog
open={offlineDialogOpen}
onClose={() => setOfflineDialogOpen(false)}
maxWidth="md"
fullWidth
fullScreen={isMobile}
>
<DialogTitle>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Storage color="primary" />
Offline Bible Downloads
</Box>
</DialogTitle>
<DialogContent sx={{ p: 0 }}>
<OfflineDownloadManager
availableVersions={versions}
onVersionDownloaded={(versionId) => {
console.log(`Version ${versionId} downloaded successfully`)
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setOfflineDialogOpen(false)}>
Close
</Button>
</DialogActions>
</Dialog>
{/* PWA Install Prompt */}
<InstallPrompt autoShow={true} />
{/* Copy Feedback */}
<Snackbar
open={copyFeedback.open}

View File

@@ -8,6 +8,7 @@ import { AuthProvider } from '@/components/auth/auth-provider'
import { Navigation } from '@/components/layout/navigation'
import { Footer } from '@/components/layout/footer'
import FloatingChat from '@/components/chat/floating-chat'
import { ServiceWorkerProvider } from '@/components/pwa/service-worker-provider'
import { merriweather, lato } from '@/lib/fonts'
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
@@ -53,6 +54,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
description: t('twitterDescription'),
images: [ogImageUrl],
},
manifest: '/manifest.json',
other: {
'application/ld+json': JSON.stringify({
"@context": "https://schema.org",
@@ -119,12 +121,14 @@ export default async function LocaleLayout({
<body className={`${merriweather.variable} ${lato.variable}`}>
<NextIntlClientProvider messages={messages} locale={locale}>
<MuiThemeProvider>
<AuthProvider>
<Navigation />
{children}
<Footer />
<FloatingChat />
</AuthProvider>
<ServiceWorkerProvider>
<AuthProvider>
<Navigation />
{children}
<Footer />
<FloatingChat />
</AuthProvider>
</ServiceWorkerProvider>
</MuiThemeProvider>
</NextIntlClientProvider>
</body>

View File

@@ -0,0 +1,111 @@
'use client'
import { Suspense, useState, useEffect } from 'react'
import dynamic from 'next/dynamic'
// Dynamic imports to avoid SSR issues with navigator/window
const OfflineBibleReader = dynamic(
() => import('@/components/bible/offline-bible-reader').then(mod => ({ default: mod.OfflineBibleReader })),
{ ssr: false, loading: () => <div>Loading offline reader...</div> }
)
const OfflineDownloadManager = dynamic(
() => import('@/components/bible/offline-download-manager').then(mod => ({ default: mod.OfflineDownloadManager })),
{ ssr: false, loading: () => <div>Loading download manager...</div> }
)
const InstallPrompt = dynamic(
() => import('@/components/pwa/install-prompt').then(mod => ({ default: mod.InstallPrompt })),
{ ssr: false }
)
import {
Box,
Container,
Typography,
Tabs,
Tab,
Paper
} from '@mui/material'
interface BibleVersion {
id: string
name: string
abbreviation: string
language: string
isDefault?: boolean
}
function OfflinePageContent() {
const [tabValue, setTabValue] = useState(0)
const [availableVersions, setAvailableVersions] = useState<BibleVersion[]>([])
useEffect(() => {
// Fetch available Bible versions for download
fetch('/api/bible/versions?all=true&limit=50')
.then(res => res.json())
.then(data => {
if (data.success && data.versions) {
setAvailableVersions(data.versions)
}
})
.catch(err => console.error('Failed to fetch versions:', err))
}, [])
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue)
}
return (
<Container maxWidth="lg" sx={{ py: 3 }}>
<Box sx={{ mb: 3 }}>
<Typography variant="h4" component="h1" gutterBottom>
Offline Bible Reading
</Typography>
<Typography variant="body1" color="text.secondary">
Download Bible versions for offline reading and access them without an internet connection.
</Typography>
</Box>
<Paper sx={{ mb: 3 }}>
<Tabs
value={tabValue}
onChange={handleTabChange}
variant="fullWidth"
sx={{ borderBottom: 1, borderColor: 'divider' }}
>
<Tab label="Offline Reader" />
<Tab label="Download Manager" />
</Tabs>
<Box sx={{ p: 0 }}>
{tabValue === 0 && (
<OfflineBibleReader
onRequestDownload={() => setTabValue(1)}
/>
)}
{tabValue === 1 && (
<OfflineDownloadManager
availableVersions={availableVersions}
onVersionDownloaded={() => setTabValue(0)}
/>
)}
</Box>
</Paper>
{/* PWA Install Prompt */}
<InstallPrompt autoShow={false} />
</Container>
)
}
export default function OfflinePage() {
return (
<Suspense fallback={
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
Loading offline reading interface...
</Box>
}>
<OfflinePageContent />
</Suspense>
)
}