- 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>
331 lines
9.0 KiB
TypeScript
331 lines
9.0 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
// import { useTranslations } from 'next-intl'
|
|
import {
|
|
Box,
|
|
Card,
|
|
CardContent,
|
|
Typography,
|
|
Button,
|
|
IconButton,
|
|
Slide,
|
|
Paper,
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogActions,
|
|
List,
|
|
ListItem,
|
|
ListItemIcon,
|
|
ListItemText
|
|
} from '@mui/material'
|
|
import {
|
|
Close,
|
|
GetApp,
|
|
Smartphone,
|
|
CloudOff,
|
|
Speed,
|
|
Notifications,
|
|
Home,
|
|
Info
|
|
} from '@mui/icons-material'
|
|
import { usePWA } from '@/components/pwa/service-worker-provider'
|
|
|
|
interface InstallPromptProps {
|
|
autoShow?: boolean
|
|
}
|
|
|
|
export function InstallPrompt({ autoShow = false }: InstallPromptProps) {
|
|
// const t = useTranslations('pwa')
|
|
const [showPrompt, setShowPrompt] = useState(false)
|
|
const [showInfo, setShowInfo] = useState(false)
|
|
const [isInstalled, setIsInstalled] = useState(false)
|
|
const { canInstall, installPrompt, isInstalled: pwaInstalled } = usePWA()
|
|
|
|
useEffect(() => {
|
|
// Check if app is already installed
|
|
if (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches) {
|
|
setIsInstalled(true)
|
|
}
|
|
|
|
// Auto show prompt if configured and can install
|
|
if (autoShow && canInstall && !isInstalled && !hasUserDismissed()) {
|
|
const timer = setTimeout(() => {
|
|
setShowPrompt(true)
|
|
}, 3000) // Show after 3 seconds
|
|
|
|
return () => clearTimeout(timer)
|
|
}
|
|
}, [autoShow, canInstall, isInstalled])
|
|
|
|
useEffect(() => {
|
|
setIsInstalled(pwaInstalled)
|
|
}, [pwaInstalled])
|
|
|
|
const hasUserDismissed = () => {
|
|
return localStorage.getItem('pwa-install-dismissed') === 'true'
|
|
}
|
|
|
|
const markAsDismissed = () => {
|
|
localStorage.setItem('pwa-install-dismissed', 'true')
|
|
}
|
|
|
|
const handleInstall = async () => {
|
|
try {
|
|
await installPrompt()
|
|
setShowPrompt(false)
|
|
setIsInstalled(true)
|
|
} catch (error) {
|
|
console.error('Installation failed:', error)
|
|
}
|
|
}
|
|
|
|
const handleDismiss = () => {
|
|
setShowPrompt(false)
|
|
markAsDismissed()
|
|
}
|
|
|
|
const handleShowInfo = () => {
|
|
setShowInfo(true)
|
|
}
|
|
|
|
const getPlatformInstructions = () => {
|
|
const userAgent = navigator.userAgent.toLowerCase()
|
|
|
|
if (userAgent.includes('iphone') || userAgent.includes('ipad')) {
|
|
return {
|
|
platform: 'iOS (Safari)',
|
|
steps: [
|
|
'Tap the Share button in Safari',
|
|
'Scroll down and tap "Add to Home Screen"',
|
|
'Tap "Add" to install the app'
|
|
]
|
|
}
|
|
} else if (userAgent.includes('android')) {
|
|
return {
|
|
platform: 'Android (Chrome)',
|
|
steps: [
|
|
'Tap the menu (three dots) in Chrome',
|
|
'Select "Add to Home screen"',
|
|
'Tap "Add" to install the app'
|
|
]
|
|
}
|
|
} else {
|
|
return {
|
|
platform: 'Desktop',
|
|
steps: [
|
|
'Look for the install icon in your browser\'s address bar',
|
|
'Click the install button or use the browser menu',
|
|
'Follow the prompts to install the app'
|
|
]
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isInstalled) {
|
|
return null // Don't show if already installed
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{/* Install Prompt Card */}
|
|
<Slide direction="up" in={showPrompt} mountOnEnter unmountOnExit>
|
|
<Paper
|
|
elevation={8}
|
|
sx={{
|
|
position: 'fixed',
|
|
bottom: 16,
|
|
left: 16,
|
|
right: 16,
|
|
zIndex: 1300,
|
|
maxWidth: 400,
|
|
mx: 'auto'
|
|
}}
|
|
>
|
|
<Card>
|
|
<CardContent sx={{ pb: 1 }}>
|
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
|
<GetApp color="primary" sx={{ mt: 0.5 }} />
|
|
<Box sx={{ flexGrow: 1 }}>
|
|
<Typography variant="h6" gutterBottom>
|
|
Install Biblical Guide
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary" paragraph>
|
|
Install our app for offline Bible reading, faster access, and a native app experience.
|
|
</Typography>
|
|
|
|
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', mt: 2 }}>
|
|
{canInstall ? (
|
|
<Button
|
|
variant="contained"
|
|
size="small"
|
|
onClick={handleInstall}
|
|
startIcon={<GetApp />}
|
|
>
|
|
Install App
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
variant="contained"
|
|
size="small"
|
|
onClick={handleShowInfo}
|
|
startIcon={<Info />}
|
|
>
|
|
How to Install
|
|
</Button>
|
|
)}
|
|
|
|
<Button
|
|
variant="text"
|
|
size="small"
|
|
onClick={handleDismiss}
|
|
>
|
|
Not Now
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
|
|
<IconButton
|
|
size="small"
|
|
onClick={handleDismiss}
|
|
sx={{ mt: -1, mr: -1 }}
|
|
>
|
|
<Close />
|
|
</IconButton>
|
|
</Box>
|
|
</CardContent>
|
|
</Card>
|
|
</Paper>
|
|
</Slide>
|
|
|
|
{/* Installation Info Dialog */}
|
|
<Dialog
|
|
open={showInfo}
|
|
onClose={() => setShowInfo(false)}
|
|
maxWidth="sm"
|
|
fullWidth
|
|
>
|
|
<DialogTitle>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
<Smartphone color="primary" />
|
|
Install Biblical Guide App
|
|
</Box>
|
|
</DialogTitle>
|
|
|
|
<DialogContent>
|
|
<Typography variant="body1" paragraph>
|
|
Install our Progressive Web App for the best experience:
|
|
</Typography>
|
|
|
|
{/* Benefits */}
|
|
<Typography variant="h6" gutterBottom sx={{ mt: 3 }}>
|
|
Benefits
|
|
</Typography>
|
|
<List dense>
|
|
<ListItem>
|
|
<ListItemIcon>
|
|
<CloudOff color="primary" />
|
|
</ListItemIcon>
|
|
<ListItemText
|
|
primary="Offline Reading"
|
|
secondary="Download Bible versions and read without internet"
|
|
/>
|
|
</ListItem>
|
|
<ListItem>
|
|
<ListItemIcon>
|
|
<Speed color="primary" />
|
|
</ListItemIcon>
|
|
<ListItemText
|
|
primary="Faster Performance"
|
|
secondary="Native app-like speed and responsiveness"
|
|
/>
|
|
</ListItem>
|
|
<ListItem>
|
|
<ListItemIcon>
|
|
<Home color="primary" />
|
|
</ListItemIcon>
|
|
<ListItemText
|
|
primary="Home Screen Access"
|
|
secondary="Quick access from your device's home screen"
|
|
/>
|
|
</ListItem>
|
|
<ListItem>
|
|
<ListItemIcon>
|
|
<Notifications color="primary" />
|
|
</ListItemIcon>
|
|
<ListItemText
|
|
primary="Push Notifications"
|
|
secondary="Daily verses and prayer reminders (optional)"
|
|
/>
|
|
</ListItem>
|
|
</List>
|
|
|
|
{/* Installation Instructions */}
|
|
<Typography variant="h6" gutterBottom sx={{ mt: 3 }}>
|
|
How to Install ({getPlatformInstructions().platform})
|
|
</Typography>
|
|
<List dense>
|
|
{getPlatformInstructions().steps.map((step, index) => (
|
|
<ListItem key={index}>
|
|
<ListItemIcon>
|
|
<Typography variant="body2" color="primary" fontWeight="bold">
|
|
{index + 1}
|
|
</Typography>
|
|
</ListItemIcon>
|
|
<ListItemText primary={step} />
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
</DialogContent>
|
|
|
|
<DialogActions>
|
|
<Button onClick={() => setShowInfo(false)}>
|
|
Close
|
|
</Button>
|
|
{canInstall && (
|
|
<Button
|
|
variant="contained"
|
|
onClick={handleInstall}
|
|
startIcon={<GetApp />}
|
|
>
|
|
Install Now
|
|
</Button>
|
|
)}
|
|
</DialogActions>
|
|
</Dialog>
|
|
</>
|
|
)
|
|
}
|
|
|
|
// Export a hook for manual installation prompt
|
|
export function useInstallPrompt() {
|
|
const { canInstall, installPrompt } = usePWA()
|
|
const [isInstalled, setIsInstalled] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches) {
|
|
setIsInstalled(true)
|
|
}
|
|
}, [])
|
|
|
|
const showInstallPrompt = async () => {
|
|
if (canInstall && !isInstalled) {
|
|
try {
|
|
await installPrompt()
|
|
setIsInstalled(true)
|
|
return true
|
|
} catch (error) {
|
|
console.error('Installation failed:', error)
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
return {
|
|
canInstall: canInstall && !isInstalled,
|
|
isInstalled,
|
|
showInstallPrompt
|
|
}
|
|
} |