feat: add service worker, offline page, and manifest for PWA support

This commit is contained in:
SJCoetz 2026-05-24 17:02:07 +02:00
parent a828432540
commit 954dc266e4
6 changed files with 140 additions and 1 deletions

9
public/icons/icon.svg Normal file
View file

@ -0,0 +1,9 @@
<svg width="192" height="192" viewBox="0 0 192 192" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="192" height="192" rx="32" fill="#E85D4C"/>
<rect x="32" y="48" width="128" height="96" rx="12" fill="white" fill-opacity="0.95"/>
<rect x="48" y="64" width="60" height="8" rx="4" fill="#E85D4C"/>
<rect x="48" y="80" width="96" height="6" rx="3" fill="#E85D4C" fill-opacity="0.4"/>
<rect x="48" y="94" width="80" height="6" rx="3" fill="#E85D4C" fill-opacity="0.4"/>
<rect x="48" y="108" width="88" height="6" rx="3" fill="#E85D4C" fill-opacity="0.4"/>
<rect x="48" y="124" width="40" height="12" rx="6" fill="#E85D4C"/>
</svg>

After

Width:  |  Height:  |  Size: 653 B

22
public/manifest.json Normal file
View file

@ -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
}

53
public/sw.js Normal file
View file

@ -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 });
});
})
);
});

View file

@ -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
>
<body className="min-h-full flex flex-col">
<ServiceWorkerRegistration />
<ThemeProvider>
<TooltipProvider>
<AuthProvider>

20
src/app/offline.tsx Normal file
View file

@ -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 (
<div className="flex flex-col items-center justify-center min-h-screen p-8 text-center">
<WifiOff className="h-16 w-16 text-muted-foreground mb-6" />
<h1 className="text-2xl font-bold mb-2">You are offline</h1>
<p className="text-muted-foreground mb-6 max-w-sm">
Looks like you have lost your internet connection. Please check your connection and try again.
</p>
<Link href="/">
<Button>Try Again</Button>
</Link>
</div>
);
}

View file

@ -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;
}