105 lines
3.6 KiB
TypeScript
105 lines
3.6 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect, useCallback } from 'react'
|
|
import { RefreshCw, Download } from 'lucide-react'
|
|
|
|
const CHECK_INTERVAL = 60_000 // check every 60s
|
|
|
|
export function UpdateBanner() {
|
|
const [updateAvailable, setUpdateAvailable] = useState(false)
|
|
const [initialVersion, setInitialVersion] = useState<string | null>(null)
|
|
const [updating, setUpdating] = useState(false)
|
|
|
|
const checkVersion = useCallback(async () => {
|
|
try {
|
|
const res = await fetch('/api/version', { cache: 'no-store' })
|
|
const { version } = await res.json()
|
|
if (!version) return
|
|
|
|
if (initialVersion === null) {
|
|
setInitialVersion(version)
|
|
} else if (version !== initialVersion) {
|
|
setUpdateAvailable(true)
|
|
}
|
|
} catch {
|
|
// network error, skip
|
|
}
|
|
}, [initialVersion])
|
|
|
|
useEffect(() => {
|
|
checkVersion()
|
|
const id = setInterval(checkVersion, CHECK_INTERVAL)
|
|
return () => clearInterval(id)
|
|
}, [checkVersion])
|
|
|
|
// Also check on visibility change (user returns to tab)
|
|
useEffect(() => {
|
|
const onVisible = () => {
|
|
if (document.visibilityState === 'visible') checkVersion()
|
|
}
|
|
document.addEventListener('visibilitychange', onVisible)
|
|
return () => document.removeEventListener('visibilitychange', onVisible)
|
|
}, [checkVersion])
|
|
|
|
if (!updateAvailable) return null
|
|
|
|
const handleUpdate = async () => {
|
|
setUpdating(true)
|
|
try {
|
|
// 1. Clear all service worker caches
|
|
const keys = await caches.keys()
|
|
await Promise.all(keys.map((k) => caches.delete(k)))
|
|
|
|
// 2. If a new service worker is waiting, activate it
|
|
const reg = await navigator.serviceWorker?.getRegistration()
|
|
if (reg?.waiting) {
|
|
// Listen for the new SW to take control, then reload
|
|
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
|
window.location.reload()
|
|
}, { once: true })
|
|
reg.waiting.postMessage({ type: 'SKIP_WAITING' })
|
|
// Fallback reload if controllerchange doesn't fire within 3s
|
|
setTimeout(() => window.location.reload(), 3000)
|
|
return
|
|
}
|
|
|
|
// 3. If there's an active SW but no waiting one, unregister and reload
|
|
if (reg) {
|
|
await reg.unregister()
|
|
}
|
|
} catch {
|
|
// If anything fails, just reload
|
|
}
|
|
window.location.reload()
|
|
}
|
|
|
|
return (
|
|
<div className="fixed inset-x-0 top-0 z-[100] bg-gradient-to-r from-purple-700 via-purple-600 to-indigo-600 text-white shadow-2xl animate-in slide-in-from-top duration-300">
|
|
<div className="mx-auto max-w-2xl px-4 py-4 flex flex-col items-center gap-3 sm:flex-row sm:justify-between">
|
|
<div className="flex items-center gap-3 text-center sm:text-left">
|
|
<div className="rounded-full bg-white/20 p-2 animate-pulse">
|
|
<Download className="h-5 w-5" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-base font-bold tracking-tight sm:text-lg">
|
|
Update Available
|
|
</h2>
|
|
<p className="text-xs text-purple-100 sm:text-sm">
|
|
A new version of Jefflix has been deployed
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleUpdate}
|
|
disabled={updating}
|
|
className="flex items-center gap-2 rounded-full bg-white text-purple-700 font-bold px-6 py-2.5 text-sm shadow-lg hover:bg-purple-50 active:scale-95 transition-all cursor-pointer disabled:opacity-70 whitespace-nowrap"
|
|
>
|
|
<RefreshCw className={`h-4 w-4 ${updating ? 'animate-spin' : ''}`} />
|
|
{updating ? 'Updating…' : 'Update Now'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|