From 8c13021bfb44d3ee4a2fc7c9d82c33b5917ba858 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 21 Feb 2025 09:31:23 +0100 Subject: [PATCH] feat: offline pages mode --- public/offline.html | 16 +- public/sw.js | 137 ++++++++---------- src/components/layout/ClientLayout.tsx | 14 +- .../reader/components/Thumbnail.tsx | 2 +- src/components/ui/NetworkStatus.tsx | 17 +++ src/components/ui/separator.tsx | 25 ---- src/hooks/useNetworkStatus.ts | 29 ++++ src/lib/registerSW.ts | 12 ++ 8 files changed, 136 insertions(+), 116 deletions(-) create mode 100644 src/components/ui/NetworkStatus.tsx delete mode 100644 src/components/ui/separator.tsx create mode 100644 src/hooks/useNetworkStatus.ts create mode 100644 src/lib/registerSW.ts diff --git a/public/offline.html b/public/offline.html index 6de462a..315e2a7 100644 --- a/public/offline.html +++ b/public/offline.html @@ -34,6 +34,11 @@ color: #94a3b8; margin-bottom: 2rem; } + .buttons { + display: flex; + gap: 1rem; + justify-content: center; + } button { background-color: #4f46e5; color: white; @@ -47,6 +52,12 @@ button:hover { background-color: #4338ca; } + button.secondary { + background-color: #475569; + } + button.secondary:hover { + background-color: #334155; + } @@ -56,7 +67,10 @@ Il semble que vous n'ayez pas de connexion internet. Certaines fonctionnalités de StripStream peuvent ne pas être disponibles en mode hors ligne.

- +
+ + +
diff --git a/public/sw.js b/public/sw.js index fa470ab..2f0c206 100644 --- a/public/sw.js +++ b/public/sw.js @@ -2,7 +2,6 @@ const CACHE_NAME = "stripstream-cache-v1"; const OFFLINE_PAGE = "/offline.html"; const STATIC_ASSETS = [ - "/", "/offline.html", "/manifest.json", "/favicon.svg", @@ -12,12 +11,7 @@ const STATIC_ASSETS = [ // Installation du service worker self.addEventListener("install", (event) => { - event.waitUntil( - caches.open(CACHE_NAME).then((cache) => { - return cache.addAll(STATIC_ASSETS); - }) - ); - self.skipWaiting(); + event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS))); }); // Activation et nettoyage des anciens caches @@ -29,79 +23,64 @@ self.addEventListener("activate", (event) => { ); }) ); - self.clients.claim(); }); -// Stratégie de cache différente selon le type de requête +// Fonction pour vérifier si c'est une ressource webpack +const isWebpackResource = (url) => { + return ( + url.includes("/_next/webpack") || + url.includes("webpack-hmr") || + url.includes("webpack.js") || + url.includes("webpack-runtime") || + url.includes("hot-update") + ); +}; + +// Fonction pour vérifier si c'est une ressource statique de Next.js +const isNextStaticResource = (url) => { + return url.includes("/_next/static") && !isWebpackResource(url); +}; + self.addEventListener("fetch", (event) => { - // Pour les requêtes API, on utilise "Network First" avec un timeout - if (event.request.url.includes("/api/")) { - // event.respondWith( - // Promise.race([ - // fetch(event.request.clone()) - // .then((response) => { - // // Ne mettre en cache que les réponses réussies - // if (response.ok) { - // const responseToCache = response.clone(); - // caches.open(CACHE_NAME).then((cache) => { - // cache.put(event.request, responseToCache); - // }); - // } - // return response; - // }) - // .catch(() => { - // // En cas d'erreur réseau, essayer le cache - // return caches.match(event.request).then((cachedResponse) => { - // if (cachedResponse) { - // return cachedResponse; - // } - // // Si pas de cache, renvoyer une erreur appropriée - // return new Response(JSON.stringify({ error: "Hors ligne" }), { - // status: 503, - // headers: { "Content-Type": "application/json" }, - // }); - // }); - // }), - // // Timeout après 5 secondes - // new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 5000)).catch( - // () => { - // return caches.match(event.request).then((cachedResponse) => { - // if (cachedResponse) { - // return cachedResponse; - // } - // return new Response(JSON.stringify({ error: "Timeout" }), { - // status: 504, - // headers: { "Content-Type": "application/json" }, - // }); - // }); - // } - // ), - // ]) - // ); - } else { - // Pour les autres ressources, on garde la stratégie "Cache First" - // event.respondWith( - // caches.match(event.request).then((cachedResponse) => { - // if (cachedResponse) { - // return cachedResponse; - // } - // return fetch(event.request) - // .then((response) => { - // // Mettre en cache la nouvelle réponse - // const responseToCache = response.clone(); - // caches.open(CACHE_NAME).then((cache) => { - // cache.put(event.request, responseToCache); - // }); - // return response; - // }) - // .catch(() => { - // // Si la requête échoue et que c'est une page, renvoyer la page hors ligne - // if (event.request.mode === "navigate") { - // return caches.match(OFFLINE_PAGE); - // } - // return new Response("Hors ligne", { status: 503 }); - // }); - // }) - // ); - } + // Ignorer les requêtes non GET + if (event.request.method !== "GET") return; + + // Ignorer les ressources webpack + if (isWebpackResource(event.request.url)) return; + + // Pour les ressources statiques de Next.js et les autres requêtes + event.respondWith( + fetch(event.request) + .then((response) => { + // Mettre en cache les ressources statiques de Next.js et les pages + if ( + response.ok && + (isNextStaticResource(event.request.url) || event.request.mode === "navigate") + ) { + const responseToCache = response.clone(); + caches.open(CACHE_NAME).then((cache) => { + cache.put(event.request, responseToCache); + }); + } + return response; + }) + .catch(async () => { + const cache = await caches.open(CACHE_NAME); + const cachedResponse = await cache.match(event.request); + + if (cachedResponse) { + return cachedResponse; + } + + // Si c'est une navigation, renvoyer la page hors ligne + if (event.request.mode === "navigate") { + return cache.match(OFFLINE_PAGE); + } + + return new Response(JSON.stringify({ error: "Hors ligne" }), { + status: 503, + headers: { "Content-Type": "application/json" }, + }); + }) + ); }); diff --git a/src/components/layout/ClientLayout.tsx b/src/components/layout/ClientLayout.tsx index 3b7bd75..03132bb 100644 --- a/src/components/layout/ClientLayout.tsx +++ b/src/components/layout/ClientLayout.tsx @@ -8,6 +8,8 @@ import { InstallPWA } from "../ui/InstallPWA"; import { Toaster } from "@/components/ui/toaster"; import { usePathname } from "next/navigation"; import { PreferencesProvider } from "@/contexts/PreferencesContext"; +import { registerServiceWorker } from "@/lib/registerSW"; +import { NetworkStatus } from "../ui/NetworkStatus"; // Routes qui ne nécessitent pas d'authentification const publicRoutes = ["/login", "/register"]; @@ -51,16 +53,7 @@ export default function ClientLayout({ children }: { children: React.ReactNode } useEffect(() => { // Enregistrer le service worker - if ("serviceWorker" in navigator) { - navigator.serviceWorker - .register("/sw.js") - .then(() => { - // Succès silencieux - }) - .catch((error) => { - console.error("Erreur lors de l'enregistrement du Service Worker:", error); - }); - } + registerServiceWorker(); }, []); // Ne pas afficher le header et la sidebar sur les routes publiques @@ -75,6 +68,7 @@ export default function ClientLayout({ children }: { children: React.ReactNode }
{children}
+ diff --git a/src/components/reader/components/Thumbnail.tsx b/src/components/reader/components/Thumbnail.tsx index cf6ed99..520a1a0 100644 --- a/src/components/reader/components/Thumbnail.tsx +++ b/src/components/reader/components/Thumbnail.tsx @@ -106,7 +106,7 @@ export const Thumbnail = forwardRef( return (