freezmo/public/sw.js
Martin Dvorak efbdb5a55a Give both creation CTAs their feature icon (the other way around)
Correcting the previous change: instead of stripping the snowflake from
"Freeze my deck", "Make my homework" now carries the homework flow's own
sparkles icon, and the snowflake is back on the deck button.

Both CTAs are now identical in shape — feature icon + label, full-width,
44px, 14px radius, helper line centred underneath, same muted disabled chip
and ice gradient when enabled. The computed-style diff between the two
buttons is empty and their accessible names are unchanged.

Service worker: freezmo-v14.

Verified: CTA-parity suite 32/32, desktop regression 74/74, mobile 51/51.
Lint + build clean.
2026-10-09 08:06:25 +00:00

97 lines
2.8 KiB
JavaScript

/**
* Freezmo service worker (production only).
*
* Strategy, deliberately conservative:
* - API routes are never cached and never intercepted.
* - Navigations: network first, falling back to the cached shell, then /offline.
* - /_next/static + icons: stale-while-revalidate (fast, self-healing).
* - Everything else: straight to the network.
*
* Bumping CACHE_VERSION is how a new deploy invalidates old assets.
*/
const CACHE_VERSION = "freezmo-v14";
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const ASSET_CACHE = `${CACHE_VERSION}-assets`;
const SHELL_ASSETS = [
"/",
"/offline",
"/manifest.webmanifest",
"/icons/icon-192.png",
"/icons/icon-512.png",
"/icons/apple-touch-icon.png",
];
self.addEventListener("install", (event) => {
event.waitUntil(
(async () => {
const cache = await caches.open(SHELL_CACHE);
await Promise.allSettled(SHELL_ASSETS.map((url) => cache.add(new Request(url, { cache: "reload" }))));
await self.skipWaiting();
})(),
);
});
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
const names = await caches.keys();
await Promise.all(
names.filter((name) => !name.startsWith(CACHE_VERSION)).map((name) => caches.delete(name)),
);
await self.clients.claim();
})(),
);
});
function isCacheableAsset(url) {
return (
url.pathname.startsWith("/_next/static/") ||
url.pathname.startsWith("/icons/") ||
url.pathname === "/manifest.webmanifest" ||
/\.(?:css|js|png|jpg|jpeg|svg|webp|woff2?)$/.test(url.pathname)
);
}
self.addEventListener("fetch", (event) => {
const request = event.request;
if (request.method !== "GET") return;
const url = new URL(request.url);
if (url.origin !== self.location.origin) return;
// Never touch the API: keys, notes and generated decks must always be live.
if (url.pathname.startsWith("/api/")) return;
if (request.mode === "navigate") {
event.respondWith(
(async () => {
try {
const response = await fetch(request);
const cache = await caches.open(SHELL_CACHE);
cache.put("/", response.clone()).catch(() => {});
return response;
} catch {
const cache = await caches.open(SHELL_CACHE);
return (await cache.match("/")) ?? (await cache.match("/offline")) ?? Response.error();
}
})(),
);
return;
}
if (isCacheableAsset(url)) {
event.respondWith(
(async () => {
const cache = await caches.open(ASSET_CACHE);
const cached = await cache.match(request);
const network = fetch(request)
.then((response) => {
if (response.ok) cache.put(request, response.clone()).catch(() => {});
return response;
})
.catch(() => cached);
return cached ?? network;
})(),
);
}
});