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*
|
||||
yarn-debug.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/react": "^11.1.0",
|
||||
"@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-browser-languagedetector": "^6.1.0",
|
||||
"i18next-http-backend": "^1.2.1",
|
||||
"lucide-react": "^0.15.16",
|
||||
"node-sass": "^5.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-helmet": "^6.1.0",
|
||||
|
@ -19,13 +22,16 @@
|
|||
"react-prerendered-component": "^1.2.4",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"sass": "^1.51.0",
|
||||
"typescript": "^4.6.4",
|
||||
"web-vitals": "^1.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"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": {
|
||||
"extends": [
|
||||
|
@ -44,5 +50,8 @@
|
|||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"serve": "^13.0.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
@import "../common";
|
||||
|
||||
@mixin boxMessageColor($baseColor) {
|
||||
$bgColor: adjust-color($baseColor, null, null, null, null, null, 30);
|
||||
$bgColor: adjust-color($baseColor, $lightness: 30);
|
||||
background-color: $bgColor;
|
||||
|
||||
.icon > svg {
|
||||
|
@ -10,11 +10,11 @@
|
|||
|
||||
|
||||
@media(prefers-color-scheme: dark) {
|
||||
$bgColor: adjust-color($baseColor, null, null, null, null, null, -35);
|
||||
$bgColor: adjust-color($baseColor, $lightness: -35);
|
||||
background-color: $bgColor;
|
||||
|
||||
.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 * as styles from "./Navigation.module.scss";
|
||||
import { Globe } from "lucide-react";
|
||||
import { Globe, RefreshCw } from "lucide-react";
|
||||
import LanguageChooser from "./LanguageChooser";
|
||||
import ServiceWorkerAPI from "../services/serviceWorkers"
|
||||
|
||||
const Navigation = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [langChooserActive, setLangChooserActive] = useState(false);
|
||||
const updateAvailable = ServiceWorkerAPI.useUpdatePending()
|
||||
|
||||
return (
|
||||
<header className={styles.navigation}>
|
||||
|
@ -20,6 +22,8 @@ const Navigation = () => {
|
|||
<Link to={"/tools"}>{t("site.navigation.tools")}</Link>
|
||||
<Link to={"/about"}>{t("site.navigation.about")}</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)}} />
|
||||
</nav>
|
||||
</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