diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f455182 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +### Releases + +## v1.0.0 +###### *June 19, 2020* + +It's the first stable version of the template. It's going to become a solid foundation for your next React project. + +- features: add notifications 🎉 +- readme: update bundle size, fix image titles, add section for notifications, add examples + +## v0.1.0 +###### *June 19, 2020* + +🎉 First release 🎉 diff --git a/README.md b/README.md index 69e34d6..494ca39 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,9 @@ By the same philosophy, there are other routines above the basic configuration, * [CRA](#cra) * [React Router](#react-router) * [Material UI](#material-ui) -* [Theme](#theme) * [Store](#store) +* [Theme](#theme) +* [Notifications](#notifications) * [Error Handling](#error-handling) * [Service Worker](#service-worker) * [SEO](#seo) @@ -38,15 +39,64 @@ The latest version of `react-router-dom` is integrated. Routes are defined in [/ #### Material UI -The latest version of `Material-UI` is integrated. The whole layout of the application is made by `Material-UI` components. In the demonstrated components/sections you can notice how MUI components can be customized. The styling system is also inherited from MUI. +The latest version of [Material-UI](https://material-ui.com/) is integrated. The whole layout of the application is made by `Material-UI` components. In the demonstrated components/sections you can notice how MUI components can be customized. The styling system is also inherited from MUI. + +#### Store + +For store management [overmind](https://github.com/cerebral/overmind) has been used. It's a simple store management tool. Here you can find its [implementation and integration](https://github.com/suren-atoyan/react-pwa/tree/master/src/store). + +You can use `useStore` hook exported from `store`. It'll give you `state`, `actions` and `effects`, which you can use. + +```js +// ... +import { useStore } from 'store'; + +function SomeCoolComponent() { + const { state, actions, effects } = useStore(); + + // ... +} +``` #### Theme -The [theme system](https://github.com/suren-atoyan/react-pwa/blob/master/src/theme/ThemeProvider.js) is based on MUI theme. There are two themes' styles that are defined in the [config file](https://github.com/suren-atoyan/react-pwa/blob/master/src/config/index.js). The theme provider, which is based on MUI is integrated with app and store. +The [theme system](https://github.com/suren-atoyan/react-pwa/blob/master/src/theme/ThemeProvider.js) is based on MUI theme. It's integrated with store, so, see how you can use it: -#### Store +```js +// ... +import { useStore } from 'store'; -For store management `overmind` has been used. It's a simple store management tool. Here you can find its [implementation and integration](https://github.com/suren-atoyan/react-pwa/tree/master/src/store). +function SomeCoolComponent() { + const { state, actions } = useStore(); + + // let's see what is the current theme mode + console.log(state.theme.mode); + + // and if you want to change the theme, call appropriate action + function toggleTheme() { + actions.theme.toggle(); + } +} +``` + +Also you can modify predefined theme parameters in [config](https://github.com/suren-atoyan/react-pwa/blob/master/src/config/index.js#L29) file. + +#### Notifications + +Here we've used [notistack](https://github.com/iamhosseindhv/notistack). It's integrated with store and to show a notification you just need to call the appropriate action; look how you can do it: + +```js +// ... +import { useStore } from 'store'; + +function SomeCoolComponent() { + const { actions } = useStore(); + + function showNiceWarning() { + actions.notifications.push({ message: 'Heeeeey, something went wrong I guess' }); + } +} +``` #### Error Handling @@ -76,9 +126,9 @@ There is a simple express server `/hoster/server`, which plays the role of a sta ## Size -After all these integrations the biggest bundle size is **~59KB**. It means even first load will be pretty fast (in my case it's 1.1s), further loads (already cached by service worker and workbox) will take ~0.25s. +After all these integrations the biggest bundle size is **~66KB**. It means even first load will be pretty fast (in my case it's 1.1s), further loads (already cached by service worker and workbox) will take ~0.25s. - + ## Usage @@ -110,7 +160,7 @@ The last one will build your project (`yarn build`) and start express server (`y ## Structure - + Initial files: @@ -137,7 +187,7 @@ Initial files: NOTE: The performance is not 100 because of demo server. Check the results in the [live demo](https://react-pwa.surenatoyan.com/) - + ## TODOs diff --git a/package.json b/package.json index 5bc492a..2e6b326 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-pwa", - "version": "0.1.0", + "version": "1.0.0", "private": true, "dependencies": { "@material-ui/core": "^4.10.2", @@ -9,6 +9,7 @@ "@testing-library/user-event": "^7.1.2", "dayjs": "^1.8.28", "is-mobile": "^2.2.1", + "notistack": "^0.9.17", "overmind": "^24.1.1", "overmind-react": "^25.1.1", "react": "^16.13.1", @@ -17,7 +18,8 @@ "react-helmet": "^6.1.0", "react-icons": "^3.10.0", "react-router-dom": "^5.2.0", - "react-scripts": "3.4.1" + "react-scripts": "3.4.1", + "uuid": "^8.2.0" }, "scripts": { "start": "react-scripts start", diff --git a/public/images/readme/build.png b/public/images/readme/build.png index ef7d19b..1c07cc9 100644 Binary files a/public/images/readme/build.png and b/public/images/readme/build.png differ diff --git a/public/images/readme/layout.sc.png b/public/images/readme/layout.sc.png index e837d91..9808fd2 100644 Binary files a/public/images/readme/layout.sc.png and b/public/images/readme/layout.sc.png differ diff --git a/public/images/readme/structure.png b/public/images/readme/structure.png index d2b8cb3..4945cee 100644 Binary files a/public/images/readme/structure.png and b/public/images/readme/structure.png differ diff --git a/src/config/index.js b/src/config/index.js index 5ae8a07..7195333 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -1,3 +1,5 @@ +import { isMobile } from 'utils'; + /* set your data here */ const email = 'super-email-of-the-auther@gmail.com'; const domain = 'your-project-domain.com' @@ -68,6 +70,17 @@ const title = 'React PWA'; const themePair = ['dark', 'light']; +const notifications = { + options: { + anchorOrigin: { + vertical: 'bottom', + horizontal: 'left', + }, + autoHideDuration: 3000, + }, + maxSnack: isMobile ? 3 : 4, +}; + export { messages, cancelationMessage, @@ -78,5 +91,6 @@ export { repository, title, themePair, + notifications, themes, }; diff --git a/src/index.js b/src/index.js index 8402afd..139b9de 100644 --- a/src/index.js +++ b/src/index.js @@ -8,7 +8,7 @@ if (!document.ie) { // check for ie Promise.all([ import('react'), import('react-dom'), - import('./App'), + import('App'), ]).then(([ { default: React }, { default: ReactDOM }, diff --git a/src/sections/Layout/Component.js b/src/sections/Layout/Component.js index 2d5cb23..252041b 100644 --- a/src/sections/Layout/Component.js +++ b/src/sections/Layout/Component.js @@ -5,6 +5,7 @@ import Box from '@material-ui/core/Box'; import Content from 'sections/Content'; import Copyright from 'sections/Copyright'; import Navigation from 'sections/Navigation'; +import Notifications from 'sections/Notifications'; import useStyles from './styles'; @@ -13,6 +14,7 @@ function Layout() { return ( <> + diff --git a/src/sections/Navigation/Component.js b/src/sections/Navigation/Component.js index fad1b93..bf517b1 100644 --- a/src/sections/Navigation/Component.js +++ b/src/sections/Navigation/Component.js @@ -3,7 +3,7 @@ import React, { useState } from 'react'; import AppBar from 'sections/AppBar'; import Menu from 'sections/Menu'; -export default function Navigation() { +function Navigation() { const [isMenuOpen, setIsMenuOpen] = useState(false); const handleMenuOpen = () => { @@ -20,4 +20,6 @@ export default function Navigation() { ); -}; +} + +export default Navigation; diff --git a/src/sections/Notifications/Component.js b/src/sections/Notifications/Component.js new file mode 100644 index 0000000..5fc4f1f --- /dev/null +++ b/src/sections/Notifications/Component.js @@ -0,0 +1,17 @@ +import React from 'react'; + +import { SnackbarProvider } from 'notistack'; + +import Notifier from './Notifier'; + +import { notifications } from 'config'; + +function Notifications() { + return ( + + + + ); +} + +export default Notifications; diff --git a/src/sections/Notifications/Notifier/Component.js b/src/sections/Notifications/Notifier/Component.js new file mode 100644 index 0000000..444f8dd --- /dev/null +++ b/src/sections/Notifications/Notifier/Component.js @@ -0,0 +1,49 @@ +import { useEffect, useRef } from 'react'; + +import { useSnackbar } from 'notistack'; + +import { useStore } from 'store'; + +function Notifier() { + const { state: { notifications }, actions } = useStore(); + const { enqueueSnackbar, closeSnackbar } = useSnackbar(); + const displayed = useRef([]); + + function storeDisplayed(key) { + displayed.current = [...displayed.current, key]; + } + + function removeDisplayed(key) { + displayed.current = [...displayed.current.filter(_key => key !== _key)]; + } + + useEffect(_ => { + notifications.forEach(({ message, options, dismissed }) => { + if (dismissed) { + // dismiss snackbar using notistack + closeSnackbar(options.key); + return; + } + + // do nothing if snackbar is already displayed + if (displayed.current.includes(options.key)) return; + + // display snackbar using notistack + enqueueSnackbar(message, { + ...options, + onExited(event, key) { + // removen this snackbar from the store + actions.notifications.remove(key); + removeDisplayed(key); + }, + }); + + // keep track of snackbars that we've displayed + storeDisplayed(options.key); + }); + }); + + return null; +} + +export default Notifier; diff --git a/src/sections/Notifications/Notifier/index.js b/src/sections/Notifications/Notifier/index.js new file mode 100644 index 0000000..bf109b8 --- /dev/null +++ b/src/sections/Notifications/Notifier/index.js @@ -0,0 +1,3 @@ +import Component from './Component'; + +export default Component; diff --git a/src/sections/Notifications/index.js b/src/sections/Notifications/index.js new file mode 100644 index 0000000..bf109b8 --- /dev/null +++ b/src/sections/Notifications/index.js @@ -0,0 +1,3 @@ +import Component from './Component'; + +export default Component; diff --git a/src/store/actions/index.js b/src/store/actions/index.js index a53a24f..eeacb08 100644 --- a/src/store/actions/index.js +++ b/src/store/actions/index.js @@ -1,4 +1,4 @@ -import { themePair } from 'config'; +import { themePair, notifications as notificationsDefoults } from 'config'; const theme = { toggle({ effects, state }) { @@ -21,7 +21,37 @@ const sw = { }, }; +const notifications = { + push({ state, effects }, notification) { + state.notifications.push({ + ...notification, + dismissed: false, + options: { + ...notificationsDefoults.options, + ...notification.options, + key: effects.genUUID(), + }, + }); + }, + + close({ state }, key, dismissAll = !key) { + + state.notifications = state.notifications.map( + notification => (dismissAll || notification.options.key === key) + ? { ...notification, dismissed: true } + : { ...notification } + ); + }, + + remove({ state }, key) { + state.notifications = state.notifications.filter( + notification => notification.options.key !== key, + ); + }, +}; + export { theme, sw, + notifications, }; diff --git a/src/store/effects/index.js b/src/store/effects/index.js index 2c75dc2..927dc99 100644 --- a/src/store/effects/index.js +++ b/src/store/effects/index.js @@ -1,4 +1,5 @@ import { resetApp } from 'utils'; +import { v1 as uuidv1 } from 'uuid'; const SW = {}; // don't keep it in the store @@ -26,4 +27,6 @@ const theme = { }, }; -export { sw, theme }; +const genUUID = uuidv1; + +export { sw, theme, genUUID }; diff --git a/src/store/state/index.js b/src/store/state/index.js index 9b20ebd..f08361c 100644 --- a/src/store/state/index.js +++ b/src/store/state/index.js @@ -7,6 +7,7 @@ const initialState = { isUpdated: false, registration: null, }, + notifications: [], }; export { initialState }; diff --git a/yarn.lock b/yarn.lock index fc45796..b76b000 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3008,7 +3008,7 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" -clsx@^1.0.4: +clsx@^1.0.4, clsx@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== @@ -5102,7 +5102,7 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -7257,6 +7257,14 @@ normalize-url@^3.0.0: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== +notistack@^0.9.17: + version "0.9.17" + resolved "https://registry.yarnpkg.com/notistack/-/notistack-0.9.17.tgz#dcb827f268013356f198ee7143ffda9f48a0f06f" + integrity sha512-nypTN6sEe+q98wMaxF/UwatA1yAq948+bZOo9JKYR+tU65DW0ipWyx8DseJ3UJYvb6VDD+Fqo83qwayQ46bEEA== + dependencies: + clsx "^1.1.0" + hoist-non-react-statics "^3.3.0" + npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" @@ -10654,6 +10662,11 @@ uuid@^3.0.1, uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.2.0.tgz#cb10dd6b118e2dada7d0cd9730ba7417c93d920e" + integrity sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q== + v8-compile-cache@^2.0.3: version "2.1.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e"