From 954dc266e41601ddcddcd79994db649abcaae1c1 Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Sun, 24 May 2026 17:02:07 +0200 Subject: [PATCH] feat: add service worker, offline page, and manifest for PWA support --- public/icons/icon.svg | 9 ++++ public/manifest.json | 22 ++++++++ public/sw.js | 53 +++++++++++++++++++ src/app/layout.tsx | 21 +++++++- src/app/offline.tsx | 20 +++++++ .../service-worker-registration.tsx | 16 ++++++ 6 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 public/icons/icon.svg create mode 100644 public/manifest.json create mode 100644 public/sw.js create mode 100644 src/app/offline.tsx create mode 100644 src/components/service-worker-registration.tsx diff --git a/public/icons/icon.svg b/public/icons/icon.svg new file mode 100644 index 0000000..182daef --- /dev/null +++ b/public/icons/icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..502d693 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,22 @@ +{ + "name": "WFDASH", + "short_name": "WFDASH", + "description": "Business CRM — Quotes, Orders, Customers", + "start_url": "/", + "display": "standalone", + "background_color": "#FDF6F5", + "theme_color": "#E85D4C", + "orientation": "any", + "icons": [ + { + "src": "/icons/icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any maskable" + } + ], + "categories": ["business", "productivity"], + "screenshots": [], + "related_applications": [], + "prefer_related_applications": false +} \ No newline at end of file diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..f8605b9 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,53 @@ +const CACHE_NAME = "wfdash-v1"; +const OFFLINE_URL = "/offline"; + +const PRECACHE_ASSETS = [ + "/", + "/offline", + "/manifest.json", +]; + +self.addEventListener("install", (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + return cache.addAll(PRECACHE_ASSETS); + }) + ); + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches.keys().then((keys) => { + return Promise.all( + keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)) + ); + }) + ); + self.clients.claim(); +}); + +self.addEventListener("fetch", (event) => { + if (event.request.method !== "GET") return; + if (event.request.url.includes("/api/")) return; + + event.respondWith( + fetch(event.request) + .then((response) => { + const clone = response.clone(); + caches.open(CACHE_NAME).then((cache) => { + cache.put(event.request, clone); + }); + return response; + }) + .catch(() => { + return caches.match(event.request).then((cached) => { + if (cached) return cached; + if (event.request.mode === "navigate") { + return caches.match(OFFLINE_URL); + } + return new Response("Offline", { status: 503 }); + }); + }) + ); +}); \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index dd983ce..f8b85f6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,10 +1,11 @@ -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { ThemeProvider } from "@/components/theme-provider"; import { AuthProvider } from "@/components/auth-provider"; import { Toaster } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; +import { ServiceWorkerRegistration } from "@/components/service-worker-registration"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -16,9 +17,26 @@ const geistMono = Geist_Mono({ subsets: ["latin"], }); +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + viewportFit: "cover", + themeColor: "#E85D4C", +}; + export const metadata: Metadata = { title: "WFDASH - Business CRM", description: "Business CRM for managing customers, companies, suppliers, quotes, and orders", + manifest: "/manifest.json", + appleWebApp: { + capable: true, + statusBarStyle: "default", + title: "WFDASH", + }, + icons: { + icon: "/icons/icon.svg", + apple: "/icons/icon.svg", + }, }; export default function RootLayout({ children }: { children: React.ReactNode }) { @@ -29,6 +47,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) suppressHydrationWarning > + diff --git a/src/app/offline.tsx b/src/app/offline.tsx new file mode 100644 index 0000000..7988185 --- /dev/null +++ b/src/app/offline.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { WifiOff } from "lucide-react"; +import Link from "next/link"; + +export default function OfflinePage() { + return ( +
+ +

You are offline

+

+ Looks like you have lost your internet connection. Please check your connection and try again. +

+ + + +
+ ); +} \ No newline at end of file diff --git a/src/components/service-worker-registration.tsx b/src/components/service-worker-registration.tsx new file mode 100644 index 0000000..9054f37 --- /dev/null +++ b/src/components/service-worker-registration.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { useEffect } from "react"; + +export function ServiceWorkerRegistration() { + useEffect(() => { + if ("serviceWorker" in navigator) { + navigator.serviceWorker + .register("/sw.js") + .then((reg) => console.log("SW registered:", reg.scope)) + .catch((err) => console.error("SW registration failed:", err)); + } + }, []); + + return null; +} \ No newline at end of file