mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2026-08-28 04:54:04 +00:00
feat: add PWA support
This commit is contained in:
parent
7f0f6137e0
commit
ecdf901dc4
6 changed files with 240 additions and 19 deletions
|
|
@ -5,6 +5,10 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Nginx Proxy Manager</title>
|
||||
<meta name="description" content="Manage Nginx hosts with a simple, powerful interface" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-title" content="NPM" />
|
||||
<meta name="application-name" content="Nginx Proxy Manager" />
|
||||
<link rel="preload" href="/images/logo-no-text.svg" as="image" type="image/svg+xml" fetchPriority="high">
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
|
|
@ -30,7 +34,7 @@
|
|||
<meta
|
||||
name="msapplication-config"
|
||||
content="/images/favicon/browserconfig.xml" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<meta name="theme-color" content="#206bc4" />
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,25 @@
|
|||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/favicons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/images/favicons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
"name": "Nginx Proxy Manager",
|
||||
"short_name": "NPM",
|
||||
"description": "Manage Nginx hosts with a simple, powerful interface",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "any",
|
||||
"theme_color": "#206bc4",
|
||||
"background_color": "#ffffff",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/favicon/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/favicon/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
58
frontend/public/offline.html
Normal file
58
frontend/public/offline.html
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#206bc4" />
|
||||
<title>Nginx Proxy Manager is offline</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
body {
|
||||
align-items: center;
|
||||
background: #f6f8fb;
|
||||
color: #1f2937;
|
||||
display: flex;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
main {
|
||||
background: #fff;
|
||||
border: 1px solid #dce2ea;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 12px 32px rgb(0 0 0 / 8%);
|
||||
max-width: 28rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
img { height: 4.5rem; margin-bottom: 1rem; }
|
||||
h1 { font-size: 1.4rem; margin: 0 0 .5rem; }
|
||||
p { color: #4b5563; line-height: 1.5; margin: 0 0 1.25rem; }
|
||||
button {
|
||||
background: #206bc4;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
padding: .65rem 1rem;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body { background: #111827; color: #f9fafb; }
|
||||
main { background: #1f2937; border-color: #374151; }
|
||||
p { color: #d1d5db; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<img src="/images/logo-no-text.svg" alt="" />
|
||||
<h1>Nginx Proxy Manager is offline</h1>
|
||||
<p>The app shell is installed, but management actions require a connection to the NPM server.</p>
|
||||
<button type="button" onclick="window.location.reload()">Try again</button>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
72
frontend/public/sw.js
Normal file
72
frontend/public/sw.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
const CACHE_VERSION = "npm-pwa-v1";
|
||||
const APP_SHELL = [
|
||||
"/",
|
||||
"/index.html",
|
||||
"/offline.html",
|
||||
"/images/logo-no-text.svg",
|
||||
"/images/favicon/android-chrome-192x192.png",
|
||||
"/images/favicon/android-chrome-512x512.png",
|
||||
];
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_VERSION).then((cache) => cache.addAll(APP_SHELL)),
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.keys()
|
||||
.then((keys) => Promise.all(keys.filter((key) => key !== CACHE_VERSION).map((key) => caches.delete(key))))
|
||||
.then(() => self.clients.claim()),
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
const { request } = event;
|
||||
|
||||
if (request.method !== "GET") {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
if (url.origin !== self.location.origin || url.pathname.startsWith("/api/")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.mode === "navigate") {
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
.then((response) => {
|
||||
const copy = response.clone();
|
||||
caches.open(CACHE_VERSION).then((cache) => cache.put("/index.html", copy));
|
||||
return response;
|
||||
})
|
||||
.catch(async () => (await caches.match("/index.html")) || caches.match("/offline.html")),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
caches.match(request).then((cached) => {
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
return fetch(request).then((response) => {
|
||||
if (response.ok) {
|
||||
const copy = response.clone();
|
||||
caches.open(CACHE_VERSION).then((cache) => cache.put(request, copy));
|
||||
}
|
||||
return response;
|
||||
});
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("message", (event) => {
|
||||
if (event.data?.type === "SKIP_WAITING") {
|
||||
self.skipWaiting();
|
||||
}
|
||||
});
|
||||
|
|
@ -1,16 +1,34 @@
|
|||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useEffect } from "react";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import EasyModal from "ez-modal-react";
|
||||
import { RawIntlProvider } from "react-intl";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import { toast, ToastContainer } from "react-toastify";
|
||||
import { AuthProvider, LocaleProvider, ThemeProvider } from "src/context";
|
||||
import { intl } from "src/locale";
|
||||
import Router from "src/Router.tsx";
|
||||
import { registerPwa } from "src/modules/Pwa";
|
||||
|
||||
// Create a client
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
function App() {
|
||||
useEffect(() => {
|
||||
registerPwa({
|
||||
onOfflineReady: () => {
|
||||
toast.info("Nginx Proxy Manager is ready for offline launch.");
|
||||
},
|
||||
onUpdateReady: (activateUpdate) => {
|
||||
toast.info(
|
||||
<button className="btn btn-primary btn-sm" type="button" onClick={activateUpdate}>
|
||||
Update available. Reload
|
||||
</button>,
|
||||
{ autoClose: false },
|
||||
);
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<RawIntlProvider value={intl}>
|
||||
<LocaleProvider>
|
||||
|
|
|
|||
63
frontend/src/modules/Pwa.ts
Normal file
63
frontend/src/modules/Pwa.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
type RegisterPwaOptions = {
|
||||
onOfflineReady?: () => void;
|
||||
onUpdateReady?: (activateUpdate: () => void) => void;
|
||||
};
|
||||
|
||||
function listenForWaitingWorker(registration: ServiceWorkerRegistration, onUpdateReady?: (activateUpdate: () => void) => void) {
|
||||
const notify = (worker?: ServiceWorker | null) => {
|
||||
if (!worker || !onUpdateReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
onUpdateReady(() => worker.postMessage({ type: "SKIP_WAITING" }));
|
||||
};
|
||||
|
||||
if (registration.waiting) {
|
||||
notify(registration.waiting);
|
||||
}
|
||||
|
||||
registration.addEventListener("updatefound", () => {
|
||||
const worker = registration.installing;
|
||||
worker?.addEventListener("statechange", () => {
|
||||
if (worker.state === "installed" && navigator.serviceWorker.controller) {
|
||||
notify(worker);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function registerPwa({ onOfflineReady, onUpdateReady }: RegisterPwaOptions = {}) {
|
||||
if (!import.meta.env.PROD || !("serviceWorker" in navigator)) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
navigator.serviceWorker
|
||||
.register("/sw.js")
|
||||
.then((registration) => {
|
||||
listenForWaitingWorker(registration, onUpdateReady);
|
||||
if (!navigator.serviceWorker.controller) {
|
||||
registration.addEventListener("updatefound", () => {
|
||||
const worker = registration.installing;
|
||||
worker?.addEventListener("statechange", () => {
|
||||
if (worker.state === "installed") {
|
||||
onOfflineReady?.();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to register service worker", error);
|
||||
});
|
||||
});
|
||||
|
||||
let refreshing = false;
|
||||
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
||||
if (refreshing) {
|
||||
return;
|
||||
}
|
||||
refreshing = true;
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue