- 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>
238 lines
6.8 KiB
TypeScript
238 lines
6.8 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import { useTranslations } from 'next-intl'
|
|
|
|
interface ServiceWorkerContextType {
|
|
isInstalling: boolean
|
|
isInstalled: boolean
|
|
isUpdateAvailable: boolean
|
|
updateServiceWorker: () => Promise<void>
|
|
installPrompt: () => Promise<void>
|
|
canInstall: boolean
|
|
}
|
|
|
|
export function ServiceWorkerProvider({ children }: { children: React.ReactNode }) {
|
|
const [isInstalling, setIsInstalling] = useState(false)
|
|
const [isInstalled, setIsInstalled] = useState(false)
|
|
const [isUpdateAvailable, setIsUpdateAvailable] = useState(false)
|
|
const [canInstall, setCanInstall] = useState(false)
|
|
const [deferredPrompt, setDeferredPrompt] = useState<any>(null)
|
|
const [registration, setRegistration] = useState<ServiceWorkerRegistration | null>(null)
|
|
const t = useTranslations('pwa')
|
|
|
|
useEffect(() => {
|
|
if (typeof window === 'undefined' || !('serviceWorker' in navigator)) {
|
|
console.log('[PWA] Service Worker not supported')
|
|
return
|
|
}
|
|
|
|
registerServiceWorker()
|
|
setupInstallPrompt()
|
|
|
|
// Cleanup function
|
|
return () => {
|
|
if (registration) {
|
|
registration.removeEventListener('updatefound', handleUpdateFound)
|
|
}
|
|
}
|
|
}, [])
|
|
|
|
const registerServiceWorker = async () => {
|
|
try {
|
|
setIsInstalling(true)
|
|
console.log('[PWA] Registering service worker...')
|
|
|
|
const reg = await navigator.serviceWorker.register('/sw.js', {
|
|
scope: '/',
|
|
updateViaCache: 'none'
|
|
})
|
|
|
|
setRegistration(reg)
|
|
setIsInstalled(true)
|
|
console.log('[PWA] Service worker registered successfully')
|
|
|
|
// Check for updates
|
|
reg.addEventListener('updatefound', handleUpdateFound)
|
|
|
|
// Check if there's already an update waiting
|
|
if (reg.waiting) {
|
|
setIsUpdateAvailable(true)
|
|
}
|
|
|
|
// Listen for controlling service worker changes
|
|
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
|
console.log('[PWA] Service worker controller changed')
|
|
window.location.reload()
|
|
})
|
|
|
|
} catch (error) {
|
|
console.error('[PWA] Service worker registration failed:', error)
|
|
} finally {
|
|
setIsInstalling(false)
|
|
}
|
|
}
|
|
|
|
const handleUpdateFound = () => {
|
|
if (!registration) return
|
|
|
|
const newWorker = registration.installing
|
|
if (!newWorker) return
|
|
|
|
console.log('[PWA] New service worker installing...')
|
|
|
|
newWorker.addEventListener('statechange', () => {
|
|
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
|
console.log('[PWA] New service worker installed, update available')
|
|
setIsUpdateAvailable(true)
|
|
}
|
|
})
|
|
}
|
|
|
|
const updateServiceWorker = async () => {
|
|
if (!registration || !registration.waiting) {
|
|
console.log('[PWA] No update available')
|
|
return
|
|
}
|
|
|
|
console.log('[PWA] Updating service worker...')
|
|
registration.waiting.postMessage({ type: 'SKIP_WAITING' })
|
|
setIsUpdateAvailable(false)
|
|
}
|
|
|
|
const setupInstallPrompt = () => {
|
|
// Listen for beforeinstallprompt event
|
|
window.addEventListener('beforeinstallprompt', (e) => {
|
|
console.log('[PWA] Install prompt available')
|
|
e.preventDefault()
|
|
setDeferredPrompt(e)
|
|
setCanInstall(true)
|
|
})
|
|
|
|
// Listen for app installed event
|
|
window.addEventListener('appinstalled', () => {
|
|
console.log('[PWA] App installed')
|
|
setCanInstall(false)
|
|
setDeferredPrompt(null)
|
|
showNotification('App installed successfully!', 'success')
|
|
})
|
|
|
|
// Check if already installed
|
|
if (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches) {
|
|
console.log('[PWA] App is running in standalone mode')
|
|
setCanInstall(false)
|
|
}
|
|
}
|
|
|
|
const installPrompt = async () => {
|
|
if (!deferredPrompt) {
|
|
console.log('[PWA] No install prompt available')
|
|
return
|
|
}
|
|
|
|
try {
|
|
console.log('[PWA] Showing install prompt...')
|
|
const result = await deferredPrompt.prompt()
|
|
console.log('[PWA] Install prompt result:', result.outcome)
|
|
|
|
if (result.outcome === 'accepted') {
|
|
setCanInstall(false)
|
|
setDeferredPrompt(null)
|
|
}
|
|
} catch (error) {
|
|
console.error('[PWA] Install prompt failed:', error)
|
|
}
|
|
}
|
|
|
|
const showNotification = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
|
|
// Show a toast notification (you can integrate with your existing notification system)
|
|
console.log(`[PWA] ${type.toUpperCase()}: ${message}`)
|
|
|
|
// Create a simple toast
|
|
const toast = document.createElement('div')
|
|
toast.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: ${type === 'success' ? '#4CAF50' : type === 'error' ? '#f44336' : '#2196F3'};
|
|
color: white;
|
|
padding: 12px 20px;
|
|
border-radius: 8px;
|
|
z-index: 10000;
|
|
font-family: system-ui, -apple-system, sans-serif;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
|
animation: slideIn 0.3s ease;
|
|
`
|
|
toast.textContent = message
|
|
|
|
// Add animation keyframes if not already added
|
|
if (!document.querySelector('#pwa-toast-styles')) {
|
|
const style = document.createElement('style')
|
|
style.id = 'pwa-toast-styles'
|
|
style.textContent = `
|
|
@keyframes slideIn {
|
|
from { transform: translateX(100%); opacity: 0; }
|
|
to { transform: translateX(0); opacity: 1; }
|
|
}
|
|
@keyframes slideOut {
|
|
from { transform: translateX(0); opacity: 1; }
|
|
to { transform: translateX(100%); opacity: 0; }
|
|
}
|
|
`
|
|
document.head.appendChild(style)
|
|
}
|
|
|
|
document.body.appendChild(toast)
|
|
|
|
// Remove after 4 seconds
|
|
setTimeout(() => {
|
|
toast.style.animation = 'slideOut 0.3s ease'
|
|
setTimeout(() => {
|
|
document.body.removeChild(toast)
|
|
}, 300)
|
|
}, 4000)
|
|
}
|
|
|
|
// Provide context value
|
|
const contextValue: ServiceWorkerContextType = {
|
|
isInstalling,
|
|
isInstalled,
|
|
isUpdateAvailable,
|
|
updateServiceWorker,
|
|
installPrompt,
|
|
canInstall
|
|
}
|
|
|
|
// For now, just provide the functions globally
|
|
// You can later create a proper React context if needed
|
|
useEffect(() => {
|
|
if (typeof window !== 'undefined') {
|
|
(window as any).pwa = contextValue
|
|
}
|
|
}, [contextValue])
|
|
|
|
return <>{children}</>
|
|
}
|
|
|
|
// Export hook for using PWA functionality
|
|
export function usePWA(): ServiceWorkerContextType {
|
|
if (typeof window === 'undefined') {
|
|
return {
|
|
isInstalling: false,
|
|
isInstalled: false,
|
|
isUpdateAvailable: false,
|
|
updateServiceWorker: async () => {},
|
|
installPrompt: async () => {},
|
|
canInstall: false
|
|
}
|
|
}
|
|
|
|
return (window as any).pwa || {
|
|
isInstalling: false,
|
|
isInstalled: false,
|
|
isUpdateAvailable: false,
|
|
updateServiceWorker: async () => {},
|
|
installPrompt: async () => {},
|
|
canInstall: false
|
|
}
|
|
} |