freezmo/public/sw.js
Martin Dvorak a8c752828e Drop the sticky bar: the Freeze my deck CTA now sits in the flow like the homework one
The last visible difference between the two CTAs was the container: the
deck button lived in a full-bleed sticky strip (translucent background,
hairline top border, its own padding), which also tinted the disabled chip
differently. The homework button sits plainly in the page flow.

- The CTA block is now a plain space-y-2 stack at the end of the form:
  full-width button + centred helper line, exactly like /homework.
- /create's page padding switches from the sticky-era .pb-cta (9rem, phone
  override 4.6rem) to .pb-tabbar, and lg:pb-20 to lg:pb-16 — identical to
  the homework page. The now-unused .pb-cta rules are gone.
- Result: same button width (358px @390 / 688px @1280), height, radius,
  helper placement and trailing space on both pages; neither has a sticky
  ancestor any more.

Service worker: freezmo-v16.

Verified: CTA-flow suite 26/26 (below-the-fold at rest, no sticky ancestor,
full-width, helper centred underneath, width/height/radius/trailing-space
parity at 390px and 1280px), desktop regression 74/74, mobile 51/51,
i18n 27/27. Lint + build clean.
2026-10-09 10:52:23 +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-v16";
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;
})(),
);
}
});