diff --git a/assets/js/app.js b/assets/js/app.js
index 860b946..fec4c3c 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -55,3 +55,16 @@ liveSocket.connect();
window.liveSocket = liveSocket;
handleDarkMode();
+
+if ("serviceWorker" in navigator) {
+ // Register a service worker hosted at the root of the
+ // site using the default scope.
+ navigator.serviceWorker
+ .register("/assets/serviceworker.js", { scope: "/" })
+ .then(
+ (registration) => console.log("Service worker registration succeeded."),
+ (error) => console.error(`Service worker registration failed: ${error}`),
+ );
+} else {
+ console.error("Service workers are not supported.");
+}
diff --git a/assets/js/serviceworker.js b/assets/js/serviceworker.js
new file mode 100644
index 0000000..6d4fb15
--- /dev/null
+++ b/assets/js/serviceworker.js
@@ -0,0 +1,69 @@
+"use strict";
+
+const version = "20241231";
+const staticCacheName = "static-" + version;
+const cacheList = [staticCacheName];
+
+async function updateStaticCache() {
+ let staticCache = await caches.open(staticCacheName);
+ staticCache.addAll([
+ "/assets/video.js",
+ "/assets/app.css",
+ "/assets/app.js",
+ "/images/signee.png",
+ "/images/pfeil.png",
+ "/images/avatar.jpg",
+ "/font/noway-regular-webfont.woff",
+ "/font/noway-regular-webfont.woff2",
+ "/font/Virgil.woff2",
+ ]);
+}
+
+async function clearOldCaches() {
+ let keys = await caches.keys();
+ return Promise.all(
+ keys
+ .filter((key) => !cacheList.includes(key))
+ .map((key) => caches.delete(key)),
+ );
+}
+
+addEventListener("install", (installEvent) => {
+ installEvent.waitUntil(updateStaticCache());
+ skipWaiting();
+});
+
+addEventListener("activate", (activateEvent) => {
+ activateEvent.waitUntil(clearOldCaches());
+ clients.claim();
+});
+
+addEventListener("fetch", (fetchEvent) => {
+ let request = fetchEvent.request;
+ let url = new URL(request.url);
+
+ // Only deal with requests to my own server
+ if (url.origin !== location.origin) {
+ return;
+ }
+
+ // Only deal with GET requests
+ if (request.method !== "GET") {
+ return;
+ }
+
+ // For HTML requests, try the preload first, then network, fall back to the cache, finally the offline page
+ if (
+ request.mode === "navigate" ||
+ request.headers.get("Accept").includes("text/html")
+ ) {
+ return;
+ }
+
+ // For non-HTML requests, look in the cache first, fall back to the network
+ fetchEvent.respondWith(
+ caches.match(request).then((responseFromCache) => {
+ return responseFromCache || fetch(request);
+ }),
+ );
+});
diff --git a/config/config.exs b/config/config.exs
index 718fb19..a7ec87f 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -36,7 +36,7 @@ config :bun,
install: [args: ~w(install), cd: Path.expand("../assets", __DIR__), env: %{}],
default: [
args:
- ~w(build js/app.js js/storybook.js js/video.js --format=iife --outdir=../priv/static/assets --external /fonts/* --external /images/*),
+ ~w(build js/app.js js/storybook.js js/video.js js/serviceworker.js --format=iife --outdir=../priv/static/assets --external /fonts/* --external /images/*),
cd: Path.expand("../assets", __DIR__),
env: %{}
],
diff --git a/lib/kobrakai_web/components/layouts/root.html.heex b/lib/kobrakai_web/components/layouts/root.html.heex
index 178787c..0529d35 100644
--- a/lib/kobrakai_web/components/layouts/root.html.heex
+++ b/lib/kobrakai_web/components/layouts/root.html.heex
@@ -19,7 +19,13 @@
-