diff --git a/.circleci/config.yml b/.circleci/config.yml index 4cba6ae..ea100e5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -31,5 +31,5 @@ jobs: command: 'npm start' background: true - run: - name: "Runing tests" + name: "Running tests" command: npx cypress run --record --key $CYPRESS_RECORD_KEY \ No newline at end of file diff --git a/.firebase/hosting.YnVpbGQ.cache b/.firebase/hosting.YnVpbGQ.cache new file mode 100644 index 0000000..74fbd01 --- /dev/null +++ b/.firebase/hosting.YnVpbGQ.cache @@ -0,0 +1,30 @@ +asset-manifest.json,1607015017838,637795db4c9bfd5a83b3be695b53eefbf41babc0a6c41be26001a69a491f6f74 +index.html,1607015017838,5a3074114a4d1fef5062cd2eac42e3ba4f0a93a99fa90c2331849ae6564fbc08 +logo192.png,1607014993783,caff018b7f1e8fd481eb1c50d75b0ef236bcd5078b1d15c8bb348453fee30293 +static/css/6.f8d41b26.chunk.css,1607015017835,7ac0851dee7003928162d122ea99fa0f91ca2d6db5f260f79caeddc32d2f1c0d +favicon.ico,1607014993782,b72f7455f00e4e58792d2bca892abb068e2213838c0316d6b7a0d6d16acd1955 +static/css/6.f8d41b26.chunk.css.map,1607015017834,c9b94518337e29837d8cff7ac2931be2ac01c811cecb2a9d1a2ce641335684b7 +static/css/main.f21e830a.chunk.css,1607015017832,3cca8007a83800f3781e82526aad6f6f1e5c3f5ac22159d570b229ecb9a1e2af +logo512.png,1607014993789,191fc21360b4ccfb1cda11a1efb97f489ed22672ca83f4064316802bbfdd750e +manifest.json,1607014993792,341d52628782f8ac9290bbfc43298afccb47b7cbfcee146ae30cf0f46bc30900 +robots.txt,1607014993797,391d14b3c2f8c9143a27a28c7399585142228d4d1bdbe2c87ac946de411fa9a2 +static/js/2.a7db07db.chunk.js.LICENSE.txt,1607015017834,10c6f21b02a3e5df21b9e300b25eded1e0353200e84518db8b2a9e7a3f6fcfe0 +static/css/main.f21e830a.chunk.css.map,1607015017836,0db2a1c52b95862370abc59e0a78ef66d33c430ff681c5cd76698e749f4e7d58 +static/js/3.aec9c59e.chunk.js,1607015017834,1c530ebea53490d63f9b1af418fee0cda9f536bd21bc3d423ae62a95d97d3257 +service-worker.js,1607015017829,240be5c423d354b5bdef2d5db613c5b6e3dd7171e695fc5e48a0d62f97956287 +static/js/4.32f755a1.chunk.js,1607015017834,4dfaa4606163e479d71a11ffe73dd4497f5e3f63566113c557fed2a26d3b4a99 +static/js/5.cc09b653.chunk.js,1607015017835,dc2679a31890ec4bc0c4e77e63f65b5425a7e8562af0368ee4872ac064ccd84a +static/js/3.aec9c59e.chunk.js.map,1607015017839,46b43a72df7a7e55faa57974c7d53ce46ee6e463b1bbfa45a0924602d0bcd09b +static/js/6.191246bc.chunk.js,1607015017835,9aadd1b5f10f0347bb0fbb3a34879ded59e7b6db54916e525d0d362866eb400a +static/js/4.32f755a1.chunk.js.map,1607015017839,a60d0641a940bcc61cbb3ca13a394849bba2811d7bd3e1ba5422d9a92a69e4f4 +static/js/6.191246bc.chunk.js.map,1607015017839,abdc56840e0d82df161534c3b136e246f0957a712457686a7635f1a2d69365ad +static/js/7.79b372a3.chunk.js,1607015017834,318484a47de83a32dc833d262d39c633389d5547749557fca99c8eee9fdd8d0c +static/js/7.79b372a3.chunk.js.map,1607015017839,1baca1e1ef9f233b79e085adc12822c1d738e4795592a4490b10657e628482a0 +static/js/main.ebc54cc1.chunk.js,1607015017833,84888735d5f02c1ade2c80b5c06e92683690a46f9435a3232e75c3a7cdbbe948 +static/js/runtime-main.fb277b5b.js,1607015017834,bb90654d55a167f829d48b64677d62c7646562a60084b81c7cf64786e011d862 +static/js/main.ebc54cc1.chunk.js.map,1607015017834,ff645b903412484021173807497d582efa86419125ad580354b704d1470cf19a +static/js/5.cc09b653.chunk.js.map,1607015017839,a6871eaaa33e64c2431ae7afef4bb59ef5c69c23f6030ab1ea3d7288c20c7e42 +static/js/runtime-main.fb277b5b.js.map,1607015017834,b2a0123c407990521461178849037449ed5f2f3bb9fcc055bd0e242ba13c8b57 +static/js/2.a7db07db.chunk.js,1607015017833,f4cdf46ab9a94e5d6cd51d0ab7c49ba241f79e4b358362611beed13ed2aacffc +service-worker.js.map,1607015017829,5a94c4d369c4b21cf3dd2122d036d46642394e0cf0c61d77e33537736ce82275 +static/js/2.a7db07db.chunk.js.map,1607015017839,9210ddbad9d8e9a2acce3f2caa819526c47a6abb7e9d8880ce6ce2d7c45b491c diff --git a/.gitignore b/.gitignore index c31b89a..3ed3be7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. .eslintcache +.firebase/** # dependencies /node_modules @@ -22,3 +23,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + + .vscode/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1a135c2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "json.schemas": [ + { + "fileMatch": [ + "cypress.json" + ], + "url": "https://on.cypress.io/cypress.schema.json" + } + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9b8b5a0..edb074a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3969,6 +3969,11 @@ "shallow-clone": "^3.0.0" } }, + "clsx": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", + "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -12228,6 +12233,14 @@ "workbox-webpack-plugin": "5.1.4" } }, + "react-toastify": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-7.0.3.tgz", + "integrity": "sha512-cxZ5rfurC8LzcZQMTYc8RHIkQTs+BFur18Pzk6Loz6uS8OXUWm6nXVlH/wqglz4Z7UAE8xxcF5mRjfE13487uQ==", + "requires": { + "clsx": "^1.1.1" + } + }, "read-pkg": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", diff --git a/package.json b/package.json index 1536868..8a3eac2 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "react-dom": "^17.0.1", "react-router-dom": "^5.2.0", "react-scripts": "4.0.1", + "react-toastify": "^7.0.3", "web-vitals": "^0.2.4", "workbox-background-sync": "^5.1.3", "workbox-broadcast-update": "^5.1.3", diff --git a/src/App.js b/src/App.js index e34eeba..973ffa5 100644 --- a/src/App.js +++ b/src/App.js @@ -1,6 +1,7 @@ // @ts-nocheck import React, { Fragment, Suspense, lazy } from 'react'; import { Route, Switch, Redirect } from 'react-router-dom'; +import { ToastContainer } from 'react-toastify'; import Skeleton from './components/Skeleton'; import Progress from './components/Progress/Progress'; @@ -80,6 +81,7 @@ function App() { + ); diff --git a/src/index.css b/src/index.css index f6a7c9b..39867bf 100644 --- a/src/index.css +++ b/src/index.css @@ -1,18 +1,18 @@ -:root{ - --title: #2D3848; +:root { + --title: #2d3848; --content: #666c75; --header: #ffffff; - --body:#ffffff; - --bg:#ffffff; + --body: #ffffff; + --bg: #ffffff; --link: #3854b2; --skeleton: #eaeaea; } -.dark{ +.dark { --title: #ffffff; --content: #ffffff; - --header: #1A202C; - --body:#3A4556; - --bg:#2d3747; + --header: #1a202c; + --body: #3a4556; + --bg: #2d3747; --link: #74befa; --skeleton: #1d2939; } @@ -40,13 +40,25 @@ body { width: 15px; } *::-webkit-scrollbar-thumb { - background: -webkit-gradient(linear, left top, left bottom, from(var(--skeleton)), to(var(--skeleton))); + background: -webkit-gradient( + linear, + left top, + left bottom, + from(var(--skeleton)), + to(var(--skeleton)) + ); background: linear-gradient(to bottom, var(--title), var(--title)); border-radius: 30px; box-shadow: inset 3px 3px 3px var(--title), inset -2px -2px 2px rgba(0, 0, 0, 0.25); } *::-webkit-scrollbar-track { - background: linear-gradient(to right, var(--body), var(--body) 0.0625rem, var(--skeleton) 0.0625rem, var(--skeleton)); + background: linear-gradient( + to right, + var(--body), + var(--body) 0.0625rem, + var(--skeleton) 0.0625rem, + var(--skeleton) + ); } .container { max-width: 980px; @@ -75,7 +87,7 @@ p { left: 16px; right: 16px; bottom: 16px; - background-color: #1A202C; + background-color: #1a202c; border-radius: 4px; min-width: 288px; padding: 1rem; @@ -88,6 +100,9 @@ p { border-radius: 4px; margin: 0; } +.Toastify__toast--dark { + background: #323337; +} @keyframes dot { 50% { transform: translateX(96px); diff --git a/src/index.js b/src/index.js index fe774bc..ad497a7 100644 --- a/src/index.js +++ b/src/index.js @@ -3,8 +3,10 @@ import ReactDOM from 'react-dom'; import { BrowserRouter as Router } from 'react-router-dom'; import './index.css'; import App from './App'; +import { toast } from 'react-toastify'; import * as serviceWorkerRegistration from './serviceWorkerRegistration'; import reportWebVitals from './reportWebVitals'; +import 'react-toastify/dist/ReactToastify.css'; ReactDOM.render( diff --git a/src/service-worker.js b/src/service-worker.js index 0f1e0ce..39ae988 100644 --- a/src/service-worker.js +++ b/src/service-worker.js @@ -26,47 +26,50 @@ precacheAndRoute(self.__WB_MANIFEST); // 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. + // 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.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. + 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') + 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'), // Customize this strategy as needed, e.g., by changing to CacheFirst. - new StaleWhileRevalidate({ - cacheName: 'images', - plugins: [ - // Ensure that once this runtime cache reaches a maximum size the - // least-recently used images are removed. - new ExpirationPlugin({ maxEntries: 50 }), - ], - }) + // Add in any other file extensions or routing criteria as needed. + ({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst. + new StaleWhileRevalidate({ + cacheName: 'images', + 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(); - } + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting(); + } }); // Any other custom service worker logic can go here. +self.addEventListener('install', function (event) { + self.skipWaiting(); +}); diff --git a/src/serviceWorkerRegistration.js b/src/serviceWorkerRegistration.js index 2262ecd..2898641 100644 --- a/src/serviceWorkerRegistration.js +++ b/src/serviceWorkerRegistration.js @@ -1,3 +1,4 @@ +import { toast } from 'react-toastify'; // This optional code is used to register a service worker. // register() is not called by default. @@ -11,127 +12,148 @@ // 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}$/) + 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; - } + 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`; + // let isAppOnline = navigator.onLine; - window.addEventListener('load', () => { - const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; + // window.addEventListener('online', () => { + // if (!isAppOnline) { + // // toast('🦄 The connectivity is back, sync in progress...'); + // isAppOnline = true; + // } + // }); + // window.addEventListener('offline', () => { + // toast.dark( + // 'The app is running offline, any changes mades during this time will be synced as soon as the connectivity is back' + // ); + // isAppOnline = false; + // }); + if (isLocalhost) { + // This is running on localhost. Let's check if a service worker still exists or not. + checkValidServiceWorker(swUrl, config); - 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); - } - }); - } + // 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.' - ); + 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.'); + toast.dark(`Update available! Click to update.`, { + position: 'top-right', + toastId: 'appUpdateAvailable', + onClick: () => { + // registration.waiting.postMessage('skipWaiting'); + window.location.reload(); + }, + autoClose: false, + }); + // 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); - }); + // 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.'); - }); + // 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); - }); - } + if ('serviceWorker' in navigator) { + navigator.serviceWorker.ready + .then((registration) => { + registration.unregister(); + }) + .catch((error) => { + console.error(error.message); + }); + } }