mirror of
https://github.com/Unkn0wnCat/data-toolbox-site.git
synced 2025-04-28 09:36:24 +02:00
Add update functionality and typescript support
This commit is contained in:
parent
1d5261f058
commit
8ef979d36a
10 changed files with 3092 additions and 3507 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -21,3 +21,6 @@
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
|
# certs
|
||||||
|
/*.pem
|
||||||
|
|
13
package.json
13
package.json
|
@ -7,11 +7,14 @@
|
||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
"@testing-library/user-event": "^12.1.10",
|
"@testing-library/user-event": "^12.1.10",
|
||||||
|
"@types/jest": "^27.4.1",
|
||||||
|
"@types/node": "^17.0.31",
|
||||||
|
"@types/react": "^18.0.8",
|
||||||
|
"@types/react-dom": "^18.0.3",
|
||||||
"i18next": "^20.2.1",
|
"i18next": "^20.2.1",
|
||||||
"i18next-browser-languagedetector": "^6.1.0",
|
"i18next-browser-languagedetector": "^6.1.0",
|
||||||
"i18next-http-backend": "^1.2.1",
|
"i18next-http-backend": "^1.2.1",
|
||||||
"lucide-react": "^0.15.16",
|
"lucide-react": "^0.15.16",
|
||||||
"node-sass": "^5.0.0",
|
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
|
@ -19,13 +22,16 @@
|
||||||
"react-prerendered-component": "^1.2.4",
|
"react-prerendered-component": "^1.2.4",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
|
"sass": "^1.51.0",
|
||||||
|
"typescript": "^4.6.4",
|
||||||
"web-vitals": "^1.0.1"
|
"web-vitals": "^1.0.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject",
|
||||||
|
"buildServe": "yarn build && serve -s build --ssl-cert ./192.168.1.150.pem --ssl-key ./192.168.1.150-key.pem"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
|
@ -44,5 +50,8 @@
|
||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"serve": "^13.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
@import "../common";
|
@import "../common";
|
||||||
|
|
||||||
@mixin boxMessageColor($baseColor) {
|
@mixin boxMessageColor($baseColor) {
|
||||||
$bgColor: adjust-color($baseColor, null, null, null, null, null, 30);
|
$bgColor: adjust-color($baseColor, $lightness: 30);
|
||||||
background-color: $bgColor;
|
background-color: $bgColor;
|
||||||
|
|
||||||
.icon > svg {
|
.icon > svg {
|
||||||
|
@ -10,11 +10,11 @@
|
||||||
|
|
||||||
|
|
||||||
@media(prefers-color-scheme: dark) {
|
@media(prefers-color-scheme: dark) {
|
||||||
$bgColor: adjust-color($baseColor, null, null, null, null, null, -35);
|
$bgColor: adjust-color($baseColor, $lightness: -35);
|
||||||
background-color: $bgColor;
|
background-color: $bgColor;
|
||||||
|
|
||||||
.icon > svg {
|
.icon > svg {
|
||||||
color: adjust-color($baseColor, null, null, null, null, null, 5);
|
color: adjust-color($baseColor, $lightness: 5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,13 +4,15 @@ import { Link } from "react-router-dom";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import * as styles from "./Navigation.module.scss";
|
import * as styles from "./Navigation.module.scss";
|
||||||
import { Globe } from "lucide-react";
|
import { Globe, RefreshCw } from "lucide-react";
|
||||||
import LanguageChooser from "./LanguageChooser";
|
import LanguageChooser from "./LanguageChooser";
|
||||||
|
import ServiceWorkerAPI from "../services/serviceWorkers"
|
||||||
|
|
||||||
const Navigation = () => {
|
const Navigation = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [langChooserActive, setLangChooserActive] = useState(false);
|
const [langChooserActive, setLangChooserActive] = useState(false);
|
||||||
|
const updateAvailable = ServiceWorkerAPI.useUpdatePending()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={styles.navigation}>
|
<header className={styles.navigation}>
|
||||||
|
@ -20,6 +22,8 @@ const Navigation = () => {
|
||||||
<Link to={"/tools"}>{t("site.navigation.tools")}</Link>
|
<Link to={"/tools"}>{t("site.navigation.tools")}</Link>
|
||||||
<Link to={"/about"}>{t("site.navigation.about")}</Link>
|
<Link to={"/about"}>{t("site.navigation.about")}</Link>
|
||||||
<Link to={"#"} onClick={() => {setLangChooserActive(true)}} title="Change Language"><Globe/></Link>
|
<Link to={"#"} onClick={() => {setLangChooserActive(true)}} title="Change Language"><Globe/></Link>
|
||||||
|
{updateAvailable && <Link to={"#"} onClick={() => {ServiceWorkerAPI.forceUpdate()}} title="Update Available"><RefreshCw /></Link>}
|
||||||
|
|
||||||
<LanguageChooser active={langChooserActive} onDone={() => {setLangChooserActive(false)}} />
|
<LanguageChooser active={langChooserActive} onDone={() => {setLangChooserActive(false)}} />
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
1
src/react-app-env.d.ts
vendored
Normal file
1
src/react-app-env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="react-scripts" />
|
|
@ -1,113 +0,0 @@
|
||||||
/* eslint-disable no-restricted-globals */
|
|
||||||
|
|
||||||
// This service worker can be customized!
|
|
||||||
// See https://developers.google.com/web/tools/workbox/modules
|
|
||||||
// for the list of available Workbox modules, or add any other
|
|
||||||
// code you'd like.
|
|
||||||
// You can also remove this file if you'd prefer not to use a
|
|
||||||
// service worker, and the Workbox build step will be skipped.
|
|
||||||
|
|
||||||
import { clientsClaim } from 'workbox-core';
|
|
||||||
import { ExpirationPlugin } from 'workbox-expiration';
|
|
||||||
import {CacheableResponsePlugin} from 'workbox-cacheable-response';
|
|
||||||
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
|
|
||||||
import { registerRoute } from 'workbox-routing';
|
|
||||||
import { StaleWhileRevalidate, CacheFirst } from 'workbox-strategies';
|
|
||||||
import {setCacheNameDetails} from 'workbox-core';
|
|
||||||
import {BackgroundSyncPlugin} from 'workbox-background-sync';
|
|
||||||
|
|
||||||
setCacheNameDetails({
|
|
||||||
prefix: 'kevins-toolbox',
|
|
||||||
suffix: 'v1',
|
|
||||||
precache: 'precache',
|
|
||||||
runtime: 'runtime'
|
|
||||||
});
|
|
||||||
|
|
||||||
clientsClaim();
|
|
||||||
|
|
||||||
// Precache all of the assets generated by your build process.
|
|
||||||
// Their URLs are injected into the manifest variable below.
|
|
||||||
// This variable must be present somewhere in your service worker file,
|
|
||||||
// even if you decide not to use precaching. See https://cra.link/PWA
|
|
||||||
precacheAndRoute(self.__WB_MANIFEST);
|
|
||||||
|
|
||||||
// Set up App Shell-style routing, so that all navigation requests
|
|
||||||
// are fulfilled with your index.html shell. Learn more at
|
|
||||||
// https://developers.google.com/web/fundamentals/architecture/app-shell
|
|
||||||
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
|
|
||||||
registerRoute(
|
|
||||||
// Return false to exempt requests from being fulfilled by index.html.
|
|
||||||
({ request, url }) => {
|
|
||||||
// If this isn't a navigation, skip.
|
|
||||||
/*if (request.mode !== 'navigate') {
|
|
||||||
return false;
|
|
||||||
}*/ // If this is a URL that starts with /_, skip.
|
|
||||||
|
|
||||||
if (url.pathname.startsWith('/_')) {
|
|
||||||
return false;
|
|
||||||
} // If this looks like a URL for a resource, because it contains // a file extension, skip.
|
|
||||||
|
|
||||||
if (url.pathname.match(fileExtensionRegexp)) {
|
|
||||||
return false;
|
|
||||||
} // Return true to signal that we want to use the handler.
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
|
|
||||||
);
|
|
||||||
registerRoute(
|
|
||||||
({request}) => request.destination === 'image',
|
|
||||||
new CacheFirst({
|
|
||||||
cacheName: 'images',
|
|
||||||
plugins: [
|
|
||||||
new CacheableResponsePlugin({
|
|
||||||
statuses: [0, 200],
|
|
||||||
}),
|
|
||||||
new ExpirationPlugin({
|
|
||||||
maxEntries: 60,
|
|
||||||
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
registerRoute(
|
|
||||||
({ url }) => url.origin === self.location.origin && url.pathname.endsWith('translation.json'),
|
|
||||||
new StaleWhileRevalidate({
|
|
||||||
cacheName: 'translations',
|
|
||||||
plugins: [
|
|
||||||
new ExpirationPlugin({ maxEntries: 5 }),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
registerRoute(
|
|
||||||
({url}) => url.origin === 'https://fonts.googleapis.com' ||
|
|
||||||
url.origin === 'https://fonts.gstatic.com',
|
|
||||||
new StaleWhileRevalidate({
|
|
||||||
cacheName: 'google-fonts',
|
|
||||||
plugins: [
|
|
||||||
new ExpirationPlugin({maxEntries: 20}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
registerRoute(
|
|
||||||
({request}) => request.destination === 'script' ||
|
|
||||||
request.destination === 'style',
|
|
||||||
new StaleWhileRevalidate({
|
|
||||||
plugins: [
|
|
||||||
new BackgroundSyncPlugin()
|
|
||||||
]
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// This allows the web app to trigger skipWaiting via
|
|
||||||
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
|
|
||||||
self.addEventListener('message', (event) => {
|
|
||||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
|
||||||
self.skipWaiting();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Any other custom service worker logic can go here.
|
|
100
src/service-worker.ts
Normal file
100
src/service-worker.ts
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
/// <reference lib="webworker" />
|
||||||
|
/* eslint-disable no-restricted-globals */
|
||||||
|
|
||||||
|
// This service worker can be customized!
|
||||||
|
// See https://developers.google.com/web/tools/workbox/modules
|
||||||
|
// for the list of available Workbox modules, or add any other
|
||||||
|
// code you'd like.
|
||||||
|
// You can also remove this file if you'd prefer not to use a
|
||||||
|
// service worker, and the Workbox build step will be skipped.
|
||||||
|
|
||||||
|
import { clientsClaim } from 'workbox-core';
|
||||||
|
import { ExpirationPlugin } from 'workbox-expiration';
|
||||||
|
import {CacheableResponsePlugin} from 'workbox-cacheable-response';
|
||||||
|
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
|
||||||
|
import { registerRoute } from 'workbox-routing';
|
||||||
|
import { StaleWhileRevalidate, CacheFirst } from 'workbox-strategies';
|
||||||
|
import {setCacheNameDetails} from 'workbox-core';
|
||||||
|
import {BackgroundSyncPlugin} from 'workbox-background-sync';
|
||||||
|
|
||||||
|
declare const self: ServiceWorkerGlobalScope;
|
||||||
|
|
||||||
|
setCacheNameDetails({
|
||||||
|
prefix: 'kevins-toolbox',
|
||||||
|
suffix: 'v1',
|
||||||
|
precache: 'precache',
|
||||||
|
runtime: 'runtime'
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
clientsClaim();
|
||||||
|
|
||||||
|
precacheAndRoute(self.__WB_MANIFEST);
|
||||||
|
|
||||||
|
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
|
||||||
|
registerRoute(
|
||||||
|
({ request, url }: { request: Request; url: URL }) => {
|
||||||
|
if (request.mode !== 'navigate') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname.startsWith('/_')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname.match(fileExtensionRegexp)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
registerRoute(
|
||||||
|
({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'),
|
||||||
|
new StaleWhileRevalidate({
|
||||||
|
cacheName: 'images',
|
||||||
|
plugins: [
|
||||||
|
new ExpirationPlugin({ maxEntries: 50 }),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
registerRoute(
|
||||||
|
({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.json') && url.pathname.startsWith('/locales'),
|
||||||
|
new StaleWhileRevalidate({
|
||||||
|
cacheName: 'locales',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
self.addEventListener('message', (event) => {
|
||||||
|
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||||
|
self.skipWaiting();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const broadcast = new BroadcastChannel('sw-updates');
|
||||||
|
|
||||||
|
broadcast.addEventListener('message', (event) => {
|
||||||
|
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||||
|
self.skipWaiting();
|
||||||
|
clientsClaim();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
broadcast.postMessage({
|
||||||
|
type: 'UPDATE_AVAILABLE'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
registerRoute(
|
||||||
|
({ url }) => url.origin === "https://images.unsplash.com",
|
||||||
|
new StaleWhileRevalidate({
|
||||||
|
cacheName: 'unsplashImages',
|
||||||
|
plugins: []
|
||||||
|
})
|
||||||
|
)
|
107
src/services/serviceWorkers.ts
Normal file
107
src/services/serviceWorkers.ts
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
let eventListeners: Map<events, ((ev: Event) => void)[]> = new Map<events, ((ev: Event) => void)[]>();
|
||||||
|
|
||||||
|
let pendingUpdate = false;
|
||||||
|
|
||||||
|
navigator.serviceWorker.getRegistration().then((reg) => {
|
||||||
|
if(reg && reg.waiting) {
|
||||||
|
pendingUpdate = true;
|
||||||
|
let handlers = eventListeners.get(events.updateAvailable) || [];
|
||||||
|
|
||||||
|
handlers.forEach((func) => {
|
||||||
|
func({
|
||||||
|
type: events.updateAvailable
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
enum events {
|
||||||
|
updateAvailable
|
||||||
|
}
|
||||||
|
|
||||||
|
type Event = {
|
||||||
|
type: events
|
||||||
|
}
|
||||||
|
|
||||||
|
const broadcast = new BroadcastChannel('sw-updates');
|
||||||
|
|
||||||
|
broadcast.addEventListener('message', (event) => {
|
||||||
|
console.log(event);
|
||||||
|
|
||||||
|
if(event.data && event.data.type === "UPDATE_AVAILABLE") {
|
||||||
|
pendingUpdate = true;
|
||||||
|
|
||||||
|
let handlers = eventListeners.get(events.updateAvailable) || [];
|
||||||
|
|
||||||
|
handlers.forEach((func) => {
|
||||||
|
func({
|
||||||
|
type: events.updateAvailable
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const isUpdatePending = () => {
|
||||||
|
return pendingUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const forceUpdate = () => {
|
||||||
|
broadcast.postMessage({
|
||||||
|
type: "SKIP_WAITING"
|
||||||
|
})
|
||||||
|
|
||||||
|
pendingUpdate = false;
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
const on = (event: events, handler: (ev: Event) => void) => {
|
||||||
|
let list = eventListeners.get(event) || [];
|
||||||
|
|
||||||
|
list.push(handler);
|
||||||
|
|
||||||
|
eventListeners.set(event, list);
|
||||||
|
}
|
||||||
|
|
||||||
|
const off = (event: events, handler: (ev: Event) => void) => {
|
||||||
|
let list = eventListeners.get(event) || [];
|
||||||
|
|
||||||
|
let index = list.indexOf(handler);
|
||||||
|
|
||||||
|
if(index === -1) return;
|
||||||
|
|
||||||
|
list.splice(index, 1);
|
||||||
|
|
||||||
|
eventListeners.set(event, list);
|
||||||
|
}
|
||||||
|
|
||||||
|
const useUpdatePending = () => {
|
||||||
|
const [updatePending, setUpdatePending] = useState(isUpdatePending)
|
||||||
|
|
||||||
|
const updateAvailable = () => {
|
||||||
|
setUpdatePending(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
on(ServiceWorkerAPI.events.updateAvailable, updateAvailable);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
off(ServiceWorkerAPI.events.updateAvailable, updateAvailable);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return updatePending
|
||||||
|
}
|
||||||
|
|
||||||
|
const ServiceWorkerAPI = {
|
||||||
|
on,
|
||||||
|
off,
|
||||||
|
forceUpdate,
|
||||||
|
isUpdatePending,
|
||||||
|
events,
|
||||||
|
useUpdatePending
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ServiceWorkerAPI
|
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue