fix: minor PWA fixes and implement offline support

Minor fixes such as setting the start_url to `/editor`. Also add
complete offline support by updating configuration.

This is especially nice for mobile users.

I've had some slight issues running this via `npm run dev`.
However, `npm run build && npm run preview` seems to work fine.
This commit is contained in:
Felix Zedén Yverås 2024-07-12 22:44:34 +02:00
parent 18b4267047
commit 22a7603c21
8 changed files with 113 additions and 48 deletions

1
.gitignore vendored
View File

@ -10,6 +10,7 @@ lerna-debug.log*
node_modules node_modules
dist dist
dist-ssr dist-ssr
dev-dist
*.local *.local
# Editor directories and files # Editor directories and files

3
package-lock.json generated
View File

@ -49,7 +49,8 @@
"prettier": "3.2.5", "prettier": "3.2.5",
"tailwindcss": "^3.3.6", "tailwindcss": "^3.3.6",
"vite": "^5.0.11", "vite": "^5.0.11",
"vite-plugin-pwa": "^0.20.0" "vite-plugin-pwa": "^0.20.0",
"workbox-window": "^7.1.0"
} }
}, },
"node_modules/@aashutoshrathi/word-wrap": { "node_modules/@aashutoshrathi/word-wrap": {

View File

@ -51,7 +51,8 @@
"prettier": "3.2.5", "prettier": "3.2.5",
"tailwindcss": "^3.3.6", "tailwindcss": "^3.3.6",
"vite": "^5.0.11", "vite": "^5.0.11",
"vite-plugin-pwa": "^0.20.0" "vite-plugin-pwa": "^0.20.0",
"workbox-window": "^7.1.0"
}, },
"overrides": { "overrides": {
"follow-redirects": "^1.15.4" "follow-redirects": "^1.15.4"

View File

@ -9,6 +9,7 @@ import LandingPage from "./pages/LandingPage";
import SettingsContextProvider from "./context/SettingsContext"; import SettingsContextProvider from "./context/SettingsContext";
import useSettings from "./hooks/useSettings"; import useSettings from "./hooks/useSettings";
import NotFound from "./pages/NotFound"; import NotFound from "./pages/NotFound";
import { PwaUpdatePrompt } from "./components/PwaUpdatePrompt";
export default function App() { export default function App() {
return ( return (
@ -53,6 +54,7 @@ export default function App() {
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
<PwaUpdatePrompt />
</SettingsContextProvider> </SettingsContextProvider>
); );
} }

View File

@ -1,6 +1,8 @@
import { useState } from "react"; import { useState } from "react";
import { useRegisterSW } from "virtual:pwa-register/react";
import { import {
IconCaretdown, IconCaretdown,
IconCloud,
IconChevronRight, IconChevronRight,
IconChevronUp, IconChevronUp,
IconChevronDown, IconChevronDown,
@ -18,6 +20,7 @@ import {
InputNumber, InputNumber,
Tooltip, Tooltip,
Spin, Spin,
Tag,
Toast, Toast,
Popconfirm, Popconfirm,
} from "@douyinfe/semi-ui"; } from "@douyinfe/semi-ui";
@ -1287,9 +1290,13 @@ export default function ControlPanel({
}); });
useHotkeys("ctrl+alt+w, meta+alt+w", fitWindow, { preventDefault: true }); useHotkeys("ctrl+alt+w, meta+alt+w", fitWindow, { preventDefault: true });
const {
offlineReady: [isOfflineReady],
} = useRegisterSW();
return ( return (
<> <>
{layout.header && header()} {layout.header && header(isOfflineReady)}
{layout.toolbar && toolbar()} {layout.toolbar && toolbar()}
<Modal <Modal
modal={modal} modal={modal}
@ -1498,10 +1505,10 @@ export default function ControlPanel({
} }
} }
function header() { function header(isOfflineReady) {
return ( return (
<nav className="flex justify-between pt-1 items-center whitespace-nowrap"> <nav className="flex justify-between pt-1 items-center whitespace-nowrap">
<div className="flex justify-start items-center"> <div className="flex justify-start items-center grow">
<Link to="/"> <Link to="/">
<img <img
width={54} width={54}
@ -1510,7 +1517,7 @@ export default function ControlPanel({
className="ms-8 min-w-[54px]" className="ms-8 min-w-[54px]"
/> />
</Link> </Link>
<div className="ms-1 mt-1"> <div className="ms-1 sm:me-1 xl:me-6 mt-1 grow">
<div className="flex items-center ms-3 gap-2"> <div className="flex items-center ms-3 gap-2">
{databases[database].image && ( {databases[database].image && (
<img <img
@ -1535,7 +1542,7 @@ export default function ControlPanel({
</div> </div>
{(showEditName || modal === MODAL.RENAME) && <IconEdit />} {(showEditName || modal === MODAL.RENAME) && <IconEdit />}
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-start items-center">
<div className="flex justify-start text-md select-none me-2"> <div className="flex justify-start text-md select-none me-2">
{Object.keys(menu).map((category) => ( {Object.keys(menu).map((category) => (
<Dropdown <Dropdown
@ -1642,6 +1649,13 @@ export default function ControlPanel({
> >
{getState()} {getState()}
</Button> </Button>
{isOfflineReady && (
<span className="ms-auto">
<Tag prefixIcon={<IconCloud />} size="large">
{t("available_offline")}
</Tag>
</span>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,34 @@
import { useRegisterSW } from "virtual:pwa-register/react";
import { Typography, Toast } from "@douyinfe/semi-ui";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
const { Text } = Typography;
export function PwaUpdatePrompt() {
const { t } = useTranslation();
const {
needRefresh: [isRefreshNeeded],
updateServiceWorker,
} = useRegisterSW();
useEffect(() => {
if (!isRefreshNeeded) return;
Toast.info({
duration: 0, // indefinite
content: (
<div>
<h5>{t("update_available")}</h5>
<p className="text-xs">{t("reload_page_to_update")}</p>
<div className="mt-2">
<Text link onClick={updateServiceWorker}>
{t("reload_now")}
</Text>
</div>
</div>
),
});
}, [isRefreshNeeded, updateServiceWorker, t]);
return <></>;
}

View File

@ -6,6 +6,10 @@ const english = {
const en = { const en = {
translation: { translation: {
available_offline: "Available offline",
update_available: "An updated version of drawDB is available!",
reload_page_to_update: "Reload the page to update",
reload_now: "Reload now",
report_bug: "Report a bug", report_bug: "Report a bug",
import: "Import", import: "Import",
file: "File", file: "File",

View File

@ -1,47 +1,55 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import { VitePWA } from "vite-plugin-pwa"; import { VitePWA } from "vite-plugin-pwa";
const manifestForPlugIn = {
registerType: "prompt",
includeAssests: ["favicon.ico", "apple-touc-icon.png"],
manifest: {
name: "DrawDB",
short_name: "DrawDB",
icons: [
{
src: "/pwa-192x192.png",
sizes: "192x192",
type: "image/png",
purpose: "any",
},
{
src: "/pwa-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "any",
},
{
src: "/pwa-maskable-192x192.png",
sizes: "192x192",
type: "image/png",
purpose: "maskable",
},
{
src: "/pwa-maskable-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
],
start_url: "/editor",
display: "standalone",
background_color: "#14475b",
theme_color: "#14475b",
description:
"Free, simple, and intuitive database design tool and SQL generator.",
},
};
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react(), VitePWA(manifestForPlugIn)], plugins: [
react(),
VitePWA({
workbox: {
globPatterns: ["**/*"],
maximumFileSizeToCacheInBytes: 15_000_000,
},
includeAssets: ["**/*"],
registerType: "prompt",
manifest: {
name: "DrawDB",
short_name: "DrawDB",
icons: [
{
src: "/pwa-192x192.png",
sizes: "192x192",
type: "image/png",
purpose: "any",
},
{
src: "/pwa-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "any",
},
{
src: "/pwa-maskable-192x192.png",
sizes: "192x192",
type: "image/png",
purpose: "maskable",
},
{
src: "/pwa-maskable-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
],
scope: "/",
start_url: "/editor",
display: "standalone",
background_color: "#14475b",
theme_color: "#14475b",
description:
"Free, simple, and intuitive database design tool and SQL generator.",
},
}),
],
}); });