feat: offline pages mode
This commit is contained in:
@@ -34,6 +34,11 @@
|
|||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
button {
|
button {
|
||||||
background-color: #4f46e5;
|
background-color: #4f46e5;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -47,6 +52,12 @@
|
|||||||
button:hover {
|
button:hover {
|
||||||
background-color: #4338ca;
|
background-color: #4338ca;
|
||||||
}
|
}
|
||||||
|
button.secondary {
|
||||||
|
background-color: #475569;
|
||||||
|
}
|
||||||
|
button.secondary:hover {
|
||||||
|
background-color: #334155;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -56,7 +67,10 @@
|
|||||||
Il semble que vous n'ayez pas de connexion internet. Certaines fonctionnalités de
|
Il semble que vous n'ayez pas de connexion internet. Certaines fonctionnalités de
|
||||||
StripStream peuvent ne pas être disponibles en mode hors ligne.
|
StripStream peuvent ne pas être disponibles en mode hors ligne.
|
||||||
</p>
|
</p>
|
||||||
<button onclick="window.location.reload()">Réessayer</button>
|
<div class="buttons">
|
||||||
|
<button class="secondary" onclick="window.history.back()">Retour</button>
|
||||||
|
<button onclick="window.location.reload()">Réessayer</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
137
public/sw.js
137
public/sw.js
@@ -2,7 +2,6 @@ const CACHE_NAME = "stripstream-cache-v1";
|
|||||||
const OFFLINE_PAGE = "/offline.html";
|
const OFFLINE_PAGE = "/offline.html";
|
||||||
|
|
||||||
const STATIC_ASSETS = [
|
const STATIC_ASSETS = [
|
||||||
"/",
|
|
||||||
"/offline.html",
|
"/offline.html",
|
||||||
"/manifest.json",
|
"/manifest.json",
|
||||||
"/favicon.svg",
|
"/favicon.svg",
|
||||||
@@ -12,12 +11,7 @@ const STATIC_ASSETS = [
|
|||||||
|
|
||||||
// Installation du service worker
|
// Installation du service worker
|
||||||
self.addEventListener("install", (event) => {
|
self.addEventListener("install", (event) => {
|
||||||
event.waitUntil(
|
event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS)));
|
||||||
caches.open(CACHE_NAME).then((cache) => {
|
|
||||||
return cache.addAll(STATIC_ASSETS);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
self.skipWaiting();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Activation et nettoyage des anciens caches
|
// 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) => {
|
self.addEventListener("fetch", (event) => {
|
||||||
// Pour les requêtes API, on utilise "Network First" avec un timeout
|
// Ignorer les requêtes non GET
|
||||||
if (event.request.url.includes("/api/")) {
|
if (event.request.method !== "GET") return;
|
||||||
// event.respondWith(
|
|
||||||
// Promise.race([
|
// Ignorer les ressources webpack
|
||||||
// fetch(event.request.clone())
|
if (isWebpackResource(event.request.url)) return;
|
||||||
// .then((response) => {
|
|
||||||
// // Ne mettre en cache que les réponses réussies
|
// Pour les ressources statiques de Next.js et les autres requêtes
|
||||||
// if (response.ok) {
|
event.respondWith(
|
||||||
// const responseToCache = response.clone();
|
fetch(event.request)
|
||||||
// caches.open(CACHE_NAME).then((cache) => {
|
.then((response) => {
|
||||||
// cache.put(event.request, responseToCache);
|
// Mettre en cache les ressources statiques de Next.js et les pages
|
||||||
// });
|
if (
|
||||||
// }
|
response.ok &&
|
||||||
// return response;
|
(isNextStaticResource(event.request.url) || event.request.mode === "navigate")
|
||||||
// })
|
) {
|
||||||
// .catch(() => {
|
const responseToCache = response.clone();
|
||||||
// // En cas d'erreur réseau, essayer le cache
|
caches.open(CACHE_NAME).then((cache) => {
|
||||||
// return caches.match(event.request).then((cachedResponse) => {
|
cache.put(event.request, responseToCache);
|
||||||
// if (cachedResponse) {
|
});
|
||||||
// return cachedResponse;
|
}
|
||||||
// }
|
return response;
|
||||||
// // Si pas de cache, renvoyer une erreur appropriée
|
})
|
||||||
// return new Response(JSON.stringify({ error: "Hors ligne" }), {
|
.catch(async () => {
|
||||||
// status: 503,
|
const cache = await caches.open(CACHE_NAME);
|
||||||
// headers: { "Content-Type": "application/json" },
|
const cachedResponse = await cache.match(event.request);
|
||||||
// });
|
|
||||||
// });
|
if (cachedResponse) {
|
||||||
// }),
|
return cachedResponse;
|
||||||
// // Timeout après 5 secondes
|
}
|
||||||
// new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 5000)).catch(
|
|
||||||
// () => {
|
// Si c'est une navigation, renvoyer la page hors ligne
|
||||||
// return caches.match(event.request).then((cachedResponse) => {
|
if (event.request.mode === "navigate") {
|
||||||
// if (cachedResponse) {
|
return cache.match(OFFLINE_PAGE);
|
||||||
// return cachedResponse;
|
}
|
||||||
// }
|
|
||||||
// return new Response(JSON.stringify({ error: "Timeout" }), {
|
return new Response(JSON.stringify({ error: "Hors ligne" }), {
|
||||||
// status: 504,
|
status: 503,
|
||||||
// headers: { "Content-Type": "application/json" },
|
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 });
|
|
||||||
// });
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { InstallPWA } from "../ui/InstallPWA";
|
|||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { PreferencesProvider } from "@/contexts/PreferencesContext";
|
import { PreferencesProvider } from "@/contexts/PreferencesContext";
|
||||||
|
import { registerServiceWorker } from "@/lib/registerSW";
|
||||||
|
import { NetworkStatus } from "../ui/NetworkStatus";
|
||||||
|
|
||||||
// Routes qui ne nécessitent pas d'authentification
|
// Routes qui ne nécessitent pas d'authentification
|
||||||
const publicRoutes = ["/login", "/register"];
|
const publicRoutes = ["/login", "/register"];
|
||||||
@@ -51,16 +53,7 @@ export default function ClientLayout({ children }: { children: React.ReactNode }
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Enregistrer le service worker
|
// Enregistrer le service worker
|
||||||
if ("serviceWorker" in navigator) {
|
registerServiceWorker();
|
||||||
navigator.serviceWorker
|
|
||||||
.register("/sw.js")
|
|
||||||
.then(() => {
|
|
||||||
// Succès silencieux
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Erreur lors de l'enregistrement du Service Worker:", error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Ne pas afficher le header et la sidebar sur les routes publiques
|
// Ne pas afficher le header et la sidebar sur les routes publiques
|
||||||
@@ -75,6 +68,7 @@ export default function ClientLayout({ children }: { children: React.ReactNode }
|
|||||||
<main className={`${!isPublicRoute ? "container pt-4 md:pt-8" : ""}`}>{children}</main>
|
<main className={`${!isPublicRoute ? "container pt-4 md:pt-8" : ""}`}>{children}</main>
|
||||||
<InstallPWA />
|
<InstallPWA />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
<NetworkStatus />
|
||||||
</div>
|
</div>
|
||||||
</PreferencesProvider>
|
</PreferencesProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export const Thumbnail = forwardRef<HTMLButtonElement, ThumbnailProps>(
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
ref={(node) => {
|
ref={(node) => {
|
||||||
thumbnailRef.current = node;
|
// thumbnailRef.current = node;
|
||||||
if (typeof ref === "function") {
|
if (typeof ref === "function") {
|
||||||
ref(node);
|
ref(node);
|
||||||
} else if (ref) {
|
} else if (ref) {
|
||||||
|
|||||||
17
src/components/ui/NetworkStatus.tsx
Normal file
17
src/components/ui/NetworkStatus.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useNetworkStatus } from "@/hooks/useNetworkStatus";
|
||||||
|
import { WifiOff } from "lucide-react";
|
||||||
|
|
||||||
|
export function NetworkStatus() {
|
||||||
|
const isOnline = useNetworkStatus();
|
||||||
|
|
||||||
|
if (isOnline) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed bottom-4 left-4 z-[100] flex items-center gap-2 rounded-lg bg-destructive px-4 py-2 text-sm text-destructive-foreground shadow-lg">
|
||||||
|
<WifiOff className="h-4 w-4" />
|
||||||
|
<span>Hors ligne</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const Separator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
|
||||||
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
|
|
||||||
<SeparatorPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
decorative={decorative}
|
|
||||||
orientation={orientation}
|
|
||||||
className={cn(
|
|
||||||
"shrink-0 bg-border",
|
|
||||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
|
||||||
|
|
||||||
export { Separator };
|
|
||||||
29
src/hooks/useNetworkStatus.ts
Normal file
29
src/hooks/useNetworkStatus.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
export const useNetworkStatus = () => {
|
||||||
|
const [isOnline, setIsOnline] = useState<boolean>(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Fonction pour mettre à jour l'état
|
||||||
|
const updateOnlineStatus = () => {
|
||||||
|
setIsOnline(navigator.onLine);
|
||||||
|
};
|
||||||
|
|
||||||
|
// État initial
|
||||||
|
setIsOnline(navigator.onLine);
|
||||||
|
|
||||||
|
// Écouter les changements d'état de la connexion
|
||||||
|
window.addEventListener("online", updateOnlineStatus);
|
||||||
|
window.addEventListener("offline", updateOnlineStatus);
|
||||||
|
|
||||||
|
// Nettoyage
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("online", updateOnlineStatus);
|
||||||
|
window.removeEventListener("offline", updateOnlineStatus);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return isOnline;
|
||||||
|
};
|
||||||
12
src/lib/registerSW.ts
Normal file
12
src/lib/registerSW.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export const registerServiceWorker = async () => {
|
||||||
|
if (typeof window === "undefined" || !("serviceWorker" in navigator)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const registration = await navigator.serviceWorker.register("/sw.js");
|
||||||
|
console.log("Service Worker registered with scope:", registration.scope);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Service Worker registration failed:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user