diff --git a/crabfit-frontend/deploy.sh b/crabfit-frontend/deploy.sh old mode 100644 new mode 100755 diff --git a/crabfit-frontend/package.json b/crabfit-frontend/package.json index 30ab620..448e214 100644 --- a/crabfit-frontend/package.json +++ b/crabfit-frontend/package.json @@ -12,6 +12,18 @@ "@types/node": "^14.14.31", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.1", + "workbox-background-sync": "^5.1.3", + "workbox-broadcast-update": "^5.1.3", + "workbox-cacheable-response": "^5.1.3", + "workbox-core": "^5.1.3", + "workbox-expiration": "^5.1.3", + "workbox-google-analytics": "^5.1.3", + "workbox-navigation-preload": "^5.1.3", + "workbox-precaching": "^5.1.3", + "workbox-range-requests": "^5.1.3", + "workbox-routing": "^5.1.3", + "workbox-strategies": "^5.1.3", + "workbox-streams": "^5.1.3", "axios": "^0.21.1", "dayjs": "^1.10.4", "react": "^17.0.1", diff --git a/crabfit-frontend/src/App.tsx b/crabfit-frontend/src/App.tsx index de2c8b5..19fe3e0 100644 --- a/crabfit-frontend/src/App.tsx +++ b/crabfit-frontend/src/App.tsx @@ -18,9 +18,15 @@ const App = () => { const colortheme = useSettingsStore(state => state.theme); const darkQuery = window.matchMedia('(prefers-color-scheme: dark)'); const [isDark, setIsDark] = useState(darkQuery.matches); + const [offline, setOffline] = useState(!window.navigator.onLine); darkQuery.addListener(e => colortheme === 'System' && setIsDark(e.matches)); + useEffect(() => { + window.addEventListener('offline', () => setOffline(true)); + window.addEventListener('online', () => setOffline(false)); + }, []); + useEffect(() => { setIsDark(colortheme === 'System' ? darkQuery.matches : colortheme === 'Dark'); }, [colortheme, darkQuery.matches]); @@ -28,7 +34,6 @@ const App = () => { return ( - {process.env.NODE_ENV !== 'production' && } ({ html: { @@ -68,12 +73,12 @@ const App = () => { ( }> - + )} /> ( }> - + )} /> diff --git a/crabfit-frontend/src/index.tsx b/crabfit-frontend/src/index.tsx index 7998135..caecbeb 100644 --- a/crabfit-frontend/src/index.tsx +++ b/crabfit-frontend/src/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; +import * as serviceWorkerRegistration from './serviceWorkerRegistration'; import reportWebVitals from './reportWebVitals'; ReactDOM.render( @@ -10,6 +11,8 @@ ReactDOM.render( document.getElementById('root') ); +serviceWorkerRegistration.register(); + // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals diff --git a/crabfit-frontend/src/pages/Event/Event.tsx b/crabfit-frontend/src/pages/Event/Event.tsx index bb474ce..f37204e 100644 --- a/crabfit-frontend/src/pages/Event/Event.tsx +++ b/crabfit-frontend/src/pages/Event/Event.tsx @@ -49,6 +49,7 @@ const Event = (props) => { const { register, handleSubmit } = useForm(); const { id } = props.match.params; + const { offline } = props; const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone); const [user, setUser] = useState(null); const [password, setPassword] = useState(null); @@ -297,10 +298,17 @@ const Event = (props) => { ) : ( -
- Event not found - Check that the url you entered is correct. -
+ offline ? ( +
+ You are offline + A Crab Fit doesn't work offline.
Make sure you're connected to the internet and try again.
+
+ ) : ( +
+ Event not found + Check that the url you entered is correct. +
+ ) )} diff --git a/crabfit-frontend/src/pages/Home/Home.tsx b/crabfit-frontend/src/pages/Home/Home.tsx index b9747b3..88aa382 100644 --- a/crabfit-frontend/src/pages/Home/Home.tsx +++ b/crabfit-frontend/src/pages/Home/Home.tsx @@ -32,6 +32,7 @@ import { Stat, StatNumber, StatLabel, + OfflineMessage, } from './homeStyle'; import api from 'services'; @@ -43,7 +44,7 @@ dayjs.extend(utc); dayjs.extend(timezone); dayjs.extend(customParseFormat); -const Home = () => { +const Home = ({ offline }) => { const { register, handleSubmit } = useForm({ defaultValues: { timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, @@ -154,52 +155,59 @@ const Home = () => { About / Donate - - + {offline ? ( + +

🦀📵

+

You can't create a Crab Fit when you don't have an internet connection. Please make sure you're connected.

+
+ ) : ( + + - + - + - + - {error && ( - setError(null)}>{error} - )} + {error && ( + setError(null)}>{error} + )} -
- -
-
+
+ +
+
+ )} @@ -207,11 +215,11 @@ const Home = () => {

About Crab Fit

- {stats.eventCount ?? '10+'} + {stats.eventCount ?? '100+'} Events created - {stats.personCount ?? '10+'} + {stats.personCount ?? '100+'} Availabilities entered diff --git a/crabfit-frontend/src/pages/Home/homeStyle.ts b/crabfit-frontend/src/pages/Home/homeStyle.ts index 2f05692..6f5460e 100644 --- a/crabfit-frontend/src/pages/Home/homeStyle.ts +++ b/crabfit-frontend/src/pages/Home/homeStyle.ts @@ -88,3 +88,8 @@ export const StatNumber = styled.span` export const StatLabel = styled.span` display: block; `; + +export const OfflineMessage = styled.div` + text-align: center; + margin: 50px 0 20px; +`; diff --git a/crabfit-frontend/src/service-worker.js b/crabfit-frontend/src/service-worker.js new file mode 100644 index 0000000..fbe61d2 --- /dev/null +++ b/crabfit-frontend/src/service-worker.js @@ -0,0 +1,90 @@ +/* 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 { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching'; +import { registerRoute } from 'workbox-routing'; +import { StaleWhileRevalidate, NetworkFirst } from 'workbox-strategies'; + +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') +); + +// An example runtime caching route for requests that aren't handled by the +// precache, in this case same-origin .png requests like those from in public/ +registerRoute( + // Add in any other file extensions or routing criteria as needed. + ({ url }) => url.origin === self.location.origin && ( + url.pathname.endsWith('.png') + || url.pathname.endsWith('.svg') + || url.pathname.endsWith('.jpg') + || url.pathname.endsWith('.jpeg') + || url.pathname.endsWith('.ico') + || url.pathname.endsWith('.ttf') + || url.pathname.endsWith('.woff') + || url.pathname.endsWith('.woff2') + ), // Customize this strategy as needed, e.g., by changing to CacheFirst. + new StaleWhileRevalidate({ + cacheName: 'res', + plugins: [ + // Ensure that once this runtime cache reaches a maximum size the + // least-recently used images are removed. + new ExpirationPlugin({ maxEntries: 50 }), + ], + }) +); + +// 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. + +registerRoute( + ({ url }) => url.origin === self.location.origin && ( + url.pathname.endsWith('.js') + || url.pathname.endsWith('.css') + || url.pathname.endsWith('.json') + ), + new NetworkFirst({ cacheName: 'res' }) +); diff --git a/crabfit-frontend/src/serviceWorkerRegistration.js b/crabfit-frontend/src/serviceWorkerRegistration.js new file mode 100644 index 0000000..2262ecd --- /dev/null +++ b/crabfit-frontend/src/serviceWorkerRegistration.js @@ -0,0 +1,137 @@ +// This optional code is used to register a service worker. +// register() is not called by default. + +// This lets the app load faster on subsequent visits in production, and gives +// it offline capabilities. However, it also means that developers (and users) +// will only see deployed updates on subsequent visits to a page, after all the +// existing tabs open on the page have been closed, since previously cached +// resources are updated in the background. + +// To learn more about the benefits of this model and instructions on how to +// opt-in, read https://cra.link/PWA + +const isLocalhost = Boolean( + window.location.hostname === 'localhost' || + // [::1] is the IPv6 localhost address. + window.location.hostname === '[::1]' || + // 127.0.0.0/8 are considered localhost for IPv4. + window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/) +); + +export function register(config) { + if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { + // The URL constructor is available in all browsers that support SW. + const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); + if (publicUrl.origin !== window.location.origin) { + // Our service worker won't work if PUBLIC_URL is on a different origin + // from what our page is served on. This might happen if a CDN is used to + // serve assets; see https://github.com/facebook/create-react-app/issues/2374 + return; + } + + window.addEventListener('load', () => { + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; + + if (isLocalhost) { + // This is running on localhost. Let's check if a service worker still exists or not. + checkValidServiceWorker(swUrl, config); + + // Add some additional logging to localhost, pointing developers to the + // service worker/PWA documentation. + navigator.serviceWorker.ready.then(() => { + console.log( + 'This web app is being served cache-first by a service ' + + 'worker. To learn more, visit https://cra.link/PWA' + ); + }); + } else { + // Is not localhost. Just register service worker + registerValidSW(swUrl, config); + } + }); + } +} + +function registerValidSW(swUrl, config) { + navigator.serviceWorker + .register(swUrl) + .then((registration) => { + registration.onupdatefound = () => { + const installingWorker = registration.installing; + if (installingWorker == null) { + return; + } + installingWorker.onstatechange = () => { + if (installingWorker.state === 'installed') { + if (navigator.serviceWorker.controller) { + // At this point, the updated precached content has been fetched, + // but the previous service worker will still serve the older + // content until all client tabs are closed. + console.log( + 'New content is available and will be used when all ' + + 'tabs for this page are closed. See https://cra.link/PWA.' + ); + + // Execute callback + if (config && config.onUpdate) { + config.onUpdate(registration); + } + } else { + // At this point, everything has been precached. + // It's the perfect time to display a + // "Content is cached for offline use." message. + console.log('Content is cached for offline use.'); + + // Execute callback + if (config && config.onSuccess) { + config.onSuccess(registration); + } + } + } + }; + }; + }) + .catch((error) => { + console.error('Error during service worker registration:', error); + }); +} + +function checkValidServiceWorker(swUrl, config) { + // Check if the service worker can be found. If it can't reload the page. + fetch(swUrl, { + headers: { 'Service-Worker': 'script' }, + }) + .then((response) => { + // Ensure service worker exists, and that we really are getting a JS file. + const contentType = response.headers.get('content-type'); + if ( + response.status === 404 || + (contentType != null && contentType.indexOf('javascript') === -1) + ) { + // No service worker found. Probably a different app. Reload the page. + navigator.serviceWorker.ready.then((registration) => { + registration.unregister().then(() => { + window.location.reload(); + }); + }); + } else { + // Service worker found. Proceed as normal. + registerValidSW(swUrl, config); + } + }) + .catch(() => { + console.log('No internet connection found. App is running in offline mode.'); + }); +} + +export function unregister() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.ready + .then((registration) => { + registration.unregister(); + }) + .catch((error) => { + console.error(error.message); + }); + } +} diff --git a/crabfit-frontend/yarn.lock b/crabfit-frontend/yarn.lock index 5f963c4..02d7658 100644 --- a/crabfit-frontend/yarn.lock +++ b/crabfit-frontend/yarn.lock @@ -11352,14 +11352,14 @@ word-wrap@^1.2.3, word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -workbox-background-sync@^5.1.4: +workbox-background-sync@^5.1.3, workbox-background-sync@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-5.1.4.tgz#5ae0bbd455f4e9c319e8d827c055bb86c894fd12" integrity sha512-AH6x5pYq4vwQvfRDWH+vfOePfPIYQ00nCEB7dJRU1e0n9+9HMRyvI63FlDvtFT2AvXVRsXvUt7DNMEToyJLpSA== dependencies: workbox-core "^5.1.4" -workbox-broadcast-update@^5.1.4: +workbox-broadcast-update@^5.1.3, workbox-broadcast-update@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-5.1.4.tgz#0eeb89170ddca7f6914fa3523fb14462891f2cfc" integrity sha512-HTyTWkqXvHRuqY73XrwvXPud/FN6x3ROzkfFPsRjtw/kGZuZkPzfeH531qdUGfhtwjmtO/ZzXcWErqVzJNdXaA== @@ -11408,26 +11408,26 @@ workbox-build@^5.1.4: workbox-sw "^5.1.4" workbox-window "^5.1.4" -workbox-cacheable-response@^5.1.4: +workbox-cacheable-response@^5.1.3, workbox-cacheable-response@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-5.1.4.tgz#9ff26e1366214bdd05cf5a43da9305b274078a54" integrity sha512-0bfvMZs0Of1S5cdswfQK0BXt6ulU5kVD4lwer2CeI+03czHprXR3V4Y8lPTooamn7eHP8Iywi5QjyAMjw0qauA== dependencies: workbox-core "^5.1.4" -workbox-core@^5.1.4: +workbox-core@^5.1.3, workbox-core@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-5.1.4.tgz#8bbfb2362ecdff30e25d123c82c79ac65d9264f4" integrity sha512-+4iRQan/1D8I81nR2L5vcbaaFskZC2CL17TLbvWVzQ4qiF/ytOGF6XeV54pVxAvKUtkLANhk8TyIUMtiMw2oDg== -workbox-expiration@^5.1.4: +workbox-expiration@^5.1.3, workbox-expiration@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-5.1.4.tgz#92b5df461e8126114943a3b15c55e4ecb920b163" integrity sha512-oDO/5iC65h2Eq7jctAv858W2+CeRW5e0jZBMNRXpzp0ZPvuT6GblUiHnAsC5W5lANs1QS9atVOm4ifrBiYY7AQ== dependencies: workbox-core "^5.1.4" -workbox-google-analytics@^5.1.4: +workbox-google-analytics@^5.1.3, workbox-google-analytics@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-5.1.4.tgz#b3376806b1ac7d7df8418304d379707195fa8517" integrity sha512-0IFhKoEVrreHpKgcOoddV+oIaVXBFKXUzJVBI+nb0bxmcwYuZMdteBTp8AEDJacENtc9xbR0wa9RDCnYsCDLjA== @@ -11437,35 +11437,35 @@ workbox-google-analytics@^5.1.4: workbox-routing "^5.1.4" workbox-strategies "^5.1.4" -workbox-navigation-preload@^5.1.4: +workbox-navigation-preload@^5.1.3, workbox-navigation-preload@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-5.1.4.tgz#30d1b720d26a05efc5fa11503e5cc1ed5a78902a" integrity sha512-Wf03osvK0wTflAfKXba//QmWC5BIaIZARU03JIhAEO2wSB2BDROWI8Q/zmianf54kdV7e1eLaIEZhth4K4MyfQ== dependencies: workbox-core "^5.1.4" -workbox-precaching@^5.1.4: +workbox-precaching@^5.1.3, workbox-precaching@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-5.1.4.tgz#874f7ebdd750dd3e04249efae9a1b3f48285fe6b" integrity sha512-gCIFrBXmVQLFwvAzuGLCmkUYGVhBb7D1k/IL7pUJUO5xacjLcFUaLnnsoVepBGAiKw34HU1y/YuqvTKim9qAZA== dependencies: workbox-core "^5.1.4" -workbox-range-requests@^5.1.4: +workbox-range-requests@^5.1.3, workbox-range-requests@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-5.1.4.tgz#7066a12c121df65bf76fdf2b0868016aa2bab859" integrity sha512-1HSujLjgTeoxHrMR2muDW2dKdxqCGMc1KbeyGcmjZZAizJTFwu7CWLDmLv6O1ceWYrhfuLFJO+umYMddk2XMhw== dependencies: workbox-core "^5.1.4" -workbox-routing@^5.1.4: +workbox-routing@^5.1.3, workbox-routing@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-5.1.4.tgz#3e8cd86bd3b6573488d1a2ce7385e547b547e970" integrity sha512-8ljknRfqE1vEQtnMtzfksL+UXO822jJlHTIR7+BtJuxQ17+WPZfsHqvk1ynR/v0EHik4x2+826Hkwpgh4GKDCw== dependencies: workbox-core "^5.1.4" -workbox-strategies@^5.1.4: +workbox-strategies@^5.1.3, workbox-strategies@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-5.1.4.tgz#96b1418ccdfde5354612914964074d466c52d08c" integrity sha512-VVS57LpaJTdjW3RgZvPwX0NlhNmscR7OQ9bP+N/34cYMDzXLyA6kqWffP6QKXSkca1OFo/v6v7hW7zrrguo6EA== @@ -11473,7 +11473,7 @@ workbox-strategies@^5.1.4: workbox-core "^5.1.4" workbox-routing "^5.1.4" -workbox-streams@^5.1.4: +workbox-streams@^5.1.3, workbox-streams@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-5.1.4.tgz#05754e5e3667bdc078df2c9315b3f41210d8cac0" integrity sha512-xU8yuF1hI/XcVhJUAfbQLa1guQUhdLMPQJkdT0kn6HP5CwiPOGiXnSFq80rAG4b1kJUChQQIGPrq439FQUNVrw==