Feature: PWA (#479)

* feat: add PWA support with offline capabilities

- Add web app manifest with icons and theme configuration
- Add service worker with cache-first strategy for static assets
- Add useServiceWorker hook for SW registration
- Add PWA meta tags and Apple-specific tags to index.html
- Update vite.config.ts for optimal asset caching

* Update package-lock.json
This commit was merged in pull request #479.
This commit is contained in:
Nunzio Marfè
2026-01-12 08:36:03 +01:00
committed by GitHub
parent ceff07c685
commit 816172d67b
6 changed files with 247 additions and 3 deletions

View File

@@ -0,0 +1,71 @@
import { useEffect, useState, useCallback } from "react";
import { isElectron } from "@/ui/main-axios";
interface ServiceWorkerState {
isSupported: boolean;
isRegistered: boolean;
updateAvailable: boolean;
}
/**
* Hook to manage PWA Service Worker registration.
* Only registers in production web environment (not in Electron).
*/
export function useServiceWorker(): ServiceWorkerState {
const [state, setState] = useState<ServiceWorkerState>({
isSupported: false,
isRegistered: false,
updateAvailable: false,
});
const handleUpdateFound = useCallback(
(registration: ServiceWorkerRegistration) => {
const newWorker = registration.installing;
if (!newWorker) return;
newWorker.addEventListener("statechange", () => {
if (
newWorker.state === "installed" &&
navigator.serviceWorker.controller
) {
setState((prev) => ({ ...prev, updateAvailable: true }));
console.log("[SW] Update available");
}
});
},
[],
);
useEffect(() => {
const isSupported =
"serviceWorker" in navigator && !isElectron() && import.meta.env.PROD;
setState((prev) => ({ ...prev, isSupported }));
if (!isSupported) return;
const registerSW = async () => {
try {
const registration = await navigator.serviceWorker.register("/sw.js");
console.log("[SW] Registered:", registration.scope);
setState((prev) => ({ ...prev, isRegistered: true }));
registration.addEventListener("updatefound", () =>
handleUpdateFound(registration),
);
} catch (error) {
console.error("[SW] Registration failed:", error);
}
};
if (document.readyState === "complete") {
registerSW();
} else {
window.addEventListener("load", registerSW);
return () => window.removeEventListener("load", registerSW);
}
}, [handleUpdateFound]);
return state;
}

View File

@@ -8,6 +8,7 @@ import { ThemeProvider } from "@/components/theme-provider";
import { ElectronVersionCheck } from "@/ui/desktop/user/ElectronVersionCheck.tsx";
import "./i18n/i18n";
import { isElectron } from "./ui/main-axios.ts";
import { useServiceWorker } from "@/hooks/use-service-worker";
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
@@ -58,6 +59,9 @@ function RootApp() {
const isMobile = width < 768;
const [showVersionCheck, setShowVersionCheck] = useState(true);
// PWA Service Worker registration (production web only)
useServiceWorker();
const userAgent =
navigator.userAgent || navigator.vendor || (window as any).opera || "";
const isTermixMobile = /Termix-Mobile/.test(userAgent);
@@ -114,3 +118,4 @@ createRoot(document.getElementById("root")!).render(
</ThemeProvider>
</StrictMode>,
);