fix pwa fully

This commit is contained in:
fccview
2025-09-01 15:23:59 +01:00
parent da11d3503e
commit 7383a13c13
8 changed files with 109 additions and 23 deletions

View File

@@ -0,0 +1,58 @@
"use client";
import { useCallback, useEffect, useState } from "react";
type BeforeInstallPromptEvent = Event & {
prompt: () => Promise<void>;
userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
};
export const PWAInstallPrompt = (): JSX.Element | null => {
const [deferred, setDeferred] = useState<BeforeInstallPromptEvent | null>(
null
);
const [isInstalled, setIsInstalled] = useState<boolean>(false);
useEffect(() => {
if (typeof window === "undefined") return;
const onBeforeInstallPrompt = (e: Event) => {
e.preventDefault();
setDeferred(e as BeforeInstallPromptEvent);
};
const onAppInstalled = () => {
setDeferred(null);
setIsInstalled(true);
};
window.addEventListener("beforeinstallprompt", onBeforeInstallPrompt);
window.addEventListener("appinstalled", onAppInstalled);
if (window.matchMedia("(display-mode: standalone)").matches) {
setIsInstalled(true);
}
return () => {
window.removeEventListener("beforeinstallprompt", onBeforeInstallPrompt);
window.removeEventListener("appinstalled", onAppInstalled);
};
}, []);
const onInstall = useCallback(async () => {
if (!deferred) return;
try {
await deferred.prompt();
const choice = await deferred.userChoice;
if (choice.outcome === "accepted") {
setDeferred(null);
}
} catch (_err) {}
}, [deferred]);
if (isInstalled || !deferred) return null;
return (
<button
className="px-3 py-1 rounded-md border border-border/50 bg-background/80 hover:bg-background/60"
onClick={onInstall}
>
Install App
</button>
);
};

View File

@@ -0,0 +1,23 @@
"use client";
import { useEffect } from "react";
export const ServiceWorkerRegister = (): null => {
useEffect(() => {
if (typeof window === "undefined") return;
if (!("serviceWorker" in navigator)) return;
const register = async () => {
try {
const registrations = await navigator.serviceWorker.getRegistrations();
const alreadyRegistered = registrations.some((r) =>
r.scope.endsWith("/")
);
if (alreadyRegistered) return;
await navigator.serviceWorker.register("/sw.js", { scope: "/" });
} catch (_err) {}
};
register();
}, []);
return null;
};

View File

@@ -92,7 +92,7 @@ export const Sidebar = forwardRef<HTMLDivElement, SidebarProps>(
>
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="absolute -right-3 top-6 w-6 h-6 bg-background border border-border rounded-full items-center justify-center hover:bg-accent transition-colors z-40 hidden lg:flex"
className="absolute -right-3 top-[21.5vh] w-6 h-6 bg-background border border-border rounded-full items-center justify-center hover:bg-accent transition-colors z-40 hidden lg:flex"
>
{isCollapsed ? (
<ChevronRight className="h-3 w-3" />

View File

@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { JetBrains_Mono, Inter } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "./_components/ui/ThemeProvider";
import { ServiceWorkerRegister } from "./_components/ui/ServiceWorkerRegister";
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
@@ -55,24 +56,7 @@ export default function RootLayout({
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="Cr*nMaster" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="apple-touch-icon" href="/icon-192x192.png" />
<script
dangerouslySetInnerHTML={{
__html: `
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js')
.then(function(registration) {
console.log('SW registered: ', registration);
})
.catch(function(registrationError) {
console.log('SW registration failed: ', registrationError);
});
});
}
`,
}}
/>
<link rel="apple-touch-icon" href="/logo.png" />
</head>
<body className={`${inter.variable} ${jetbrainsMono.variable} font-sans`}>
<ThemeProvider
@@ -83,6 +67,7 @@ export default function RootLayout({
>
{children}
</ThemeProvider>
<ServiceWorkerRegister />
</body>
</html>
);

View File

@@ -5,6 +5,7 @@ import { fetchScripts } from "./_server/actions/scripts";
import { ThemeToggle } from "./_components/ui/ThemeToggle";
import { LogoutButton } from "./_components/ui/LogoutButton";
import { ToastContainer } from "./_components/ui/Toast";
import { PWAInstallPrompt } from "./_components/ui/PWAInstallPrompt";
export const dynamic = "force-dynamic";
export default async function Home() {
@@ -90,6 +91,7 @@ export default async function Home() {
<div className="flex items-center gap-2 fixed bottom-4 left-4 lg:right-4 lg:left-auto z-10 bg-background/80 backdrop-blur-md border border-border/50 rounded-lg p-1">
<ThemeToggle />
<PWAInstallPrompt />
</div>
</div>
);

View File

@@ -32,6 +32,6 @@ export function middleware(request: NextRequest) {
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
"/((?!_next/static|_next/image|favicon.ico|site.webmanifest|sw.js|app-icons).*)",
],
}

View File

@@ -2,11 +2,29 @@ const withPWA = require('next-pwa')({
dest: 'public',
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === 'development'
disable: process.env.NODE_ENV === 'development',
buildExcludes: [/middleware-manifest\.json$/]
})
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: '/manifest.json',
headers: [
{ key: 'Content-Type', value: 'application/manifest+json' },
],
},
{
source: '/sw.js',
headers: [
{ key: 'Service-Worker-Allowed', value: '/' },
{ key: 'Cache-Control', value: 'no-cache' },
],
},
]
},
}
module.exports = withPWA(nextConfig)

View File

@@ -12,13 +12,13 @@
"src": "/logo.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
"purpose": "any maskable"
},
{
"src": "/logo.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
"purpose": "any maskable"
}
],
"categories": ["productivity", "utilities"],