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:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
111
app/[locale]/offline/page.tsx
Normal file
111
app/[locale]/offline/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user