feat: add service worker, offline page, and manifest for PWA support
This commit is contained in:
parent
a828432540
commit
954dc266e4
6 changed files with 140 additions and 1 deletions
9
public/icons/icon.svg
Normal file
9
public/icons/icon.svg
Normal 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
22
public/manifest.json
Normal 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
53
public/sw.js
Normal 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 });
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
@ -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
20
src/app/offline.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
16
src/components/service-worker-registration.tsx
Normal file
16
src/components/service-worker-registration.tsx
Normal 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;
|
||||
}
|
||||
Loading…
Reference in a new issue