This commit is contained in:
Stefan Lazic committed 2026-08-03 21:15:24 +01:00
1 parent ec5583f972
commit 546613e677
189 files changed
+9873 -655

No files matched your search

@@ -0,0 +1,81 @@
"use client";
import { useEffect, useState } from "react";
/**
* Registers the service worker once, app-wide, and surfaces the
* update-available handshake public/sw.js sets up: a new SW version
* installs and sits `waiting` rather than taking over immediately, so an
* open tab isn't silently switched to a new app version mid-session. This
* component detects that waiting worker and lets the user opt into the
* refresh instead.
*/
export function ServiceWorkerManager() {
const [waitingWorker, setWaitingWorker] = useState<ServiceWorker | null>(null);
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
if (!("serviceWorker" in navigator)) return;
navigator.serviceWorker
.register("/sw.js")
.then((registration) => {
// An update was already downloaded (e.g. by another tab) before this page loaded.
if (registration.waiting && navigator.serviceWorker.controller) {
setWaitingWorker(registration.waiting);
}
registration.addEventListener("updatefound", () => {
const installing = registration.installing;
if (!installing) return;
installing.addEventListener("statechange", () => {
// Only a real update — an installing worker with no existing
// controller is just this tab's first-ever SW install, not an update.
if (installing.state === "installed" && navigator.serviceWorker.controller) {
setWaitingWorker(installing);
}
});
});
})
.catch(() => {
// Best-effort — offline/install/update support degrades gracefully without it.
});
let reloaded = false;
const onControllerChange = () => {
if (reloaded) return;
reloaded = true;
window.location.reload();
};
navigator.serviceWorker.addEventListener("controllerchange", onControllerChange);
return () => navigator.serviceWorker.removeEventListener("controllerchange", onControllerChange);
}, []);
if (!waitingWorker || dismissed) return null;
return (
<div
role="status"
className="rounded-jumbo bg-ink fixed inset-x-6 top-4 z-50 mx-auto flex max-w-sm items-center justify-between gap-3 px-5 py-3 text-sm text-white shadow-xl"
>
<span>A new version is available.</span>
<div className="flex items-center gap-3">
<button
type="button"
className="font-bold whitespace-nowrap underline"
onClick={() => waitingWorker.postMessage({ type: "SKIP_WAITING" })}
>
Refresh
</button>
<button
type="button"
aria-label="Dismiss"
className="text-white/60"
onClick={() => setDismissed(true)}
>
✕
</button>
</div>
</div>
);
}