├── .gitignore ├── .nvmrc ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── README.md ├── assets ├── Paypal-button.png ├── Paypal-button@2x.png └── Paypal-button@3x.png ├── docs └── .DS_Store ├── front ├── .editorconfig ├── .eslintrc.json ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .vscode │ └── settings.json ├── .yarnclean ├── coverage │ ├── clover.xml │ ├── coverage-final.json │ ├── lcov-report │ │ ├── base.css │ │ ├── block-navigation.js │ │ ├── components │ │ │ ├── backToTop │ │ │ │ ├── BackToTop.tsx.html │ │ │ │ ├── backToTopButton │ │ │ │ │ ├── BackToTopButton.tsx.html │ │ │ │ │ ├── UpIcon.tsx.html │ │ │ │ │ ├── index.html │ │ │ │ │ └── styled │ │ │ │ │ │ ├── WithRightMargin.tsx.html │ │ │ │ │ │ └── index.html │ │ │ │ └── index.html │ │ │ ├── fadeInEntrance │ │ │ │ ├── FadeInEntrance.tsx.html │ │ │ │ ├── index.html │ │ │ │ ├── index.ts.html │ │ │ │ └── styled │ │ │ │ │ ├── FadeInDiv.tsx.html │ │ │ │ │ └── index.html │ │ │ ├── logoutRoute │ │ │ │ ├── LogoutRoute.tsx.html │ │ │ │ ├── index.html │ │ │ │ └── index.ts.html │ │ │ ├── navigation │ │ │ │ ├── NavigationBar.tsx.html │ │ │ │ ├── index.html │ │ │ │ └── index.ts.html │ │ │ ├── privateRoute │ │ │ │ ├── PrivateRoute.tsx.html │ │ │ │ ├── index.html │ │ │ │ └── index.ts.html │ │ │ └── scrollToTop │ │ │ │ ├── ScrollToTop.tsx.html │ │ │ │ ├── hooks │ │ │ │ └── useScrollToTopOnLocationChange │ │ │ │ │ ├── index.html │ │ │ │ │ └── index.ts.html │ │ │ │ └── index.html │ │ ├── config │ │ │ ├── appConfig.ts.html │ │ │ ├── index.html │ │ │ └── navigation.ts.html │ │ ├── favicon.png │ │ ├── index.html │ │ ├── layout │ │ │ └── mainLayout │ │ │ │ ├── MainLayout.tsx.html │ │ │ │ ├── index.html │ │ │ │ └── index.ts.html │ │ ├── pages │ │ │ ├── about │ │ │ │ ├── About.tsx.html │ │ │ │ ├── index.html │ │ │ │ └── index.ts.html │ │ │ ├── home │ │ │ │ ├── Home.tsx.html │ │ │ │ ├── index.html │ │ │ │ ├── index.ts.html │ │ │ │ └── styled │ │ │ │ │ ├── HomeInfo.tsx.html │ │ │ │ │ ├── MainTitle.ts.html │ │ │ │ │ ├── MainTitle.tsx.html │ │ │ │ │ └── index.html │ │ │ ├── login │ │ │ │ ├── Login.tsx.html │ │ │ │ ├── index.html │ │ │ │ └── index.ts.html │ │ │ ├── pageNotFound │ │ │ │ ├── PageNotFound.tsx.html │ │ │ │ ├── index.html │ │ │ │ └── index.ts.html │ │ │ └── protected │ │ │ │ ├── Protected.tsx.html │ │ │ │ ├── index.html │ │ │ │ └── index.ts.html │ │ ├── prettify.css │ │ ├── prettify.js │ │ ├── redux │ │ │ ├── middleware │ │ │ │ ├── fetchMiddleware.ts.html │ │ │ │ └── index.html │ │ │ └── modules │ │ │ │ ├── fakeModuleWithFetch │ │ │ │ ├── index.html │ │ │ │ └── index.ts.html │ │ │ │ └── userAuth │ │ │ │ ├── index.html │ │ │ │ ├── index.ts.html │ │ │ │ └── selectors.ts.html │ │ ├── services │ │ │ ├── API │ │ │ │ ├── fetchTools.ts.html │ │ │ │ └── index.html │ │ │ ├── auth │ │ │ │ ├── index.html │ │ │ │ └── index.ts.html │ │ │ └── sw │ │ │ │ ├── index.html │ │ │ │ └── registerServiceWorker.ts.html │ │ ├── sort-arrow-sprite.png │ │ └── sorter.js │ └── lcov.info ├── index.html ├── jest.config.js ├── package-lock.json ├── package.json ├── src │ ├── Root.tsx │ ├── components │ │ ├── backToTop │ │ │ ├── BackToTop.tsx │ │ │ ├── __tests__ │ │ │ │ ├── BackToTop.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── BackToTop.test.tsx.snap │ │ │ └── backToTopButton │ │ │ │ ├── BackToTopButton.tsx │ │ │ │ ├── UpIcon.tsx │ │ │ │ ├── __tests__ │ │ │ │ ├── BackToTopButton.test.tsx │ │ │ │ ├── UpIcon.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ ├── BackToTopButton.test.tsx.snap │ │ │ │ │ └── UpIcon.test.tsx.snap │ │ │ │ └── styled │ │ │ │ └── WithRightMargin.tsx │ │ ├── fadeInEntrance │ │ │ ├── FadeInEntrance.tsx │ │ │ ├── __tests__ │ │ │ │ ├── FadeInEntrance.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── FadeInEntrance.test.tsx.snap │ │ │ ├── index.ts │ │ │ └── styled │ │ │ │ └── FadeInDiv.tsx │ │ ├── logoutRoute │ │ │ ├── LogoutRoute.tsx │ │ │ ├── __tests__ │ │ │ │ ├── LogoutRoute.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── LogoutRoute.test.tsx.snap │ │ │ └── index.ts │ │ ├── navigation │ │ │ ├── NavigationBar.tsx │ │ │ ├── __tests__ │ │ │ │ ├── NavigationBar.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── NavigationBar.test.tsx.snap │ │ │ └── index.ts │ │ ├── privateRoute │ │ │ ├── PrivateRoute.tsx │ │ │ ├── __tests__ │ │ │ │ ├── PrivateRoute.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── PrivateRoute.test.tsx.snap │ │ │ └── index.ts │ │ └── scrollToTop │ │ │ ├── ScrollToTop.tsx │ │ │ ├── __tests__ │ │ │ ├── ScrollToTop.test.tsx │ │ │ └── __snapshots__ │ │ │ │ └── ScrollToTop.test.tsx.snap │ │ │ ├── hooks │ │ │ └── useScrollToTopOnLocationChange │ │ │ │ └── index.ts │ │ │ └── index.ts │ ├── config │ │ ├── appConfig.ts │ │ └── navigation.ts │ ├── index.html │ ├── index.tsx │ ├── layout │ │ └── mainLayout │ │ │ ├── MainLayout.tsx │ │ │ ├── __tests__ │ │ │ ├── MainLayout.test.tsx │ │ │ └── __snapshots__ │ │ │ │ └── MainLayout.test.tsx.snap │ │ │ └── index.ts │ ├── mock │ │ ├── fakeAPI.json │ │ └── userInfosMock.json │ ├── pages │ │ ├── about │ │ │ ├── About.tsx │ │ │ ├── __tests__ │ │ │ │ ├── About.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── About.test.tsx.snap │ │ │ └── index.ts │ │ ├── home │ │ │ ├── Home.tsx │ │ │ ├── __tests__ │ │ │ │ ├── Home.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── Home.test.tsx.snap │ │ │ ├── index.ts │ │ │ └── styled │ │ │ │ ├── HomeInfo.tsx │ │ │ │ ├── LightNote.tsx │ │ │ │ └── MainTitle.tsx │ │ ├── login │ │ │ ├── Login.tsx │ │ │ ├── __tests__ │ │ │ │ ├── Login.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── Login.test.tsx.snap │ │ │ └── index.ts │ │ ├── pageNotFound │ │ │ ├── PageNotFound.tsx │ │ │ ├── __tests__ │ │ │ │ ├── PageNotFound.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── PageNotFound.test.tsx.snap │ │ │ └── index.ts │ │ └── protected │ │ │ ├── Protected.tsx │ │ │ ├── __tests__ │ │ │ ├── Protected.test.tsx │ │ │ └── __snapshots__ │ │ │ │ └── Protected.test.tsx.snap │ │ │ └── index.ts │ ├── redux │ │ ├── RootState.d.ts │ │ ├── middleware │ │ │ └── .gitkeep │ │ ├── modules │ │ │ ├── reducers.ts │ │ │ └── userAuth │ │ │ │ ├── __tests__ │ │ │ │ └── userAuth.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── selectors.ts │ │ │ │ └── types.d.ts │ │ └── store │ │ │ └── configureStore.ts │ ├── routes │ │ ├── MainRoutes.tsx │ │ └── routes.ts │ ├── services │ │ ├── API │ │ │ ├── example.ts │ │ │ └── fetchTools.ts │ │ ├── auth │ │ │ └── index.ts │ │ ├── getLocationOrigin │ │ │ └── index.ts │ │ └── sw │ │ │ └── registerServiceWorker.ts │ ├── style │ │ └── GlobalStyles.ts │ └── types │ │ ├── auth │ │ └── index.d.ts │ │ ├── process.d.ts │ │ ├── require.d.ts │ │ └── user │ │ └── index.d.ts ├── test │ ├── __mocks__ │ │ └── fileMock.ts │ └── setupTests.js ├── tsconfig.json ├── webpack.analyze.config.js ├── webpack.dev.config.js ├── webpack.hot.reload.config.js └── webpack.production.config.js ├── package-lock.json ├── package.json ├── preview └── preview.png └── server ├── .editorconfig ├── .nvmrc ├── .prettierrc ├── jest.config.js ├── out ├── index.js ├── lib │ └── expressServer.js └── middleware │ └── errors.js ├── package-lock.json ├── package.json ├── src ├── index.ts ├── lib │ ├── asyncWrap.ts │ └── expressServer.ts └── middleware │ └── errors.ts ├── test └── setupTests.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | *.log 4 | 5 | build/ 6 | dist/ 7 | public/assets/ 8 | /coverage 9 | /.nyc_output 10 | 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - 16 5 | sudo: false 6 | cache: 7 | npm: true 8 | directories: 9 | - node_modules 10 | services: 11 | - xvfb 12 | before_install: 13 | - cd front 14 | before_script: 15 | - npm install 16 | after_success: npm run test 17 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Launch minimalist prod like server", 8 | "program": "${workspaceRoot}/src/server/index.js" 9 | }, 10 | { 11 | "type": "node", 12 | "request": "launch", 13 | "name": "Launch hot reload server", 14 | "program": "${workspaceRoot}/server.hot.reload.js" 15 | }, 16 | { 17 | "type": "chrome", 18 | "request": "launch", 19 | "name": "Launch Chrome (for dev/prod bundles)", 20 | "url": "http://localhost:8082", 21 | "webRoot": "${workspaceRoot}" 22 | }, 23 | { 24 | "type": "chrome", 25 | "request": "launch", 26 | "name": "Launch Chrome (for hot reload)", 27 | "url": "http://localhost:3000", 28 | "webRoot": "${workspaceRoot}" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 8.0.0 2 | 3 | - update README 4 | - upgrade server dependencies 5 | - upgrade server NodeJS minimum requirement to v16 6 | - upgrade front dependencies 7 | - upgrade front NodeJS minimum requirement to v16 8 | - upgrade front webpack from 4 to 5 9 | - upgrade front dependencies (a bit of cleaning) 10 | - fix eslint and prettier 11 | 12 | # v7.0.2 13 | 14 | - upgrade `react-router` to `v5` 15 | - get rid of `yarn` and move to `npm` 16 | - migration to `react-testing-library` 17 | - `reactstrap` upgrade 18 | 19 | # v7.0.1 20 | 21 | - test migration from enzyme to [react-testing-library](https://github.com/testing-library/react-testing-library) (_because of useEffect hook not being testable with enzyme_) 22 | 23 | # v7.0.0 24 | 25 | - React 16.8.x to 16.9.x 26 | - migrate front to Typescript 27 | - replace Uglyfy-js webpack plugin with TerserPlugin (_for hooks compatibility_) 28 | 29 | # v6.1.1 30 | 31 | - upgrade `styled-components` to v4+ (_now uses createGlobalStyle()_) 32 | - upgrade `loadable-components` to 2.2.3 (_max version compatible with reactsnap, avoiding flashing on application start_) 33 | - upgrade `react-snap` to lastest 34 | - upgrade dependencies 35 | - upgrade `react-hot-loader` (_redux store initialization and top index file code changed_) 36 | - migrate server code to Typescript (_NOTE: server code is no more in **ROOT**/src/server but in **ROOT**/server_) 37 | 38 | # v6.1.0 39 | 40 | - migration to `bootstrap 4` and `reactstrap` 41 | - drop `react-router-redux@alpha5` (_deprecated_) for `react-connected-router` 42 | 43 | # v6.0.0 44 | 45 | - upgrade to `React 16.3.x` 46 | - upgrade to `webpack 4` 47 | - upgrade to `react-hot-loader v4` 48 | - drop `CSS Module` in favor of `styled-components` (_scoped style, theme support, better scaling in huge applications, simplify toolchain and keep nearly SASS syntax_) 49 | - add `flow types` (_even a little typing at least for better dev experience_) 50 | - drop `prop-types`(_static and dynamic typing apart, flow type does far more so avoid writing 2 differents typing system_) 51 | - `workbox-webpack-plugin` (_service worker caching powerful tool from Google_) 52 | - [loadable-components](https://github.com/smooth-code/loadable-components) (_split your code: here splitted just by routes, by you can split a component level if you feel the need_) 53 | - [react-snaphot](https://github.com/stereobooster/react-snap) (_SEO friendly_) 54 | - `webpack-bundle-analyzer`: analyze your bundle size (_maybe you should split or lazy load some part of your application: you will see clearly how to fix that_) 55 | - drop `moment` for `date-fns` (_since far smaller size and job's done_) 56 | - drop `mocha` and all library around it for `jest` (_all in one toolset and snapshot testing_) 57 | 58 | # v5.0.0 59 | 60 | - upgrade to React 16.x 61 | - `react-router 4+` with `react-router-redux ^5.0.0-alpha.8` (_read this [nice article about migrating from react-router 3 to react-router 4](https://codeburst.io/react-router-v4-unofficial-migration-guide-5a370b8905a)_) 62 | - add few flow types (_still keep propTypes_) 63 | - updated hot reload (_[read hot reload starter](https://gaearon.github.io/react-hot-loader/getstarted/)_) 64 | - use `CSS module` (_keep coding style with SASS but get benefit of css module for a more peasant and component pattern coding_) 65 | 66 | # v4.0.0 67 | 68 | - upgrade `React 15.6.x +` 69 | - upgrade to `webpack 3` 70 | - clean with rimraf before bundles building 71 | - scroll to top on route 72 | - login view 73 | - protected view 74 | - JWT auth. private views 75 | - file organization (_views connected to redux are no more in container but are index.js in same directory as View.js_) 76 | 77 | # v3.0.0 78 | 79 | - upgrade to `react-router v4` 80 | 81 | # v2.3.0 82 | 83 | - upgrade to `webpack 2.x` 84 | - upgrade `React 15.5.x +` 85 | - `PropTypes` from 'prop-types' (_react 15.5 breaking change_) 86 | 87 | # v2.2.0 88 | 89 | - `cross-env` added so no more particular windows command 90 | - serve dev and prod bundles 91 | - `npm run serve-dev`: with server hot reload (_uses nodemon_) 92 | - `npm run serve-prod`: production like node-express server 93 | 94 | # v2.1.0 95 | 96 | - `whatwg-fetch` is now replaced by [axios](https://github.com/mzabriskie/axios). 97 | - `react-addons-shallow-compare` is removed since ReactJS 15.4+ PureComponent does the job 98 | - splits vendors script and css from main bundle (_extract-text-webpack-plugin v1.x_) 99 | - create map file (DEV) 100 | - prepared `launch.json` for VSCode debugger 101 | - add typescript types (typings) 102 | - add flow types (flow-typed) 103 | 104 | # v2.0.0 105 | 106 | - redux-devtools is now replaced by [redux-devtools-extension](https://github.com/zalmoxisus/redux-devtools-extension#redux-devtools-extension). 107 | -------------------------------------------------------------------------------- /assets/Paypal-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MacKentoch/react-redux-bootstrap-webpack-starter/0dd39685404d0857b61aef0d3b49e87c2fc1ba76/assets/Paypal-button.png -------------------------------------------------------------------------------- /assets/Paypal-button@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MacKentoch/react-redux-bootstrap-webpack-starter/0dd39685404d0857b61aef0d3b49e87c2fc1ba76/assets/Paypal-button@2x.png -------------------------------------------------------------------------------- /assets/Paypal-button@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MacKentoch/react-redux-bootstrap-webpack-starter/0dd39685404d0857b61aef0d3b49e87c2fc1ba76/assets/Paypal-button@3x.png -------------------------------------------------------------------------------- /docs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MacKentoch/react-redux-bootstrap-webpack-starter/0dd39685404d0857b61aef0d3b49e87c2fc1ba76/docs/.DS_Store -------------------------------------------------------------------------------- /front/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /front/.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /front/.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.json 2 | **/*.txt 3 | **/*.xml 4 | **/*.svg 5 | -------------------------------------------------------------------------------- /front/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "bracketSpacing": true, 5 | "jsxBracketSameLine": false, 6 | "singleQuote": true, 7 | "overrides": [], 8 | "printWidth": 80, 9 | "useTabs": false, 10 | "tabWidth": 2, 11 | "parser": "typescript" 12 | } 13 | -------------------------------------------------------------------------------- /front/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /front/.yarnclean: -------------------------------------------------------------------------------- 1 | @types/react-native 2 | -------------------------------------------------------------------------------- /front/coverage/lcov-report/block-navigation.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var jumpToCode = (function init() { 3 | // Classes of code we would like to highlight in the file view 4 | var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; 5 | 6 | // Elements to highlight in the file listing view 7 | var fileListingElements = ['td.pct.low']; 8 | 9 | // We don't want to select elements that are direct descendants of another match 10 | var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` 11 | 12 | // Selecter that finds elements on the page to which we can jump 13 | var selector = 14 | fileListingElements.join(', ') + 15 | ', ' + 16 | notSelector + 17 | missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` 18 | 19 | // The NodeList of matching elements 20 | var missingCoverageElements = document.querySelectorAll(selector); 21 | 22 | var currentIndex; 23 | 24 | function toggleClass(index) { 25 | missingCoverageElements 26 | .item(currentIndex) 27 | .classList.remove('highlighted'); 28 | missingCoverageElements.item(index).classList.add('highlighted'); 29 | } 30 | 31 | function makeCurrent(index) { 32 | toggleClass(index); 33 | currentIndex = index; 34 | missingCoverageElements.item(index).scrollIntoView({ 35 | behavior: 'smooth', 36 | block: 'center', 37 | inline: 'center' 38 | }); 39 | } 40 | 41 | function goToPrevious() { 42 | var nextIndex = 0; 43 | if (typeof currentIndex !== 'number' || currentIndex === 0) { 44 | nextIndex = missingCoverageElements.length - 1; 45 | } else if (missingCoverageElements.length > 1) { 46 | nextIndex = currentIndex - 1; 47 | } 48 | 49 | makeCurrent(nextIndex); 50 | } 51 | 52 | function goToNext() { 53 | var nextIndex = 0; 54 | 55 | if ( 56 | typeof currentIndex === 'number' && 57 | currentIndex < missingCoverageElements.length - 1 58 | ) { 59 | nextIndex = currentIndex + 1; 60 | } 61 | 62 | makeCurrent(nextIndex); 63 | } 64 | 65 | return function jump(event) { 66 | if ( 67 | document.getElementById('fileSearch') === document.activeElement && 68 | document.activeElement != null 69 | ) { 70 | // if we're currently focused on the search input, we don't want to navigate 71 | return; 72 | } 73 | 74 | switch (event.which) { 75 | case 78: // n 76 | case 74: // j 77 | goToNext(); 78 | break; 79 | case 66: // b 80 | case 75: // k 81 | case 80: // p 82 | goToPrevious(); 83 | break; 84 | } 85 | }; 86 | })(); 87 | window.addEventListener('keydown', jumpToCode); 88 | -------------------------------------------------------------------------------- /front/coverage/lcov-report/components/backToTop/backToTopButton/styled/WithRightMargin.tsx.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for components/backToTop/backToTopButton/styled/WithRightMargin.tsx 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files / components/backToTop/backToTopButton/styled WithRightMargin.tsx

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 3/3 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 2/2 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 0/0 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 3/3 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 | 63 |
64 |
65 |

 66 | 
1 67 | 2 68 | 3 69 | 4 70 | 5 71 | 6 72 | 7 73 | 83x 74 |   75 | 3x 76 |   77 |   78 |   79 | 3x 80 |  
import styled from 'styled-components';
 81 |  
 82 | const WithRightMargin = styled.div`
 83 |   margin-right: 10px;
 84 | `;
 85 |  
 86 | export default WithRightMargin;
 87 |  
88 | 89 |
90 |
91 | 96 | 97 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /front/coverage/lcov-report/components/backToTop/backToTopButton/styled/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for components/backToTop/backToTopButton/styled 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files components/backToTop/backToTopButton/styled

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 3/3 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 2/2 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 0/0 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 3/3 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 | 63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
FileStatementsBranchesFunctionsLines
WithRightMargin.tsx 84 |
85 |
100%3/3100%2/2100%0/0100%3/3
98 |
99 |
100 |
101 | 106 | 107 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /front/coverage/lcov-report/components/backToTop/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for components/backToTop 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files components/backToTop

23 |
24 | 25 |
26 | 65.11% 27 | Statements 28 | 28/43 29 |
30 | 31 | 32 |
33 | 16.66% 34 | Branches 35 | 4/24 36 |
37 | 38 | 39 |
40 | 57.14% 41 | Functions 42 | 4/7 43 |
44 | 45 | 46 |
47 | 55.88% 48 | Lines 49 | 19/34 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 | 63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
FileStatementsBranchesFunctionsLines
BackToTop.tsx 84 |
85 |
65.11%28/4316.66%4/2457.14%4/755.88%19/34
98 |
99 |
100 |
101 | 106 | 107 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /front/coverage/lcov-report/components/fadeInEntrance/FadeInEntrance.tsx.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for components/fadeInEntrance/FadeInEntrance.tsx 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files / components/fadeInEntrance FadeInEntrance.tsx

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 5/5 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 1/1 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 5/5 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 | 63 |
64 |
65 |

 66 | 
1 67 | 2 68 | 3 69 | 4 70 | 5 71 | 6 72 | 7 73 | 8 74 | 9 75 | 10 76 | 11 77 | 12 78 | 13 79 | 14 80 | 15 81 | 16 82 | 17  83 | 6x 84 |   85 |   86 |   87 |   88 |   89 |   90 |   91 | 6x 92 | 6x 93 |   94 |   95 | 6x 96 |   97 | 6x 98 |  
import React, { ReactNode } from 'react';
 99 | import FadeInDiv from './styled/FadeInDiv';
100 |  
101 | // #region types
102 | type Props = {
103 |   children: ReactNode;
104 | };
105 | // #endregion
106 |  
107 | function FadeInEntrance({ children }: Props) {
108 |   return <FadeInDiv startAnimation>{children}</FadeInDiv>;
109 | }
110 |  
111 | FadeInEntrance.displayName = 'FadeInEntrance';
112 |  
113 | export default FadeInEntrance;
114 |  
115 | 116 |
117 |
118 | 123 | 124 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /front/coverage/lcov-report/components/fadeInEntrance/index.ts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for components/fadeInEntrance/index.ts 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files / components/fadeInEntrance index.ts

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 2/2 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 0/0 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 2/2 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 | 63 |
64 |
65 |

66 | 
1 67 | 2 68 | 3 69 | 45x 70 |   71 | 5x 72 |  
import FadeInEntrance from './FadeInEntrance';
73 |  
74 | export default FadeInEntrance;
75 |  
76 | 77 |
78 |
79 | 84 | 85 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /front/coverage/lcov-report/components/fadeInEntrance/styled/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for components/fadeInEntrance/styled 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files components/fadeInEntrance/styled

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 7/7 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 8/8 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 1/1 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 6/6 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 | 63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
FileStatementsBranchesFunctionsLines
FadeInDiv.tsx 84 |
85 |
100%7/7100%8/8100%1/1100%6/6
98 |
99 |
100 |
101 | 106 | 107 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /front/coverage/lcov-report/components/scrollToTop/hooks/useScrollToTopOnLocationChange/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for components/scrollToTop/hooks/useScrollToTopOnLocationChange 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files components/scrollToTop/hooks/useScrollToTopOnLocationChange

23 |
24 | 25 |
26 | 77.77% 27 | Statements 28 | 7/9 29 |
30 | 31 | 32 |
33 | 0% 34 | Branches 35 | 0/3 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 3/3 43 |
44 | 45 | 46 |
47 | 77.77% 48 | Lines 49 | 7/9 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 | 63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
FileStatementsBranchesFunctionsLines
index.ts 84 |
85 |
77.77%7/90%0/3100%3/377.77%7/9
98 |
99 |
100 |
101 | 106 | 107 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /front/coverage/lcov-report/components/scrollToTop/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for components/scrollToTop 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files components/scrollToTop

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 9/9 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 1/1 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 9/9 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 | 63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
FileStatementsBranchesFunctionsLines
ScrollToTop.tsx 84 |
85 |
100%9/9100%0/0100%1/1100%9/9
98 |
99 |
100 |
101 | 106 | 107 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /front/coverage/lcov-report/config/appConfig.ts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for config/appConfig.ts 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files / config appConfig.ts

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 2/2 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 0/0 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 2/2 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 | 63 |
64 |
65 |

 66 | 
1 67 | 2 68 | 3 69 | 4 70 | 5 71 | 6 72 | 7 73 | 8 74 | 9 75 | 10 76 | 11 77 | 12 78 | 13 79 | 14 80 | 15 81 | 16 82 | 177x 83 |   84 |   85 |   86 |   87 |   88 |   89 |   90 |   91 |   92 |   93 |   94 |   95 |   96 |   97 | 7x 98 |  
export const appConfig = Object.freeze({
 99 |   DEV_MODE: true, // block fetch
100 |  
101 |   // api endpoints:
102 |   api: {
103 |     fakeEndPoint: 'api/somewhere',
104 |     users: 'api/someusersapi',
105 |   },
106 |  
107 |   // sw path
108 |   sw: {
109 |     path: 'public/assets/sw.js',
110 |   },
111 | });
112 |  
113 | export default appConfig;
114 |  
115 | 116 |
117 |
118 | 123 | 124 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /front/coverage/lcov-report/config/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for config 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files config

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 4/4 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 0/0 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 4/4 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 | 63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 |
FileStatementsBranchesFunctionsLines
appConfig.ts 84 |
85 |
100%2/2100%0/0100%0/0100%2/2
navigation.ts 99 |
100 |
100%2/2100%0/0100%0/0100%2/2
113 |
114 |
115 |
116 | 121 | 122 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /front/coverage/lcov-report/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MacKentoch/react-redux-bootstrap-webpack-starter/0dd39685404d0857b61aef0d3b49e87c2fc1ba76/front/coverage/lcov-report/favicon.png -------------------------------------------------------------------------------- /front/coverage/lcov-report/layout/mainLayout/index.ts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for layout/mainLayout/index.ts 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files / layout/mainLayout index.ts

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 2/2 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 0/0 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 2/2 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 | 63 |
64 |
65 |

66 | 
1 67 | 2 68 | 3 69 | 41x 70 |   71 | 1x 72 |  
import MainLayout from './MainLayout';
73 |  
74 | export default MainLayout;
75 |  
76 | 77 |
78 |
79 | 84 | 85 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /front/coverage/lcov-report/pages/home/styled/HomeInfo.tsx.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for pages/home/styled/HomeInfo.tsx 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files / pages/home/styled HomeInfo.tsx

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 3/3 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 2/2 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 0/0 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 3/3 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 | 63 |
64 |
65 |

 66 | 
1 67 | 2 68 | 3 69 | 4 70 | 5 71 | 61x 72 |   73 | 1x 74 |   75 | 1x 76 |  
import styled from 'styled-components';
 77 |  
 78 | const HomeInfo = styled.div``;
 79 |  
 80 | export default HomeInfo;
 81 |  
82 | 83 |
84 |
85 | 90 | 91 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /front/coverage/lcov-report/pages/home/styled/MainTitle.ts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for pages/home/styled/MainTitle.ts 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files / pages/home/styled MainTitle.ts

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 3/3 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 2/2 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 0/0 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 3/3 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 | 63 |
64 |
65 |

 66 | 
1 67 | 2 68 | 3 69 | 4 70 | 5 71 | 6 72 | 7 73 | 8 74 | 91x 75 |   76 | 1x 77 |   78 |   79 |   80 |   81 | 1x 82 |  
import styled from 'styled-components';
 83 |  
 84 | const MainTitle = styled.h1`
 85 |   color: #222 !important;
 86 |   font-weight: 800;
 87 | `;
 88 |  
 89 | export default MainTitle;
 90 |  
91 | 92 |
93 |
94 | 99 | 100 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /front/coverage/lcov-report/pages/home/styled/MainTitle.tsx.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for pages/home/styled/MainTitle.tsx 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files / pages/home/styled MainTitle.tsx

23 |
24 | 25 |
26 | 100% 27 | Statements 28 | 3/3 29 |
30 | 31 | 32 |
33 | 100% 34 | Branches 35 | 2/2 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 0/0 43 |
44 | 45 | 46 |
47 | 100% 48 | Lines 49 | 3/3 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 | 63 |
64 |
65 |

 66 | 
1 67 | 2 68 | 3 69 | 4 70 | 5 71 | 6 72 | 7 73 | 8 74 | 91x 75 |   76 | 1x 77 |   78 |   79 |   80 |   81 | 1x 82 |  
import styled from 'styled-components';
 83 |  
 84 | const MainTitle = styled.h1`
 85 |   color: #222 !important;
 86 |   font-weight: 800;
 87 | `;
 88 |  
 89 | export default MainTitle;
 90 |  
91 | 92 |
93 |
94 | 99 | 100 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /front/coverage/lcov-report/prettify.css: -------------------------------------------------------------------------------- 1 | .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} 2 | -------------------------------------------------------------------------------- /front/coverage/lcov-report/redux/middleware/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for redux/middleware 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files redux/middleware

23 |
24 | 25 |
26 | 36.84% 27 | Statements 28 | 14/38 29 |
30 | 31 | 32 |
33 | 37.5% 34 | Branches 35 | 3/8 36 |
37 | 38 | 39 |
40 | 100% 41 | Functions 42 | 5/5 43 |
44 | 45 | 46 |
47 | 36.11% 48 | Lines 49 | 13/36 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 | 63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
FileStatementsBranchesFunctionsLines
fetchMiddleware.ts 84 |
85 |
36.84%14/3837.5%3/8100%5/536.11%13/36
98 |
99 |
100 |
101 | 106 | 107 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /front/coverage/lcov-report/redux/modules/fakeModuleWithFetch/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for redux/modules/fakeModuleWithFetch 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files redux/modules/fakeModuleWithFetch

23 |
24 | 25 |
26 | 30.55% 27 | Statements 28 | 11/36 29 |
30 | 31 | 32 |
33 | 0% 34 | Branches 35 | 0/12 36 |
37 | 38 | 39 |
40 | 16.66% 41 | Functions 42 | 1/6 43 |
44 | 45 | 46 |
47 | 32.35% 48 | Lines 49 | 11/34 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 | 63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
FileStatementsBranchesFunctionsLines
index.ts 84 |
85 |
30.55%11/360%0/1216.66%1/632.35%11/34
98 |
99 |
100 |
101 | 106 | 107 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /front/coverage/lcov-report/services/API/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for services/API 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files services/API

23 |
24 | 25 |
26 | 60% 27 | Statements 28 | 9/15 29 |
30 | 31 | 32 |
33 | 0% 34 | Branches 35 | 0/4 36 |
37 | 38 | 39 |
40 | 0% 41 | Functions 42 | 0/2 43 |
44 | 45 | 46 |
47 | 58.33% 48 | Lines 49 | 7/12 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 | 63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
FileStatementsBranchesFunctionsLines
fetchTools.ts 84 |
85 |
60%9/150%0/40%0/258.33%7/12
98 |
99 |
100 |
101 | 106 | 107 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /front/coverage/lcov-report/services/auth/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for services/auth 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files services/auth

23 |
24 | 25 |
26 | 12.37% 27 | Statements 28 | 12/97 29 |
30 | 31 | 32 |
33 | 1.33% 34 | Branches 35 | 1/75 36 |
37 | 38 | 39 |
40 | 10% 41 | Functions 42 | 1/10 43 |
44 | 45 | 46 |
47 | 12.37% 48 | Lines 49 | 12/97 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 | 63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
FileStatementsBranchesFunctionsLines
index.ts 84 |
85 |
12.37%12/971.33%1/7510%1/1012.37%12/97
98 |
99 |
100 |
101 | 106 | 107 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /front/coverage/lcov-report/services/sw/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for services/sw 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files services/sw

23 |
24 | 25 |
26 | 25% 27 | Statements 28 | 3/12 29 |
30 | 31 | 32 |
33 | 0% 34 | Branches 35 | 0/2 36 |
37 | 38 | 39 |
40 | 0% 41 | Functions 42 | 0/2 43 |
44 | 45 | 46 |
47 | 30% 48 | Lines 49 | 3/10 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 | 63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
FileStatementsBranchesFunctionsLines
registerServiceWorker.ts 84 |
85 |
25%3/120%0/20%0/230%3/10
98 |
99 |
100 |
101 | 106 | 107 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /front/coverage/lcov-report/sort-arrow-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MacKentoch/react-redux-bootstrap-webpack-starter/0dd39685404d0857b61aef0d3b49e87c2fc1ba76/front/coverage/lcov-report/sort-arrow-sprite.png -------------------------------------------------------------------------------- /front/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ReactJS Redux Bootstrap Starter 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 |
17 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /front/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jsdom', 4 | transform: { 5 | '\\.(ts)$': 'ts-jest', 6 | }, 7 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', 8 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 9 | globals: { 10 | 'ts-jest': { 11 | tsconfig: './tsconfig.json', 12 | babelConfig: false, 13 | }, 14 | }, 15 | // testEnvironment: 'jest-environment-jsdom-fifteen', 16 | verbose: true, 17 | roots: ['/src/', '/test'], 18 | setupFiles: [], 19 | setupFilesAfterEnv: ['/test/setupTests.js'], 20 | moduleNameMapper: { 21 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 22 | '/src/test/__mocks__/fileMock.js', 23 | '\\.(css|less|sass|scss)$': 'identity-obj-proxy', 24 | }, 25 | coverageDirectory: './coverage/', 26 | collectCoverage: true, 27 | }; 28 | -------------------------------------------------------------------------------- /front/src/Root.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Component } from 'react'; 3 | import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; 4 | import { Provider } from 'react-redux'; 5 | import { ThemeProvider } from 'styled-components'; 6 | import configureStore from './redux/store/configureStore'; 7 | import ScrollTop from './components/scrollToTop'; 8 | import MainRoutes from './routes/MainRoutes'; 9 | import GlobalStyle from './style/GlobalStyles'; 10 | import Login from './pages/login'; 11 | import registerServiceWorker from './services/sw/registerServiceWorker'; 12 | import LogoutRoute from './components/logoutRoute'; 13 | import MainLayout from './layout/mainLayout'; 14 | 15 | // #region types 16 | type Props = any; 17 | type State = any; 18 | // #endregion 19 | 20 | // #region constants 21 | const { store } = configureStore({}); 22 | // #endregion 23 | 24 | class Root extends Component { 25 | componentDidMount() { 26 | // register service worker (no worry about multiple attempts to register, browser will ignore when already registered) 27 | registerServiceWorker(); 28 | } 29 | 30 | componentDidCatch(error: any, info: any) { 31 | console.log('App error: ', error); 32 | console.log('App error info: ', info); 33 | // 34 | } 35 | 36 | render() { 37 | return ( 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {/* logout: just redirects to login (App will take care of removing the token) */} 50 | 51 | 52 | {/* Application with main layout (could have multiple applications with different layouts) */} 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ); 67 | } 68 | } 69 | 70 | export default Root; 71 | -------------------------------------------------------------------------------- /front/src/components/backToTop/BackToTop.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undefined */ 2 | import React, { useState, useEffect } from 'react'; 3 | import BackToTopButton from './backToTopButton/BackToTopButton'; 4 | import { Motion, spring, presets } from 'react-motion'; 5 | 6 | // #region types 7 | type Props = { 8 | minScrollY: number; 9 | scrollTo?: string; 10 | onScrollDone?: () => any; 11 | }; 12 | // #endregion 13 | 14 | function BackToTop({ minScrollY = 120, onScrollDone }: Props) { 15 | const [showBackButton, setShowBackButton] = useState(false); 16 | const [windowScrollY, setWindowScrollY] = useState(0); 17 | const [tickingScollObserve, setTickingScollObserve] = useState(false); 18 | 19 | // #region on windows scroll callback 20 | const handleWindowScroll = () => { 21 | if (!window) { 22 | return; 23 | } 24 | 25 | /* eslint-disable no-undefined */ 26 | const currentWindowScrollY = 27 | window.pageYOffset !== undefined 28 | ? window.pageYOffset 29 | : ( 30 | document.documentElement || 31 | document.body.parentNode || 32 | document.body 33 | ).scrollTop; 34 | /* eslint-enable no-undefined */ 35 | 36 | // scroll event fires to often, using window.requestAnimationFrame to limit computations 37 | if (!tickingScollObserve) { 38 | window.requestAnimationFrame(() => { 39 | if (windowScrollY !== currentWindowScrollY) { 40 | const shouldShowBackButton = 41 | currentWindowScrollY >= minScrollY ? true : false; 42 | 43 | setWindowScrollY(currentWindowScrollY); 44 | setShowBackButton(shouldShowBackButton); 45 | } 46 | setTickingScollObserve(false); 47 | }); 48 | } 49 | 50 | setTickingScollObserve(true); 51 | }; 52 | // #endregion 53 | 54 | // #region on button click (smooth scroll) 55 | const handlesOnBackButtonClick = ( 56 | event: React.MouseEvent, 57 | ) => { 58 | event && event.preventDefault(); 59 | if (window && windowScrollY && windowScrollY > minScrollY) { 60 | // using here smoothscroll-polyfill 61 | window.scroll({ top: 0, left: 0, behavior: 'smooth' }); 62 | typeof onScrollDone === 'function' && onScrollDone(); 63 | } 64 | }; 65 | // #endregion 66 | 67 | // #region mount and unmount subscrubstions 68 | useEffect(() => { 69 | if (typeof window !== 'undefined') { 70 | window.addEventListener('scroll', handleWindowScroll); 71 | } 72 | 73 | return function unsubscribeEvents() { 74 | if (typeof window !== 'undefined') { 75 | window.removeEventListener('scroll', handleWindowScroll); 76 | } 77 | }; 78 | }); 79 | // #endregion 80 | 81 | return ( 82 | 83 | {({ x }) => ( 84 | 92 | )} 93 | 94 | ); 95 | } 96 | 97 | BackToTop.displayName = 'BackToTop'; 98 | 99 | export default BackToTop; 100 | -------------------------------------------------------------------------------- /front/src/components/backToTop/__tests__/BackToTop.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import BackToTop from '../BackToTop'; 4 | 5 | describe('BackToTop component', () => { 6 | let rootElement: any = null; 7 | 8 | const defaultProps = { 9 | minScrollY: 10, 10 | onScrollDone: jest.fn(), 11 | }; 12 | 13 | beforeEach(() => { 14 | rootElement = document.createElement('div'); 15 | document.body.appendChild(rootElement); 16 | }); 17 | 18 | afterEach(() => { 19 | rootElement && document.body.removeChild(rootElement); 20 | rootElement = null; 21 | }); 22 | 23 | it('renders as expected', () => { 24 | const props = { ...defaultProps }; 25 | const { container } = render(, rootElement); 26 | expect(container.firstChild).toMatchSnapshot(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /front/src/components/backToTop/__tests__/__snapshots__/BackToTop.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`BackToTop component renders as expected 1`] = ` 4 | 24 | `; 25 | -------------------------------------------------------------------------------- /front/src/components/backToTop/backToTopButton/BackToTopButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import cx from 'classnames'; 3 | import UpIcon from './UpIcon'; 4 | import WithRightMargin from './styled/WithRightMargin'; 5 | 6 | // #region types 7 | export type BackButtonPosition = 'bottom-left' | 'bottom-right'; 8 | type Props = { 9 | position: BackButtonPosition; 10 | onClick: (event: React.MouseEvent) => any; 11 | children?: ReactNode; 12 | motionStyle: any; 13 | }; 14 | // #endregion 15 | 16 | // #region constants 17 | const defaultBackGroundColor = '#4A4A4A'; 18 | const sideOffset = '-10px'; 19 | const bottomOffset = '40px'; 20 | const defaultWidth = '100px'; 21 | const defaultZindex = 10; 22 | const defaultOpacity = 0.5; 23 | const defaultStyle = { 24 | position: 'fixed', 25 | right: sideOffset, 26 | left: '', 27 | bottom: bottomOffset, 28 | width: defaultWidth, 29 | zIndex: defaultZindex, 30 | opacity: defaultOpacity, 31 | backgroundColor: defaultBackGroundColor, 32 | }; 33 | 34 | function setPosition(position = 'bottom-right', refStyle = defaultStyle): any { 35 | const style = { ...refStyle }; 36 | 37 | switch (position) { 38 | case 'bottom-right': 39 | style.right = sideOffset; 40 | style.left = ''; 41 | return style; 42 | 43 | case 'bottom-left': 44 | style.right = ''; 45 | style.left = sideOffset; 46 | return style; 47 | 48 | default: 49 | return refStyle; 50 | } 51 | } 52 | // #endregion 53 | 54 | const BackToTopButton = ({ 55 | onClick, 56 | position = 'bottom-right', 57 | children, 58 | motionStyle, 59 | }: Props) => { 60 | const buttonStyle = setPosition(position, { 61 | ...motionStyle, 62 | ...defaultStyle, 63 | }); 64 | 65 | return ( 66 | 80 | ); 81 | }; 82 | 83 | BackToTopButton.displayName = 'BackToTopButton'; 84 | 85 | export default BackToTopButton; 86 | -------------------------------------------------------------------------------- /front/src/components/backToTop/backToTopButton/UpIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // #region types 4 | type Props = { 5 | color: string; 6 | }; 7 | // #endregion 8 | 9 | const UpIcon = ({ color = '#F1F1F1' }: Props) => { 10 | return ( 11 | 12 | 16 | 17 | ); 18 | }; 19 | 20 | UpIcon.displayName = 'UpIcon'; 21 | 22 | export default UpIcon; 23 | -------------------------------------------------------------------------------- /front/src/components/backToTop/backToTopButton/__tests__/BackToTopButton.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import BackToTopButton, { BackButtonPosition } from '../BackToTopButton'; 4 | 5 | describe('BackToTopButton component', () => { 6 | let rootElement: any = null; 7 | 8 | beforeEach(() => { 9 | rootElement = document.createElement('div'); 10 | document.body.appendChild(rootElement); 11 | }); 12 | 13 | afterEach(() => { 14 | rootElement && document.body.removeChild(rootElement); 15 | rootElement = null; 16 | }); 17 | 18 | it('renders as expected', () => { 19 | const position: BackButtonPosition = 'bottom-left'; 20 | const props = { 21 | position, 22 | // eslint-disable-next-line @typescript-eslint/no-empty-function 23 | onClick: () => {}, 24 | motionStyle: {}, 25 | }; 26 | 27 | const { container } = render( 28 | 29 |

a child

30 |
, 31 | rootElement, 32 | ); 33 | expect(container.firstChild).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /front/src/components/backToTop/backToTopButton/__tests__/UpIcon.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import UpIcon from '../UpIcon'; 4 | 5 | describe('UpIcon component', () => { 6 | let rootElement: any = null; 7 | 8 | const defaultProps = { 9 | color: '', 10 | }; 11 | 12 | beforeEach(() => { 13 | rootElement = document.createElement('div'); 14 | document.body.appendChild(rootElement); 15 | }); 16 | 17 | afterEach(() => { 18 | rootElement && document.body.removeChild(rootElement); 19 | rootElement = null; 20 | }); 21 | 22 | it('renders as expected', () => { 23 | const props = { ...defaultProps }; 24 | const { container } = render(, rootElement); 25 | expect(container.firstChild).toMatchSnapshot(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /front/src/components/backToTop/backToTopButton/__tests__/__snapshots__/BackToTopButton.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`BackToTopButton component renders as expected 1`] = ` 4 | 12 | `; 13 | -------------------------------------------------------------------------------- /front/src/components/backToTop/backToTopButton/__tests__/__snapshots__/UpIcon.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`UpIcon component renders as expected 1`] = ` 4 | 10 | 14 | 15 | `; 16 | -------------------------------------------------------------------------------- /front/src/components/backToTop/backToTopButton/styled/WithRightMargin.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const WithRightMargin = styled.div` 4 | margin-right: 10px; 5 | `; 6 | 7 | export default WithRightMargin; 8 | -------------------------------------------------------------------------------- /front/src/components/fadeInEntrance/FadeInEntrance.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import FadeInDiv from './styled/FadeInDiv'; 3 | 4 | // #region types 5 | type Props = { 6 | children: ReactNode; 7 | }; 8 | // #endregion 9 | 10 | function FadeInEntrance({ children }: Props) { 11 | return {children}; 12 | } 13 | 14 | FadeInEntrance.displayName = 'FadeInEntrance'; 15 | 16 | export default FadeInEntrance; 17 | -------------------------------------------------------------------------------- /front/src/components/fadeInEntrance/__tests__/FadeInEntrance.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import FadeInEntrance from '../FadeInEntrance'; 4 | 5 | describe('FadeInEntrance component', () => { 6 | let rootElement: any = null; 7 | 8 | beforeEach(() => { 9 | rootElement = document.createElement('div'); 10 | document.body.appendChild(rootElement); 11 | }); 12 | 13 | afterEach(() => { 14 | rootElement && document.body.removeChild(rootElement); 15 | rootElement = null; 16 | }); 17 | 18 | it('renders as expected', () => { 19 | const props = {}; 20 | 21 | const { container } = render( 22 | 23 |

a child

24 |
, 25 | rootElement, 26 | ); 27 | expect(container.firstChild).toMatchSnapshot(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /front/src/components/fadeInEntrance/__tests__/__snapshots__/FadeInEntrance.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`FadeInEntrance component renders as expected 1`] = ` 4 |
7 |

8 | a child 9 |

10 |
11 | `; 12 | -------------------------------------------------------------------------------- /front/src/components/fadeInEntrance/index.ts: -------------------------------------------------------------------------------- 1 | import FadeInEntrance from './FadeInEntrance'; 2 | 3 | export default FadeInEntrance; 4 | -------------------------------------------------------------------------------- /front/src/components/fadeInEntrance/styled/FadeInDiv.tsx: -------------------------------------------------------------------------------- 1 | import styled, { keyframes, css } from 'styled-components'; 2 | 3 | const fadeIn = keyframes` 4 | from { 5 | opacity: 0; 6 | } 7 | to { 8 | opacity: 1; 9 | transform: none; 10 | } 11 | `; 12 | 13 | const fadeInAnimationCss = css` 14 | opacity: 0; 15 | animation-name: ${fadeIn}; 16 | animation-timing-function: ease-in; 17 | animation-duration: 0.7s; 18 | animation-delay: 0s; 19 | animation-fill-mode: both; 20 | `; 21 | 22 | type FadeInProps = { 23 | startAnimation: boolean, 24 | }; 25 | 26 | const FadeInDiv = 27 | styled.div < 28 | FadeInProps > 29 | ` 30 | ${({ startAnimation }) => startAnimation && fadeInAnimationCss}; 31 | `; 32 | 33 | export default FadeInDiv; 34 | -------------------------------------------------------------------------------- /front/src/components/logoutRoute/LogoutRoute.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | import { RouteComponentProps } from 'react-router'; 4 | import { ReduxConnectedProps, OwnProps } from './index'; 5 | 6 | // #region types 7 | type Props = RouteComponentProps & ReduxConnectedProps & OwnProps; 8 | // #endregion 9 | 10 | function LogoutRoute(props: Props) { 11 | const { disconnectUser } = props; 12 | useEffect(() => disconnectUser()); 13 | 14 | return ( 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | LogoutRoute.displayName = 'LogoutRoute'; 22 | 23 | export default LogoutRoute; 24 | -------------------------------------------------------------------------------- /front/src/components/logoutRoute/__tests__/LogoutRoute.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { MemoryRouter, Switch, Route } from 'react-router'; 4 | import { Provider } from 'react-redux'; 5 | import { ThemeProvider } from 'styled-components'; 6 | import configureStore from 'redux-mock-store'; 7 | import LogoutRoute from '../index'; 8 | 9 | const middlewares: Array = []; 10 | const mockStore = configureStore(middlewares); 11 | 12 | describe('LogoutRoute component', () => { 13 | let rootElement: any = null; 14 | 15 | beforeEach(() => { 16 | rootElement = document.createElement('div'); 17 | document.body.appendChild(rootElement); 18 | }); 19 | 20 | afterEach(() => { 21 | rootElement && document.body.removeChild(rootElement); 22 | rootElement = null; 23 | }); 24 | 25 | it('renders as expected', async () => { 26 | const initialState = { 27 | userAuth: { 28 | isFetching: false, 29 | isLogging: false, 30 | id: '', 31 | login: '', 32 | firstname: '', 33 | lastname: '', 34 | token: '', 35 | isAuthenticated: false, 36 | }, 37 | }; 38 | const store = mockStore(initialState); 39 | const { container } = await render( 40 | 41 | 42 | 43 | 44 | 45 | anywhere 46 | 47 | 48 | login page 49 | 50 | 51 | 52 | 53 | 54 | , 55 | rootElement, 56 | ); 57 | expect(container.firstChild).toMatchSnapshot(); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /front/src/components/logoutRoute/__tests__/__snapshots__/LogoutRoute.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`LogoutRoute component renders as expected 1`] = ` 4 | 5 | anywhere 6 | 7 | `; 8 | -------------------------------------------------------------------------------- /front/src/components/logoutRoute/index.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { bindActionCreators, compose, Dispatch } from 'redux'; 3 | import { withRouter } from 'react-router-dom'; 4 | import * as userAuthActions from '../../redux/modules/userAuth'; 5 | import LogoutRoute from './LogoutRoute'; 6 | 7 | // #region redux map state and dispatch to props 8 | const mapStateToProps = (/* state: RootState */) => { 9 | return {}; 10 | }; 11 | 12 | const mapDispatchToProps = (dispatch: Dispatch) => { 13 | return bindActionCreators({ ...userAuthActions }, dispatch); 14 | }; 15 | // #endregion 16 | 17 | // #region types 18 | export type ReduxConnectedProps = UserAuthActions; 19 | export type OwnProps = Record; 20 | // #endregion 21 | 22 | export default compose( 23 | connect(mapStateToProps, mapDispatchToProps), 24 | withRouter, 25 | )(LogoutRoute); 26 | -------------------------------------------------------------------------------- /front/src/components/navigation/NavigationBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | Collapse, 4 | Navbar, 5 | NavbarToggler, 6 | NavbarBrand, 7 | Nav, 8 | NavItem, 9 | NavLink, 10 | } from 'reactstrap'; 11 | import { RouteComponentProps } from 'react-router'; 12 | import { OwnProps, ReduxConnectedProps } from './index'; 13 | 14 | // #region types 15 | type Props = RouteComponentProps & OwnProps & ReduxConnectedProps; 16 | // #endregion 17 | 18 | function NavigationBar({ 19 | brand, 20 | navModel: { rightLinks }, 21 | // leftNavItemClick, 22 | // rightNavItemClick, 23 | isAuthenticated, 24 | history, 25 | disconnectUser, 26 | }: Props) { 27 | const [isOpen, setIsOpen] = useState(false); 28 | 29 | // #region navigation bar toggle 30 | const toggle = (event: React.MouseEvent) => { 31 | event?.preventDefault(); 32 | setIsOpen(!isOpen); 33 | }; 34 | // #endregion 35 | 36 | // #region handlesNavItemClick event 37 | const handlesNavItemClick = 38 | (link = '/') => 39 | (event: React.MouseEvent) => { 40 | event?.preventDefault(); 41 | history.push(link); 42 | }; 43 | // #endregion 44 | 45 | // #region disconnect 46 | const handlesDisconnect = (event: React.MouseEvent) => { 47 | event?.preventDefault(); 48 | disconnectUser(); 49 | history.push('/'); 50 | }; 51 | // #endregion 52 | 53 | return ( 54 | 55 | {brand} 56 | 57 | 58 | 74 | 75 | 76 | ); 77 | } 78 | 79 | NavigationBar.displayName = 'NavigationBar'; 80 | 81 | export default NavigationBar; 82 | -------------------------------------------------------------------------------- /front/src/components/navigation/__tests__/NavigationBar.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { MemoryRouter } from 'react-router'; 3 | import { Provider } from 'react-redux'; 4 | import { ThemeProvider } from 'styled-components'; 5 | import configureStore from 'redux-mock-store'; 6 | import { render } from '@testing-library/react'; 7 | import NavigationBar from '../index'; 8 | 9 | const middlewares: Array = []; 10 | const mockStore = configureStore(middlewares); 11 | 12 | describe('NavigationBar component', () => { 13 | let rootElement: any = null; 14 | 15 | beforeEach(() => { 16 | rootElement = document.createElement('div'); 17 | document.body.appendChild(rootElement); 18 | }); 19 | 20 | afterEach(() => { 21 | rootElement && document.body.removeChild(rootElement); 22 | rootElement = null; 23 | }); 24 | 25 | it('renders as expected', () => { 26 | const initialState = { 27 | userAuth: { token: 'FAKE_TOKEN' }, 28 | }; 29 | const store = mockStore(initialState); 30 | const props = { 31 | brand: 'test', 32 | navModel: { 33 | leftLinks: [ 34 | { 35 | link: '/', 36 | label: 'home', 37 | }, 38 | ], 39 | rightLinks: [ 40 | { 41 | link: '/', 42 | label: 'home', 43 | }, 44 | ], 45 | }, 46 | token: '', 47 | isAuthenticated: true, 48 | leftNavItemClick: jest.fn(), 49 | rightNavItemClick: jest.fn(), 50 | disconnectUser: jest.fn(), 51 | checkUserIsConnected: jest.fn(), 52 | fetchUserInfoDataIfNeeded: jest.fn(), 53 | logUserIfNeeded: jest.fn(), 54 | }; 55 | 56 | const { container } = render( 57 | 58 | 59 | 60 | 61 | 62 | 63 | , 64 | rootElement, 65 | ); 66 | expect(container.firstChild).toMatchSnapshot(); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /front/src/components/navigation/__tests__/__snapshots__/NavigationBar.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`NavigationBar component renders as expected 1`] = ` 4 | 41 | `; 42 | -------------------------------------------------------------------------------- /front/src/components/navigation/index.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { bindActionCreators, Dispatch, compose } from 'redux'; 3 | import { withRouter } from 'react-router-dom'; 4 | import * as userAuthActions from '../../redux/modules/userAuth'; 5 | import NavigationBar from './NavigationBar'; 6 | import { Link } from '../../config/navigation'; 7 | import { 8 | makeGetIsAuthenticatedSelector, 9 | makeGetTokenSelector, 10 | } from '../../redux/modules/userAuth/selectors'; 11 | 12 | // #region create selectors instances 13 | const getIsAuthenticated = makeGetIsAuthenticatedSelector(); 14 | const getToken = makeGetTokenSelector(); 15 | // #endregion 16 | 17 | // #region redux map state and dispatch to props 18 | const mapStateToProps = (state: RootState) => { 19 | return { 20 | token: getToken(state), 21 | isAuthenticated: getIsAuthenticated(state), 22 | }; 23 | }; 24 | 25 | const mapDispatchToProps = (dispatch: Dispatch) => { 26 | return bindActionCreators({ ...userAuthActions }, dispatch); 27 | }; 28 | // #endregion 29 | 30 | // #region types 31 | export type OwnProps = { 32 | brand: string; 33 | leftNavItemClick: ( 34 | event: React.MouseEvent, 35 | viewName?: string, 36 | ) => any; 37 | rightNavItemClick: ( 38 | event: React.MouseEvent, 39 | viewName?: string, 40 | ) => any; 41 | navModel: { 42 | leftLinks: Array; 43 | rightLinks: Array; 44 | }; 45 | }; 46 | export type ReduxConnectedProps = Pick< 47 | UserAuthState, 48 | 'isAuthenticated' | 'token' 49 | > & 50 | UserAuthActions; 51 | // #endregion 52 | 53 | export default compose( 54 | connect(mapStateToProps, mapDispatchToProps), 55 | withRouter, 56 | )(NavigationBar); 57 | -------------------------------------------------------------------------------- /front/src/components/privateRoute/PrivateRoute.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | import { RouteComponentProps } from 'react-router'; 4 | import { ReduxConnectedProps, OwnProps } from './index'; 5 | 6 | // #region types 7 | type Props = RouteComponentProps & OwnProps & ReduxConnectedProps; 8 | // #endregion 9 | 10 | function PrivateRoute(props: Props) { 11 | const { children: InnerComponent, ...rest } = props; 12 | const { location, isAuthenticated } = props; 13 | 14 | return ( 15 | 18 | isAuthenticated ? ( 19 | InnerComponent 20 | ) : ( 21 | 22 | ) 23 | } 24 | /> 25 | ); 26 | } 27 | 28 | PrivateRoute.displayName = 'PrivateRoute'; 29 | 30 | export default PrivateRoute; 31 | -------------------------------------------------------------------------------- /front/src/components/privateRoute/__tests__/PrivateRoute.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { Router, Switch, useHistory } from 'react-router'; 4 | import { Route } from 'react-router'; 5 | import { createHashHistory } from 'history'; 6 | import { Provider } from 'react-redux'; 7 | import { ThemeProvider } from 'styled-components'; 8 | import configureStore from 'redux-mock-store'; 9 | import PrivateRoute from '../index'; 10 | 11 | // #region constants 12 | const history = createHashHistory(); 13 | const middlewares: Array = []; 14 | const mockStore = configureStore(middlewares); 15 | // #endregion 16 | 17 | describe('PrivateRoute component', () => { 18 | let rootElement: any = null; 19 | 20 | const HomePage = () => { 21 | // const history = useHistory(); 22 | history.push('/protected'); 23 | return

Home page

; 24 | }; 25 | 26 | beforeEach(() => { 27 | rootElement = document.createElement('div'); 28 | document.body.appendChild(rootElement); 29 | }); 30 | 31 | afterEach(() => { 32 | rootElement && document.body.removeChild(rootElement); 33 | rootElement = null; 34 | }); 35 | 36 | it('renders as expected', async () => { 37 | const initialState = { 38 | userAuth: { 39 | isFetching: false, 40 | isLogging: false, 41 | id: '', 42 | login: '', 43 | firstname: '', 44 | lastname: '', 45 | token: '', 46 | isAuthenticated: false, 47 | }, 48 | }; 49 | const store = mockStore(initialState); 50 | const props = {}; 51 | 52 | const LoginPage = () =>

Login page

; 53 | const ChildPage = () =>

private page

; 54 | 55 | const { container } = render( 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | , 73 | rootElement, 74 | ); 75 | expect(container.firstChild).toMatchSnapshot(); 76 | }); 77 | 78 | it('redirects to login when not authenticated', async () => { 79 | const initialState = { 80 | userAuth: { 81 | isFetching: false, 82 | isLogging: false, 83 | id: '', 84 | login: '', 85 | firstname: '', 86 | lastname: '', 87 | token: '', 88 | isAuthenticated: false, 89 | }, 90 | }; 91 | const store = mockStore(initialState); 92 | const props = {}; 93 | const PrivatePage = () =>

private page

; 94 | const LoginPage = () =>

login page

; 95 | 96 | const { findByTestId } = render( 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | , 114 | rootElement, 115 | ); 116 | 117 | const loginPageContainer = await findByTestId('private'); 118 | expect(loginPageContainer.textContent).toBe('private page'); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /front/src/components/privateRoute/__tests__/__snapshots__/PrivateRoute.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`PrivateRoute component renders as expected 1`] = ` 4 |

5 | private page 6 |

7 | `; 8 | -------------------------------------------------------------------------------- /front/src/components/privateRoute/index.ts: -------------------------------------------------------------------------------- 1 | import { bindActionCreators, compose, Dispatch } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | import { withRouter } from 'react-router-dom'; 4 | import * as userAuthActions from '../../redux/modules/userAuth'; 5 | import PrivateRoute from './PrivateRoute'; 6 | import { makeGetIsAuthenticatedSelector } from '../../redux/modules/userAuth/selectors'; 7 | 8 | // #region create selectors instances 9 | const getIsAuthenticated = makeGetIsAuthenticatedSelector(); 10 | // #endregion 11 | 12 | // #region redux map state and dispatch to props 13 | const mapStateToProps = (state: RootState) => { 14 | return { 15 | isAuthenticated: getIsAuthenticated(state), 16 | }; 17 | }; 18 | 19 | const mapDispatchToProps = (dispatch: Dispatch) => { 20 | return bindActionCreators({ ...userAuthActions }, dispatch); 21 | }; 22 | // #endregion 23 | 24 | // #region types 25 | export type ReduxConnectedProps = Pick & 26 | UserAuthActions; 27 | export type OwnProps = Record; 28 | // #endregion 29 | 30 | export default compose( 31 | connect(mapStateToProps, mapDispatchToProps), 32 | withRouter, 33 | )(PrivateRoute); 34 | -------------------------------------------------------------------------------- /front/src/components/scrollToTop/ScrollToTop.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, ReactNode } from 'react'; 2 | import { useLocation } from 'react-router-dom'; 3 | import { useScrollToTopOnLocationChange } from './hooks/useScrollToTopOnLocationChange'; 4 | type OwnProps = { 5 | children: ReactNode; 6 | }; 7 | type Props = OwnProps; 8 | 9 | function ScrollToTop({ children }: Props) { 10 | const location = useLocation(); 11 | useScrollToTopOnLocationChange(location); 12 | 13 | return {children}; 14 | } 15 | 16 | ScrollToTop.displayName = 'ScrollToTop'; 17 | 18 | export default ScrollToTop; 19 | -------------------------------------------------------------------------------- /front/src/components/scrollToTop/__tests__/ScrollToTop.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { MemoryRouter } from 'react-router'; 4 | import ScrollToTop from '../ScrollToTop'; 5 | 6 | describe('ScrollToTop component', () => { 7 | let rootElement: any = null; 8 | 9 | beforeEach(() => { 10 | rootElement = document.createElement('div'); 11 | document.body.appendChild(rootElement); 12 | }); 13 | 14 | afterEach(() => { 15 | rootElement && document.body.removeChild(rootElement); 16 | rootElement = null; 17 | }); 18 | 19 | it('renders as expected', () => { 20 | const props = { 21 | brand: 'test', 22 | leftNavItemClick: jest.fn(), 23 | rightNavItemClick: jest.fn(), 24 | navModel: { 25 | brand: 'test', 26 | leftLinks: [ 27 | { 28 | link: '/', 29 | label: 'home', 30 | viewName: 'home', 31 | onClick: jest.fn(), 32 | }, 33 | ], 34 | rightLinks: [ 35 | { 36 | link: '/', 37 | label: 'home', 38 | viewName: 'home', 39 | onClick: jest.fn(), 40 | }, 41 | ], 42 | }, 43 | }; 44 | 45 | const { container } = render( 46 | 47 | 48 |

a child

49 |
50 |
, 51 | rootElement, 52 | ); 53 | expect(container.firstChild).toMatchSnapshot(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /front/src/components/scrollToTop/__tests__/__snapshots__/ScrollToTop.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ScrollToTop component renders as expected 1`] = ` 4 |

5 | a child 6 |

7 | `; 8 | -------------------------------------------------------------------------------- /front/src/components/scrollToTop/hooks/useScrollToTopOnLocationChange/index.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from 'react'; 2 | import { Location } from 'history'; 3 | 4 | export function useScrollToTopOnLocationChange(location: any) { 5 | const prevLocation = useRef(null); 6 | 7 | useEffect(() => { 8 | prevLocation.current = location; 9 | }, [location]); 10 | 11 | useEffect(() => { 12 | if (prevLocation.current !== location) { 13 | window && window.scrollTo(0, 0); 14 | prevLocation.current = location; 15 | } 16 | }, [location]); 17 | } 18 | -------------------------------------------------------------------------------- /front/src/components/scrollToTop/index.ts: -------------------------------------------------------------------------------- 1 | import ScrollToTop from './ScrollToTop'; 2 | 3 | export default ScrollToTop; 4 | -------------------------------------------------------------------------------- /front/src/config/appConfig.ts: -------------------------------------------------------------------------------- 1 | export const appConfig = Object.freeze({ 2 | DEV_MODE: true, // block fetch 3 | 4 | // api endpoints: 5 | api: { 6 | fakeEndPoint: 'api/somewhere', 7 | users: 'api/someusersapi', 8 | }, 9 | 10 | // sw path 11 | sw: { 12 | path: 'public/assets/sw.js', 13 | }, 14 | }); 15 | 16 | export default appConfig; 17 | -------------------------------------------------------------------------------- /front/src/config/navigation.ts: -------------------------------------------------------------------------------- 1 | export type Link = { 2 | label: string; 3 | link: string; 4 | view?: string; 5 | isRouteBtn?: boolean; 6 | }; 7 | 8 | export type Navigation = { 9 | brand: string; 10 | leftLinks: Array; 11 | rightLinks: Array; 12 | }; 13 | // #endregion 14 | 15 | const navigation = Object.freeze({ 16 | brand: 'React Redux Bootstrap Starter', 17 | leftLinks: [], 18 | rightLinks: [ 19 | { 20 | label: 'Home', 21 | link: '/', 22 | }, 23 | { 24 | label: 'Protected', 25 | link: '/protected', 26 | view: 'protected', 27 | isRouteBtn: true, 28 | }, 29 | { 30 | label: 'About', 31 | link: '/about', 32 | view: 'about', 33 | isRouteBtn: true, 34 | }, 35 | ], 36 | }); 37 | 38 | export default navigation; 39 | -------------------------------------------------------------------------------- /front/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ReactJS Redux Bootstrap Starter 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /front/src/index.tsx: -------------------------------------------------------------------------------- 1 | // import 'core-js/stable'; 2 | // import 'regenerator-runtime/runtime'; 3 | import 'bootstrap/dist/css/bootstrap.min.css'; 4 | import React from 'react'; 5 | import { hydrate, render } from 'react-dom'; 6 | import smoothScrollPolyfill from 'smoothscroll-polyfill'; 7 | import { loadComponents, getState } from 'loadable-components'; 8 | import Root from './Root'; 9 | 10 | // #region constants 11 | const ELEMENT_TO_BOOTSTRAP = 'root'; 12 | const bootstrapedElement = document.getElementById(ELEMENT_TO_BOOTSTRAP); 13 | // #endregion 14 | 15 | // #region globals (styles, polyfill ...) 16 | smoothScrollPolyfill.polyfill(); 17 | (window as any).__forceSmoothScrollPolyfill__ = true; 18 | (window as any).snapSaveState = () => getState(); 19 | 20 | (async () => { 21 | console.log( 22 | 'You have async support if you read this instead of "ReferenceError: regeneratorRuntime is not defined" error.', 23 | ); 24 | })(); 25 | // #endregion 26 | 27 | // render app (hydrate for react-snap): 28 | function renderApp(RootComponent: any) { 29 | const Application = RootComponent; 30 | // needed for react-snap: 31 | bootstrapedElement && bootstrapedElement.hasChildNodes() 32 | ? loadComponents().then(() => hydrate(, bootstrapedElement)) 33 | : render(, bootstrapedElement); 34 | } 35 | 36 | renderApp(Root); 37 | // #endregion 38 | -------------------------------------------------------------------------------- /front/src/layout/mainLayout/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useEffect, useCallback, ReactNode } from 'react'; 2 | import { withRouter } from 'react-router'; 3 | import { RouteComponentProps } from 'react-router'; 4 | import NavigationBar from '../../components/navigation'; 5 | import BackToTop from '../../components/backToTop/BackToTop'; 6 | import navigationModel from '../../config/navigation'; 7 | import registerServiceWorker from '../../services/sw/registerServiceWorker'; 8 | 9 | type Props = { 10 | children: ReactNode; 11 | } & RouteComponentProps; 12 | 13 | function MainLayout({ children }: Props) { 14 | // #region on mount effect 15 | useEffect(() => { 16 | if (typeof window !== undefined) { 17 | // register service worker (no worry about multiple attempts to register, browser will ignore when already registered) 18 | registerServiceWorker(); 19 | } 20 | }, []); 21 | // #endregion 22 | 23 | // #region callbacks 24 | /* eslint-disable no-unused-vars*/ 25 | const handleLeftNavItemClick = useCallback( 26 | (/* event: React.MouseEvent, viewName?: string */) => { 27 | // something to do here? 28 | }, 29 | [], 30 | ); 31 | 32 | const handleRightNavItemClick = useCallback( 33 | (/* event: React.MouseEvent, viewName?: string */) => { 34 | // something to do here? 35 | }, 36 | [], 37 | ); 38 | // #endregion 39 | 40 | return ( 41 | 42 |
43 | 49 |
{children}
50 | 51 |
52 |
53 | ); 54 | } 55 | 56 | export default withRouter(MainLayout); 57 | -------------------------------------------------------------------------------- /front/src/layout/mainLayout/__tests__/MainLayout.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { ThemeProvider } from 'styled-components'; 4 | import { MemoryRouter } from 'react-router'; 5 | import MainLayout from '../index'; 6 | 7 | // NOTE: we mock Navigation component (we are not testing this one) so we won't need redux provider for MainLayout test anymore 8 | jest.mock('../../../components/navigation'); 9 | jest.mock('../../../components/backToTop/BackToTop'); 10 | jest.mock('../../../services/sw/registerServiceWorker'); 11 | 12 | describe('MainLayout component', () => { 13 | let rootElement: any = null; 14 | 15 | beforeEach(() => { 16 | rootElement = document.createElement('div'); 17 | document.body.appendChild(rootElement); 18 | 19 | jest.restoreAllMocks(); 20 | }); 21 | 22 | afterEach(() => { 23 | rootElement && document.body.removeChild(rootElement); 24 | rootElement = null; 25 | }); 26 | 27 | it.only('renders as expected', () => { 28 | // eslint-disable-next-line @typescript-eslint/no-var-requires 29 | const NavigationBar = require('../../../components/navigation'); 30 | // Redux connect (shoudl return React component) is "default exported". 31 | // IMPORTANT: here we mock the return value of connect to mock NavigationBar component: 32 | NavigationBar.default.mockReturnValueOnce(() => navbar); 33 | 34 | // eslint-disable-next-line @typescript-eslint/no-var-requires 35 | const BackToTop = require('../../../components/backToTop/BackToTop'); 36 | BackToTop.default.mockImplementationOnce(() => ( 37 | backtotopbutton 38 | )); 39 | 40 | const { container } = render( 41 | 42 | 43 | 44 |

children here

45 |
46 |
47 |
, 48 | rootElement, 49 | ); 50 | expect(container.firstChild).toMatchSnapshot(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /front/src/layout/mainLayout/__tests__/__snapshots__/MainLayout.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`MainLayout component renders as expected 1`] = ` 4 |
7 |
10 |

11 | children here 12 |

13 |
14 | 15 | backtotopbutton 16 | 17 |
18 | `; 19 | -------------------------------------------------------------------------------- /front/src/layout/mainLayout/index.ts: -------------------------------------------------------------------------------- 1 | import MainLayout from './MainLayout'; 2 | 3 | export default MainLayout; 4 | -------------------------------------------------------------------------------- /front/src/mock/fakeAPI.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "label": "item 1" 5 | }, 6 | { 7 | "id": 2, 8 | "label": "item 2" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /front/src/mock/userInfosMock.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJkZW1vIiwiaWF0IjoxNTAyMzA3MzU0LCJleHAiOjE3MjMyMzIxNTQsImF1ZCI6ImRlbW8tZGVtbyIsInN1YiI6ImRlbW8iLCJHaXZlbk5hbWUiOiJKb2huIiwiU3VybmFtZSI6IkRvZSIsIkVtYWlsIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJSb2xlIjpbIlN1cGVyIGNvb2wgZGV2IiwibWFnaWMgbWFrZXIiXX0.6FjgLCypaqmRp4tDjg_idVKIzQw16e-z_rjA3R94IqQ", 3 | "user": { 4 | "id": 111, 5 | "login": "john.doe@fake.mail", 6 | "firstname": "John", 7 | "lastname": "Doe", 8 | "isAdmin": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /front/src/pages/about/About.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RouteComponentProps, withRouter } from 'react-router'; 3 | import FadeInEntrance from '../../components/fadeInEntrance'; 4 | import { OwnProps, ReduxConnectedProps } from './index'; 5 | 6 | // #region types 7 | export type Props = RouteComponentProps & ReduxConnectedProps & OwnProps; 8 | // #endregion 9 | 10 | function About() { 11 | return ( 12 | 13 |

About

14 |
15 | ); 16 | } 17 | 18 | About.displayName = 'About'; 19 | 20 | export default withRouter(About); 21 | -------------------------------------------------------------------------------- /front/src/pages/about/__tests__/About.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { MemoryRouter } from 'react-router'; 3 | import { Provider } from 'react-redux'; 4 | import { ThemeProvider } from 'styled-components'; 5 | import configureStore from 'redux-mock-store'; 6 | import { render } from '@testing-library/react'; 7 | import About from '../index'; 8 | 9 | const middlewares: Array = []; 10 | const mockStore = configureStore(middlewares); 11 | 12 | describe('About page', () => { 13 | let rootElement: any = null; 14 | 15 | beforeEach(() => { 16 | rootElement = document.createElement('div'); 17 | document.body.appendChild(rootElement); 18 | }); 19 | 20 | afterEach(() => { 21 | rootElement && document.body.removeChild(rootElement); 22 | rootElement = null; 23 | }); 24 | 25 | it('renders as expected', () => { 26 | const initialState = {}; 27 | const store = mockStore(initialState); 28 | 29 | const { container } = render( 30 | 31 | 32 | 33 | 34 | 35 | 36 | , 37 | rootElement, 38 | ); 39 | expect(container.firstChild).toMatchSnapshot(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /front/src/pages/about/__tests__/__snapshots__/About.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`About page renders as expected 1`] = ` 4 |
7 |

8 | About 9 |

10 |
11 | `; 12 | -------------------------------------------------------------------------------- /front/src/pages/about/index.ts: -------------------------------------------------------------------------------- 1 | import { bindActionCreators, compose, Dispatch } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | import About from './About'; 4 | 5 | // #region redux map state and dispatch to props 6 | const mapStateToProps = (/* state: RootState */) => { 7 | return {}; 8 | }; 9 | 10 | const mapDispatchToProps = (dispatch: Dispatch) => { 11 | return bindActionCreators({}, dispatch); 12 | }; 13 | // #endregion 14 | 15 | // #region types 16 | export type ReduxConnectedProps = Record; 17 | export type OwnProps = Record; 18 | // #endregion 19 | 20 | const connector = connect(mapStateToProps, mapDispatchToProps); 21 | export default compose(connector)(About); 22 | -------------------------------------------------------------------------------- /front/src/pages/home/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RouteComponentProps, withRouter } from 'react-router'; 3 | import { Link } from 'react-router-dom'; 4 | import Jumbotron from 'reactstrap/lib/Jumbotron'; 5 | import FadeInEntrance from '../../components/fadeInEntrance'; 6 | import HomeInfo from './styled/HomeInfo'; 7 | import MainTitle from './styled/MainTitle'; 8 | import { ReduxConnectedProps, OwnProps } from './index'; 9 | 10 | // #region types 11 | export type Props = RouteComponentProps & ReduxConnectedProps & OwnProps; 12 | // #endregion 13 | 14 | function Home() { 15 | return ( 16 | 17 | 18 | 19 | ReactJS 16.14 Bootstrap 4 20 |

with Hot Reload

21 |

and React Router v5.x

22 |

and webpack 5.x

23 |

Starter

24 |

25 | 26 | 27 |   go to about 28 | 29 |

30 |
31 |
32 |
33 | ); 34 | } 35 | 36 | Home.displayName = 'Home'; 37 | 38 | export default withRouter(Home); 39 | -------------------------------------------------------------------------------- /front/src/pages/home/__tests__/Home.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { MemoryRouter } from 'react-router'; 3 | import { Provider } from 'react-redux'; 4 | import { ThemeProvider } from 'styled-components'; 5 | import configureStore from 'redux-mock-store'; 6 | import { render } from '@testing-library/react'; 7 | import Home from '../index'; 8 | 9 | const middlewares: Array = []; 10 | const mockStore = configureStore(middlewares); 11 | 12 | describe('Home page', () => { 13 | let rootElement: any = null; 14 | 15 | beforeEach(() => { 16 | rootElement = document.createElement('div'); 17 | document.body.appendChild(rootElement); 18 | }); 19 | 20 | afterEach(() => { 21 | rootElement && document.body.removeChild(rootElement); 22 | rootElement = null; 23 | }); 24 | 25 | it('renders as expected', () => { 26 | const initialState = {}; 27 | const store = mockStore(initialState); 28 | 29 | const { container } = render( 30 | 31 | 32 | 33 | 34 | 35 | 36 | , 37 | rootElement, 38 | ); 39 | expect(container.firstChild).toMatchSnapshot(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /front/src/pages/home/__tests__/__snapshots__/Home.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Home page renders as expected 1`] = ` 4 |
7 |
10 |
13 |

16 | ReactJS 16.14 Bootstrap 4 17 |

18 |

19 | with Hot Reload ( 20 | 21 | react-hot-loader 4+ 22 | 23 | )!!! 24 |

25 |

26 | and React Router v5 27 |

28 |

29 | and webpack 5.x 30 |

31 |

32 | Starter 33 |

34 |

35 | 39 | 42 |   go to about 43 | 44 |

45 |
46 |
47 |
48 | `; 49 | -------------------------------------------------------------------------------- /front/src/pages/home/index.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { bindActionCreators, compose, Dispatch } from 'redux'; 3 | import Home from './Home'; 4 | 5 | // #region redux map state and dispatch to props 6 | const mapStateToProps = (/* state: RootState */) => { 7 | return {}; 8 | }; 9 | 10 | const mapDispatchToProps = (dispatch: Dispatch) => { 11 | return bindActionCreators({}, dispatch); 12 | }; 13 | // #endregion 14 | 15 | // #region types 16 | export type ReduxConnectedProps = Record; 17 | export type OwnProps = Record; 18 | // #endregion 19 | 20 | const connector = connect(mapStateToProps, mapDispatchToProps); 21 | export default compose(connector)(Home); 22 | -------------------------------------------------------------------------------- /front/src/pages/home/styled/HomeInfo.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const HomeInfo = styled.div``; 4 | 5 | export default HomeInfo; 6 | -------------------------------------------------------------------------------- /front/src/pages/home/styled/LightNote.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const LightNote = styled.i` 4 | font-size: 0.7em; 5 | `; 6 | 7 | export default LightNote; 8 | -------------------------------------------------------------------------------- /front/src/pages/home/styled/MainTitle.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const MainTitle = styled.h1` 4 | color: #222 !important; 5 | font-weight: 800; 6 | `; 7 | 8 | export default MainTitle; 9 | -------------------------------------------------------------------------------- /front/src/pages/login/__tests__/Login.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { MemoryRouter } from 'react-router'; 4 | import { ThemeProvider } from 'styled-components'; 5 | import configureStore from 'redux-mock-store'; 6 | import thunk from 'redux-thunk'; 7 | import { render } from '@testing-library/react'; 8 | import Login from '../index'; 9 | 10 | const middlewares = [thunk]; 11 | const mockStore = configureStore(middlewares); 12 | 13 | describe('Login page', () => { 14 | let rootElement: any = null; 15 | 16 | beforeEach(() => { 17 | rootElement = document.createElement('div'); 18 | document.body.appendChild(rootElement); 19 | }); 20 | 21 | afterEach(() => { 22 | rootElement && document.body.removeChild(rootElement); 23 | rootElement = null; 24 | }); 25 | 26 | it('renders as expected', () => { 27 | const initialState = { 28 | userAuth: { 29 | isFetching: false, 30 | isLogging: false, 31 | id: '', 32 | login: '', 33 | firstname: '', 34 | lastname: '', 35 | token: '', 36 | isAuthenticated: false, 37 | }, 38 | }; 39 | const store = mockStore(initialState); 40 | 41 | const { container } = render( 42 | 43 | 44 | 45 | 46 | 47 | 48 | , 49 | rootElement, 50 | ); 51 | expect(container.firstChild).toMatchSnapshot(); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /front/src/pages/login/__tests__/__snapshots__/Login.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Login page renders as expected 1`] = ` 4 |
7 |
10 |
13 |
16 |
20 |
21 | 22 | Login 23 | 24 |
27 | 33 |
36 | 44 |
45 |
46 |
49 | 55 |
58 | 66 |
67 |
68 |
71 |
74 | 82 |
83 |
84 |
85 |
86 |
87 |
88 |
91 |
94 |
97 | 103 |
104 |
105 |
106 |
107 |
108 | `; 109 | -------------------------------------------------------------------------------- /front/src/pages/login/index.ts: -------------------------------------------------------------------------------- 1 | import { bindActionCreators, compose, Dispatch } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | import * as userAuthActions from '../../redux/modules/userAuth'; 4 | import Login from './Login'; 5 | import { 6 | makeGetIsAuthenticatedSelector, 7 | makeGetIsFetchingSelector, 8 | makeGetIsLoggingSelector, 9 | } from '../../redux/modules/userAuth/selectors'; 10 | 11 | // #region create selectors instances 12 | const getIsAuthenticated = makeGetIsAuthenticatedSelector(); 13 | const getIsFetching = makeGetIsFetchingSelector(); 14 | const getIsLogging = makeGetIsLoggingSelector(); 15 | // #endregion 16 | 17 | // #region redux map state and dispatch to props 18 | const mapStateToProps = (state: RootState /* , ownProps: OwnProps */) => { 19 | return { 20 | isAuthenticated: getIsAuthenticated(state), 21 | isFetching: getIsFetching(state), 22 | isLogging: getIsLogging(state), 23 | }; 24 | }; 25 | 26 | const mapDispatchToProps = (dispatch: Dispatch) => { 27 | return bindActionCreators({ ...userAuthActions }, dispatch); 28 | }; 29 | // #endregion 30 | 31 | // #region types 32 | export type ReduxConnectedProps = Pick< 33 | UserAuthState, 34 | 'isAuthenticated' | 'isFetching' | 'isLogging' 35 | > & 36 | UserAuthActions; 37 | export type OwnProps = Record; 38 | // #endregion 39 | 40 | const connector = connect(mapStateToProps, mapDispatchToProps); 41 | export default compose(connector)(Login); 42 | -------------------------------------------------------------------------------- /front/src/pages/pageNotFound/PageNotFound.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RouteComponentProps, withRouter } from 'react-router'; 3 | import Jumbotron from 'reactstrap/lib/Jumbotron'; 4 | import FadeInEntrance from '../../components/fadeInEntrance'; 5 | import { OwnProps, ReduxConnectedProps } from './index'; 6 | 7 | // #region types 8 | export type Props = RouteComponentProps & ReduxConnectedProps & OwnProps; 9 | // #endregion 10 | 11 | function PageNotFound() { 12 | return ( 13 | 14 | 15 |

Sorry this page does not exists...

16 |
17 |
18 | ); 19 | } 20 | 21 | PageNotFound.displayName = 'PageNotFound'; 22 | 23 | export default withRouter(PageNotFound); 24 | -------------------------------------------------------------------------------- /front/src/pages/pageNotFound/__tests__/PageNotFound.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { MemoryRouter } from 'react-router'; 3 | import { Provider } from 'react-redux'; 4 | import configureStore from 'redux-mock-store'; 5 | import { ThemeProvider } from 'styled-components'; 6 | import { render } from '@testing-library/react'; 7 | import PageNotFound from '../index'; 8 | 9 | const middlewares: Array = []; 10 | const mockStore = configureStore(middlewares); 11 | 12 | describe('PageNotFound page', () => { 13 | let rootElement: any = null; 14 | 15 | beforeEach(() => { 16 | rootElement = document.createElement('div'); 17 | document.body.appendChild(rootElement); 18 | }); 19 | 20 | afterEach(() => { 21 | rootElement && document.body.removeChild(rootElement); 22 | rootElement = null; 23 | }); 24 | 25 | it('renders as expected', () => { 26 | const initialState = {}; 27 | const store = mockStore(initialState); 28 | 29 | const { container } = render( 30 | 31 | 32 | 33 | 34 | 35 | 36 | , 37 | rootElement, 38 | ); 39 | expect(container.firstChild).toMatchSnapshot(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /front/src/pages/pageNotFound/__tests__/__snapshots__/PageNotFound.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`PageNotFound page renders as expected 1`] = ` 4 |
7 |
10 |

11 | Sorry this page does not exists... 12 |

13 |
14 |
15 | `; 16 | -------------------------------------------------------------------------------- /front/src/pages/pageNotFound/index.ts: -------------------------------------------------------------------------------- 1 | import { bindActionCreators, compose, Dispatch } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | import PageNotFound from './PageNotFound'; 4 | 5 | // #region redux map state and dispatch to props 6 | const mapStateToProps = (/* state: RootState*/) => { 7 | return {}; 8 | }; 9 | 10 | const mapDispatchToProps = (dispatch: Dispatch) => { 11 | return bindActionCreators({}, dispatch); 12 | }; 13 | // #endregion 14 | 15 | // #region types 16 | export type ReduxConnectedProps = Record; 17 | export type OwnProps = Record; 18 | // #endregion 19 | 20 | const connector = connect(mapStateToProps, mapDispatchToProps); 21 | export default compose(connector)(PageNotFound); 22 | -------------------------------------------------------------------------------- /front/src/pages/protected/Protected.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RouteComponentProps, withRouter } from 'react-router'; 3 | import FadeInEntrance from '../../components/fadeInEntrance'; 4 | import { OwnProps, ReduxConnectedProps } from './index'; 5 | 6 | // #region types 7 | export type Props = RouteComponentProps & ReduxConnectedProps & OwnProps; 8 | // #endregion 9 | 10 | function Protected() { 11 | return ( 12 | 13 |

Protected view

14 |

If you can read, it means you are authenticated

15 |
16 | ); 17 | } 18 | 19 | Protected.displayName = 'Protected'; 20 | 21 | export default withRouter(Protected); 22 | -------------------------------------------------------------------------------- /front/src/pages/protected/__tests__/Protected.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { MemoryRouter } from 'react-router'; 3 | import { Provider } from 'react-redux'; 4 | import { ThemeProvider } from 'styled-components'; 5 | import configureStore from 'redux-mock-store'; 6 | import { render } from '@testing-library/react'; 7 | import Protected from '../index'; 8 | 9 | const middlewares: Array = []; 10 | const mockStore = configureStore(middlewares); 11 | 12 | describe('Protected page', () => { 13 | let rootElement: any = null; 14 | 15 | beforeEach(() => { 16 | rootElement = document.createElement('div'); 17 | document.body.appendChild(rootElement); 18 | }); 19 | 20 | afterEach(() => { 21 | rootElement && document.body.removeChild(rootElement); 22 | rootElement = null; 23 | }); 24 | 25 | it('renders as expected', () => { 26 | const initialState = {}; 27 | const store = mockStore(initialState); 28 | 29 | const { container } = render( 30 | 31 | 32 | 33 | 34 | 35 | 36 | , 37 | rootElement, 38 | ); 39 | expect(container.firstChild).toMatchSnapshot(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /front/src/pages/protected/__tests__/__snapshots__/Protected.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Protected page renders as expected 1`] = ` 4 |
7 |

8 | Protected view 9 |

10 |

11 | If you can read, it means you are authenticated 12 |

13 |
14 | `; 15 | -------------------------------------------------------------------------------- /front/src/pages/protected/index.ts: -------------------------------------------------------------------------------- 1 | import { bindActionCreators, compose, Dispatch } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | import Protected from './Protected'; 4 | 5 | // #region redux map state and dispatch to props 6 | const mapStateToProps = (/* state: RootState */) => { 7 | return {}; 8 | }; 9 | 10 | const mapDispatchToProps = (dispatch: Dispatch) => { 11 | return bindActionCreators({}, dispatch); 12 | }; 13 | // #endregion 14 | 15 | // #region types 16 | export type ReduxConnectedProps = Record; 17 | export type OwnProps = Record; 18 | // #endregion 19 | 20 | const connector = connect(mapStateToProps, mapDispatchToProps); 21 | export default compose(connector)(Protected); 22 | -------------------------------------------------------------------------------- /front/src/redux/RootState.d.ts: -------------------------------------------------------------------------------- 1 | declare type RootState = { 2 | userAuth: UserAuthState; 3 | }; 4 | -------------------------------------------------------------------------------- /front/src/redux/middleware/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MacKentoch/react-redux-bootstrap-webpack-starter/0dd39685404d0857b61aef0d3b49e87c2fc1ba76/front/src/redux/middleware/.gitkeep -------------------------------------------------------------------------------- /front/src/redux/modules/reducers.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import userAuth from './userAuth'; 3 | 4 | export const reducers = { 5 | userAuth, 6 | }; 7 | 8 | export default combineReducers({ 9 | ...reducers, 10 | }); 11 | -------------------------------------------------------------------------------- /front/src/redux/modules/userAuth/__tests__/userAuth.test.ts: -------------------------------------------------------------------------------- 1 | import configureMockStore from 'redux-mock-store'; 2 | import { format } from 'date-fns'; 3 | import thunk from 'redux-thunk'; 4 | import { disconnectUser, checkUserIsConnected } from '../index'; 5 | 6 | // #region constants 7 | const middlewares = [thunk]; 8 | const mockStore = configureMockStore( 9 | middlewares, 10 | ); 11 | // #endregion 12 | 13 | // #region jest mocks (JSON files) 14 | jest.mock('../../../../services/API/fetchTools', () => ({ 15 | getLocationOrigin: 'http://localhost', 16 | })); 17 | 18 | jest.mock('../../../../services/auth', () => ({ 19 | getToken() { 20 | return 'fake_token_for_test'; 21 | }, 22 | 23 | getUserInfo() { 24 | return { 25 | _id: 'some_fake_id', 26 | }; 27 | }, 28 | 29 | clearAllAppStorage() { 30 | return true; 31 | }, 32 | 33 | isExpiredToken() { 34 | return true; 35 | }, 36 | })); 37 | // #endregion 38 | 39 | describe('userAuth action creators', () => { 40 | let store: any = null; 41 | const newActionTime = format(new Date(), 'dd/MM/yyyy HH:MM'); 42 | const initialState: UserAuthState = { 43 | // actions details 44 | isFetching: false, 45 | isLogging: false, 46 | actionTime: '', 47 | 48 | // userInfos 49 | id: 'some_fake_id', 50 | login: '', 51 | firstname: '', 52 | lastname: '', 53 | token: 'fake_token_for_test', 54 | isAuthenticated: true, // authentication status (token based auth) 55 | }; 56 | 57 | beforeEach(() => { 58 | store = mockStore(initialState); 59 | }); 60 | 61 | it('disconnectUser should return valid action', async () => { 62 | const expectedAction = { type: 'DISCONNECT_USER' }; 63 | 64 | const action = disconnectUser(); 65 | expect(action).toEqual(expectedAction); 66 | store.dispatch(disconnectUser()); 67 | const actions = store.getActions(); 68 | const expectedPayload = { type: 'DISCONNECT_USER' }; 69 | expect(actions).toEqual([expectedPayload]); 70 | }); 71 | 72 | it('checkUserIsConnected should return valid action', async () => { 73 | const expectedAction = { 74 | type: 'CHECK_IF_USER_IS_AUTHENTICATED', 75 | actionTime: newActionTime, 76 | _id: 'some_fake_id', 77 | token: 'fake_token_for_test', 78 | isAuthenticated: false, 79 | }; 80 | store.dispatch(checkUserIsConnected()); 81 | const actions = store.getActions(); 82 | const expectedPayload = expectedAction; 83 | 84 | expect(actions).toEqual([expectedPayload]); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /front/src/redux/modules/userAuth/selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | // #region base selectors (state or additionnal props) 4 | export const UserAuthStateSelector = (state: RootState) => state.userAuth; 5 | // #endregion 6 | 7 | // #region consummed selectors 8 | export function makeGetIsFetchingSelector() { 9 | const getIsFetching = createSelector( 10 | UserAuthStateSelector, 11 | (UserAuthState) => { 12 | const isFetching = UserAuthState.isFetching; 13 | return isFetching; 14 | }, 15 | ); 16 | 17 | return getIsFetching; 18 | } 19 | 20 | export function makeGetActionTimeSelector() { 21 | const getActionTime = createSelector( 22 | UserAuthStateSelector, 23 | (UserAuthState) => { 24 | const actionTime = UserAuthState.actionTime; 25 | return actionTime; 26 | }, 27 | ); 28 | 29 | return getActionTime; 30 | } 31 | 32 | export function makeGetIsLoggingSelector() { 33 | const getIsLogging = createSelector( 34 | UserAuthStateSelector, 35 | (UserAuthState) => { 36 | const isLogging = UserAuthState.isLogging; 37 | return isLogging; 38 | }, 39 | ); 40 | 41 | return getIsLogging; 42 | } 43 | 44 | export function makeGetIdSelector() { 45 | const getId = createSelector(UserAuthStateSelector, (UserAuthState) => { 46 | const id = UserAuthState.id; 47 | return id; 48 | }); 49 | 50 | return getId; 51 | } 52 | 53 | export function makeGetLoginSelector() { 54 | const getLogin = createSelector(UserAuthStateSelector, (UserAuthState) => { 55 | const login = UserAuthState.login; 56 | return login; 57 | }); 58 | 59 | return getLogin; 60 | } 61 | 62 | export function makeGetFirstnameSelector() { 63 | const getFirstname = createSelector( 64 | UserAuthStateSelector, 65 | (UserAuthState) => { 66 | const firstname = UserAuthState.firstname; 67 | return firstname; 68 | }, 69 | ); 70 | 71 | return getFirstname; 72 | } 73 | 74 | export function makeGetLastnameSelector() { 75 | const getLastname = createSelector(UserAuthStateSelector, (UserAuthState) => { 76 | const lastname = UserAuthState.lastname; 77 | return lastname; 78 | }); 79 | 80 | return getLastname; 81 | } 82 | 83 | export function makeGetTokenSelector() { 84 | const getTokenSelector = createSelector( 85 | UserAuthStateSelector, 86 | (UserAuthState) => { 87 | const token = UserAuthState.token; 88 | return token; 89 | }, 90 | ); 91 | 92 | return getTokenSelector; 93 | } 94 | 95 | export function makeGetIsAuthenticatedSelector() { 96 | const getIsAuthenticatedSelector = createSelector( 97 | UserAuthStateSelector, 98 | (UserAuthState) => { 99 | const isAuthenticated = UserAuthState.isAuthenticated; 100 | return isAuthenticated; 101 | }, 102 | ); 103 | 104 | return getIsAuthenticatedSelector; 105 | } 106 | // #endregion 107 | -------------------------------------------------------------------------------- /front/src/redux/modules/userAuth/types.d.ts: -------------------------------------------------------------------------------- 1 | declare type UserAuthState = { 2 | isFetching: boolean; 3 | actionTime: string; 4 | isLogging: boolean; 5 | } & User; 6 | 7 | declare type UserAuthAction = { 8 | type: ActionType; 9 | 10 | data?: Array | any; 11 | error?: any; 12 | payload?: any; 13 | } & Partial & 14 | Partial<{ user: Partial }>; 15 | 16 | declare type UserAuthActions = { 17 | checkUserIsConnected: () => void; 18 | logUserIfNeeded: (email: string, password: string) => Promise; 19 | fetchUserInfoDataIfNeeded: (id: string) => Promise; 20 | disconnectUser: () => void; 21 | }; 22 | -------------------------------------------------------------------------------- /front/src/redux/store/configureStore.ts: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import { composeWithDevTools } from 'redux-devtools-extension'; 3 | import { createBrowserHistory as createHistory } from 'history'; 4 | import { persistReducer, persistStore } from 'redux-persist'; 5 | import storage from 'redux-persist/lib/storage'; 6 | import thunkMiddleware from 'redux-thunk'; 7 | import { createLogger } from 'redux-logger'; 8 | import reducer from '../modules/reducers'; 9 | 10 | // #region constants 11 | const isProd = process.env.NODE_ENV === 'production'; 12 | export const history = createHistory(); 13 | // #endregion 14 | 15 | // #region createStore : enhancer 16 | 17 | // #region logger middleware (dev only) 18 | const loggerMiddleware = createLogger({ 19 | level: 'info', 20 | collapsed: true, 21 | }); 22 | // #endregion 23 | 24 | const enhancer = !isProd 25 | ? composeWithDevTools( 26 | applyMiddleware( 27 | thunkMiddleware, 28 | loggerMiddleware, // logger at the end 29 | ), 30 | ) 31 | : composeWithDevTools(applyMiddleware(thunkMiddleware)); 32 | // #endregion 33 | 34 | // #region persisted reducer 35 | const persistConfig = { 36 | key: 'root', 37 | storage, 38 | blacklist: ['router'], 39 | // whitelist: ['userAuth'], 40 | }; 41 | 42 | const persistedReducer = persistReducer(persistConfig, reducer); 43 | // #endregion 44 | 45 | export default function configureStore(initialState = {}) { 46 | const store = createStore(persistedReducer, initialState, enhancer); 47 | const persistor = persistStore(store); 48 | 49 | if ((module as any).hot) { 50 | (module as any).hot?.accept('../modules/reducers', () => { 51 | // @ts-ignore 52 | const nextAppReducer = require('../modules/reducers').default; 53 | store.replaceReducer(persistReducer(persistConfig, nextAppReducer)); 54 | }); 55 | } 56 | 57 | return { store, persistor }; 58 | } 59 | -------------------------------------------------------------------------------- /front/src/routes/MainRoutes.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Switch } from 'react-router'; 3 | import { 4 | Home as AsyncHome, 5 | About as AsyncAbout, 6 | PageNotFound as AsyncPageNotFound, 7 | Protected as AsyncProtected, 8 | } from './routes'; 9 | import PrivateRoute from '../components/privateRoute'; 10 | 11 | const MainRoutes = () => { 12 | return ( 13 | 14 | {/* public views: */} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {/* private views: need user to be authenticated */} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default MainRoutes; 36 | -------------------------------------------------------------------------------- /front/src/routes/routes.ts: -------------------------------------------------------------------------------- 1 | import loadable from 'loadable-components'; 2 | 3 | export const Home = loadable(() => import('../pages/home'), { 4 | modules: ['home'], 5 | }); 6 | export const About = loadable(() => import('../pages/about'), { 7 | modules: ['about'], 8 | }); 9 | export const Login = loadable(() => import('../pages/login'), { 10 | modules: ['login'], 11 | }); 12 | export const Protected = loadable(() => import('../pages/protected'), { 13 | modules: ['protected'], 14 | }); 15 | export const PageNotFound = loadable(() => import('../pages/pageNotFound'), { 16 | modules: ['pageNotFound'], 17 | }); 18 | -------------------------------------------------------------------------------- /front/src/services/API/example.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { 3 | getMethod, 4 | jsonHeader, 5 | defaultOptions, 6 | getLocationOrigin, 7 | } from './fetchTools'; 8 | 9 | export const getSomething = async ( 10 | endpoint = 'api/getSomethingByDefault', 11 | ): Promise => { 12 | const method = getMethod.method; 13 | const headers = jsonHeader; 14 | const url = `${getLocationOrigin()}/${endpoint}`; 15 | const options = { ...defaultOptions }; 16 | 17 | const data = await axios.request({ 18 | method, 19 | url, 20 | withCredentials: true, 21 | ...headers, 22 | ...options, 23 | }); 24 | return data; 25 | }; 26 | -------------------------------------------------------------------------------- /front/src/services/API/fetchTools.ts: -------------------------------------------------------------------------------- 1 | import { Base64 } from 'js-base64'; 2 | import { Method } from 'axios'; 3 | 4 | export type GetMethod = { 5 | method: Method; 6 | }; 7 | 8 | export type PostMethod = GetMethod; 9 | 10 | // #region window.location.origin polyfill 11 | export const getLocationOrigin = (): string => { 12 | if (!window.location.origin) { 13 | const windowLocationOrigin = `${window.location.protocol}//${ 14 | window.location.hostname 15 | }${window.location.port ? ':' + window.location.port : ''}`; 16 | return windowLocationOrigin; 17 | } 18 | return window.location.origin; 19 | }; 20 | // #endregion 21 | 22 | // #region query options: 23 | export const getMethod: GetMethod = { 24 | method: 'get', 25 | }; 26 | 27 | export const postMethod: PostMethod = { 28 | method: 'post', 29 | }; 30 | 31 | export const defaultOptions = { 32 | credentials: 'same-origin', 33 | }; 34 | 35 | export const jsonHeader = { 36 | headers: { 37 | Accept: 'application/json', 38 | 'Content-Type': 'application/json', 39 | // 'Access-control-Allow-Origin': '*' 40 | }, 41 | }; 42 | // #endregion 43 | 44 | // #region general helpers 45 | export const encodeBase64 = (stringToEncode = ''): string => { 46 | return Base64.encode(stringToEncode); 47 | }; 48 | // #endregion 49 | -------------------------------------------------------------------------------- /front/src/services/getLocationOrigin/index.ts: -------------------------------------------------------------------------------- 1 | export const getLocationOrigin = (): string => { 2 | if (!window.location.origin) { 3 | const windowLocationOrigin = `${window.location.protocol}//${ 4 | window.location.hostname 5 | }${window.location.port ? ':' + window.location.port : ''}`; 6 | 7 | return windowLocationOrigin; 8 | } 9 | 10 | return window.location.origin; 11 | }; 12 | 13 | export default getLocationOrigin; 14 | -------------------------------------------------------------------------------- /front/src/services/sw/registerServiceWorker.ts: -------------------------------------------------------------------------------- 1 | import appConfig from '../../config/appConfig'; 2 | 3 | // #region constants 4 | const { path: swPath } = appConfig.sw; 5 | // #endregion 6 | 7 | function registerServiceWorker(): void { 8 | if (typeof window !== undefined) { 9 | if ('serviceWorker' in navigator) { 10 | window.addEventListener('load', async () => { 11 | try { 12 | const registration = await navigator.serviceWorker.register(swPath); 13 | console.log('SW registered: ', registration); 14 | } catch (error) { 15 | console.log('SW registration failed: ', error); 16 | } 17 | }); 18 | } 19 | } 20 | } 21 | 22 | export default registerServiceWorker; 23 | -------------------------------------------------------------------------------- /front/src/style/GlobalStyles.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | const GlobalStyle = createGlobalStyle` 4 | html, body { 5 | margin: 0; 6 | height: 100%; 7 | -webkit-font-smoothing: antialiased; 8 | } 9 | 10 | * { 11 | box-sizing: border-box; 12 | } 13 | 14 | a { 15 | text-decoration: none; 16 | color: inherit; 17 | &:hover { 18 | text-decoration: none; 19 | } 20 | } 21 | `; 22 | 23 | export default GlobalStyle; 24 | -------------------------------------------------------------------------------- /front/src/types/auth/index.d.ts: -------------------------------------------------------------------------------- 1 | declare type STORES_TYPES = 'localStorage' | 'sessionStorage'; 2 | declare type TokenKey = string; 3 | declare type UserInfoKey = string; 4 | -------------------------------------------------------------------------------- /front/src/types/process.d.ts: -------------------------------------------------------------------------------- 1 | // NOTE: this is not a node project better fast fix process.env typescrip error: 2 | declare let process: any; 3 | -------------------------------------------------------------------------------- /front/src/types/require.d.ts: -------------------------------------------------------------------------------- 1 | // // NOTE: this is not a node project better fast fix require typescrip error: 2 | // declare let require: any; 3 | -------------------------------------------------------------------------------- /front/src/types/user/index.d.ts: -------------------------------------------------------------------------------- 1 | declare type User = { 2 | id: string; 3 | login: string; 4 | firstname: string; 5 | lastname: string; 6 | token: string; 7 | isAuthenticated: boolean; 8 | }; 9 | -------------------------------------------------------------------------------- /front/test/__mocks__/fileMock.ts: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /front/test/setupTests.js: -------------------------------------------------------------------------------- 1 | require('jest-localstorage-mock'); 2 | -------------------------------------------------------------------------------- /front/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": false, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /front/webpack.analyze.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const CompressionWebpackPlugin = require('compression-webpack-plugin'); 6 | const TerserPlugin = require('terser-webpack-plugin'); 7 | const workboxPlugin = require('workbox-webpack-plugin'); 8 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 9 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); 10 | const BundleAnalyzerPlugin = 11 | require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 12 | 13 | // #region constants 14 | const nodeModulesDir = path.join(__dirname, 'node_modules'); 15 | const indexFile = path.join(__dirname, 'src/index.tsx'); 16 | // #endregion 17 | 18 | const config = { 19 | target: 'web', 20 | mode: 'production', 21 | entry: { app: indexFile }, 22 | output: { 23 | path: path.join(__dirname, '/../docs/assets'), 24 | publicPath: '/assets/', 25 | filename: '[name].[contenthash].js', 26 | chunkFilename: '[name].[chunkhash].js', 27 | assetModuleFilename: 'assets/[contenthash][ext][query]', 28 | }, 29 | resolve: { 30 | modules: ['src', 'node_modules'], 31 | extensions: ['.css', '.json', '.js', '.jsx', '.ts', '.tsx'], 32 | }, 33 | module: { 34 | rules: [ 35 | { 36 | test: /\.ts(x)?$/, 37 | use: ['ts-loader'], 38 | exclude: [nodeModulesDir], 39 | }, 40 | { 41 | test: /\.css$/, 42 | use: [MiniCssExtractPlugin.loader, 'css-loader'], 43 | }, 44 | { 45 | test: /\.(eot|woff|woff2|ttf|svg|png|jpe?g|gif)(\?\S*)?$/, 46 | type: 'asset', 47 | }, 48 | ], 49 | }, 50 | optimization: { 51 | runtimeChunk: false, 52 | splitChunks: { 53 | cacheGroups: { 54 | commons: { 55 | test: /[\\/]node_modules[\\/]/, 56 | name: 'vendors', 57 | chunks: 'all', 58 | }, 59 | styles: { 60 | name: 'styles', 61 | test: /\.css$/, 62 | chunks: 'all', 63 | enforce: true, 64 | }, 65 | }, 66 | }, 67 | minimize: true, 68 | minimizer: [ 69 | new TerserPlugin({ 70 | parallel: true, 71 | }), 72 | new CssMinimizerPlugin({}), 73 | ], 74 | }, 75 | plugins: [ 76 | new HtmlWebpackPlugin({ 77 | template: 'src/index.html', 78 | filename: '../index.html', // hack since outPut path would place in '/dist/assets/' in place of '/dist/' 79 | }), 80 | new webpack.DefinePlugin({ 81 | 'process.env.NODE_ENV': JSON.stringify('production'), 82 | }), 83 | new MiniCssExtractPlugin({ 84 | filename: '[name].[contenthash].css', 85 | chunkFilename: '[id].[chunkhash].css', 86 | }), 87 | new CompressionWebpackPlugin({ 88 | filename: '[path][base].gz[query]', 89 | algorithm: 'gzip', 90 | test: new RegExp('\\.(js|css)$'), 91 | threshold: 10240, 92 | minRatio: 0.8, 93 | }), 94 | new workboxPlugin.GenerateSW({ 95 | swDest: 'sw.js', 96 | clientsClaim: true, 97 | skipWaiting: true, 98 | }), 99 | new BundleAnalyzerPlugin(), 100 | ], 101 | }; 102 | 103 | module.exports = config; 104 | -------------------------------------------------------------------------------- /front/webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 6 | const workboxPlugin = require('workbox-webpack-plugin'); 7 | 8 | // #region constants` 9 | const nodeModulesDir = path.join(__dirname, 'node_modules'); 10 | const indexFile = path.join(__dirname, 'src/index.tsx'); 11 | // #endregion 12 | 13 | const config = { 14 | target: 'web', 15 | mode: 'development', 16 | devtool: 'source-map', 17 | entry: { 18 | app: [indexFile], 19 | }, 20 | output: { 21 | path: path.join(__dirname, '/../docs/assets'), 22 | publicPath: '/assets/', 23 | filename: '[name].[contenthash].js', 24 | chunkFilename: '[name].[chunkhash].js', 25 | assetModuleFilename: 'assets/[contenthash][ext][query]', 26 | }, 27 | resolve: { 28 | modules: ['node_modules'], 29 | extensions: ['.css', '.json', '.js', '.jsx', '.ts', '.tsx'], 30 | }, 31 | module: { 32 | rules: [ 33 | { 34 | test: /\.ts(x)?$/, 35 | use: ['ts-loader'], 36 | exclude: [nodeModulesDir], 37 | }, 38 | { 39 | test: /\.css$/, 40 | use: ['style-loader', 'css-loader'], 41 | }, 42 | { 43 | test: /\.(eot|woff|woff2|ttf|svg|png|jpe?g|gif)(\?\S*)?$/, 44 | type: 'asset', 45 | }, 46 | ], 47 | }, 48 | optimization: { 49 | runtimeChunk: false, 50 | splitChunks: { 51 | cacheGroups: { 52 | commons: { 53 | test: /[\\/]node_modules[\\/]/, 54 | name: 'vendors', 55 | chunks: 'all', 56 | }, 57 | styles: { 58 | name: 'styles', 59 | test: /\.css$/, 60 | chunks: 'all', 61 | enforce: true, 62 | }, 63 | }, 64 | }, 65 | }, 66 | plugins: [ 67 | new HtmlWebpackPlugin({ 68 | template: 'src/index.html', 69 | filename: '../index.html', // hack since outPut path would place in '/dist/assets/' in place of '/dist/' 70 | }), 71 | new webpack.DefinePlugin({ 72 | 'process.env.NODE_ENV': JSON.stringify('development'), 73 | }), 74 | new MiniCssExtractPlugin({ 75 | filename: '[name].[contenthash].css', 76 | chunkFilename: '[id].[chunkhash].css', 77 | }), 78 | new workboxPlugin.GenerateSW({ 79 | swDest: 'sw.js', 80 | clientsClaim: true, 81 | skipWaiting: true, 82 | }), 83 | ], 84 | }; 85 | 86 | module.exports = config; 87 | -------------------------------------------------------------------------------- /front/webpack.hot.reload.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | // #region constants 7 | const nodeModulesDir = path.join(__dirname, 'node_modules'); 8 | const indexFile = path.join(__dirname, 'src/index.tsx'); 9 | // #endregion 10 | 11 | const config = { 12 | mode: 'development', 13 | target: 'web', 14 | devtool: 'eval-source-map', 15 | entry: { 16 | app: [indexFile], 17 | }, 18 | output: { 19 | path: path.join(__dirname, 'docs'), 20 | filename: '[name].[contenthash].js', 21 | chunkFilename: '[name].[chunkhash].js', 22 | }, 23 | resolve: { 24 | modules: ['node_modules'], 25 | extensions: ['.ts', '.tsx', '.js', '.jsx', '.css', '.json'], 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.ts(x)?$/, 31 | use: ['ts-loader'], 32 | exclude: [nodeModulesDir], 33 | }, 34 | { 35 | test: /\.css$/, 36 | use: ['style-loader', 'css-loader'], 37 | }, 38 | { 39 | test: /\.(eot|woff|woff2|ttf|svg|png|jpe?g|gif)(\?\S*)?$/, 40 | type: 'asset', 41 | }, 42 | ], 43 | }, 44 | optimization: { 45 | runtimeChunk: false, 46 | splitChunks: { 47 | cacheGroups: { 48 | commons: { 49 | test: /[\\/]node_modules[\\/]/, 50 | name: 'vendors', 51 | chunks: 'all', 52 | }, 53 | styles: { 54 | name: 'styles', 55 | test: /\.css$/, 56 | chunks: 'all', 57 | enforce: true, 58 | }, 59 | }, 60 | }, 61 | }, 62 | devServer: { 63 | host: 'localhost', 64 | port: 3001, 65 | hot: true, 66 | static: path.join(__dirname, 'docs'), 67 | historyApiFallback: true, // browser refresh will fail otherwise 68 | headers: { 69 | 'Access-Control-Allow-Origin': '*', 70 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 71 | 'Access-Control-Allow-Headers': 72 | 'X-Requested-With, content-type, Authorization', 73 | }, 74 | }, 75 | plugins: [ 76 | new HtmlWebpackPlugin({ 77 | template: 'index.html', 78 | // filename: '../index.html', // hack since outPut path would place in '/dist/assets/' in place of '/dist/' 79 | }), 80 | new webpack.DefinePlugin({ 81 | 'process.env.NODE_ENV': JSON.stringify('development'), 82 | }), 83 | ], 84 | }; 85 | 86 | module.exports = config; 87 | -------------------------------------------------------------------------------- /front/webpack.production.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const CompressionWebpackPlugin = require('compression-webpack-plugin'); 6 | const TerserPlugin = require('terser-webpack-plugin'); 7 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 8 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); 9 | const workboxPlugin = require('workbox-webpack-plugin'); 10 | 11 | // #region constants 12 | const nodeModulesDir = path.join(__dirname, 'node_modules'); 13 | const indexFile = path.join(__dirname, 'src/index.tsx'); 14 | // #endregion 15 | 16 | const config = { 17 | target: 'web', 18 | mode: 'production', 19 | entry: { app: indexFile }, 20 | output: { 21 | path: path.join(__dirname, '/../docs/assets'), 22 | publicPath: '/assets/', 23 | filename: '[name].[contenthash].js', 24 | chunkFilename: '[name].[chunkhash].js', 25 | assetModuleFilename: 'assets/[contenthash][ext][query]', 26 | }, 27 | resolve: { 28 | modules: ['src', 'node_modules'], 29 | extensions: ['.css', '.json', '.js', '.jsx', '.ts', '.tsx'], 30 | }, 31 | module: { 32 | rules: [ 33 | { 34 | test: /\.ts(x)?$/, 35 | use: ['ts-loader'], 36 | exclude: [nodeModulesDir], 37 | }, 38 | { 39 | test: /\.css$/, 40 | use: [MiniCssExtractPlugin.loader, 'css-loader'], 41 | }, 42 | { 43 | test: /\.(eot|woff|woff2|ttf|svg|png|jpe?g|gif)(\?\S*)?$/, 44 | type: 'asset', 45 | }, 46 | ], 47 | }, 48 | optimization: { 49 | runtimeChunk: false, 50 | splitChunks: { 51 | cacheGroups: { 52 | commons: { 53 | test: /[\\/]node_modules[\\/]/, 54 | name: 'vendors', 55 | chunks: 'all', 56 | }, 57 | styles: { 58 | name: 'styles', 59 | test: /\.css$/, 60 | chunks: 'all', 61 | enforce: true, 62 | }, 63 | }, 64 | }, 65 | minimize: true, 66 | minimizer: [ 67 | new TerserPlugin({ 68 | parallel: true, 69 | }), 70 | new CssMinimizerPlugin({}), 71 | ], 72 | }, 73 | plugins: [ 74 | new HtmlWebpackPlugin({ 75 | template: 'src/index.html', 76 | filename: '../index.html', // hack since outPut path would place in '/dist/assets/' in place of '/dist/' 77 | }), 78 | new webpack.DefinePlugin({ 79 | 'process.env.NODE_ENV': JSON.stringify('production'), 80 | }), 81 | new MiniCssExtractPlugin({ 82 | filename: '[name].[contenthash].css', 83 | chunkFilename: '[id].[chunkhash].css', 84 | }), 85 | new CompressionWebpackPlugin({ 86 | filename: '[path][base].gz[query]', 87 | algorithm: 'gzip', 88 | test: new RegExp('\\.(js|css)$'), 89 | threshold: 10240, 90 | minRatio: 0.8, 91 | }), 92 | // IPORTANT: we need to serve app through https otherwise SW will throw error (so no SW in this simple case) 93 | new workboxPlugin.GenerateSW({ 94 | swDest: 'sw.js', 95 | clientsClaim: true, 96 | skipWaiting: true, 97 | }), 98 | ], 99 | }; 100 | 101 | module.exports = config; 102 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-bootstrap-webpack-starter", 3 | "version": "8.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "react-redux-bootstrap-webpack-starter", 9 | "version": "8.0.0", 10 | "license": "ISC", 11 | "engines": { 12 | "node": ">=16", 13 | "npm": ">=8.0.0" 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-bootstrap-webpack-starter", 3 | "version": "8.0.0", 4 | "description": "react js + redux + react router + hot reload + devTools + bootstrap + webpack starter", 5 | "author": "Erwan DATIN (MacKentoch)", 6 | "license": "ISC", 7 | "homepage": "https://github.com/MacKentoch/react-redux-bootstrap-webpack-starter#readme", 8 | "main": "", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/MacKentoch/react-redux-bootstrap-webpack-starter.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/MacKentoch/react-redux-bootstrap-webpack-starter/issues" 15 | }, 16 | "engines": { 17 | "node": ">=16", 18 | "npm": ">=8.0.0" 19 | }, 20 | "scripts": { 21 | "install-front": "echo '##### installing front dependencies #####' && cd ./front && npm install && cd ..", 22 | "install-server": "echo '##### installing server dependencies #####' && cd ./server && npm install && cd ..", 23 | "init": "npm run install-front && npm run install-server", 24 | "prefront-dev": "npm run init", 25 | "front-dev": "cd ./front && npm run start && cd ..", 26 | "prefront-test": "npm run init", 27 | "front-test": "cd ./front && npm run test && cd ..", 28 | "prefront-bundle-analyze": "npm run init", 29 | "front-bundle-analyze": "cd ./front && npm run analyze && cd ..", 30 | "prefront-prod": "npm run init", 31 | "front-prod": "cd ./front && npm run prod && cd ..", 32 | "prestart": "npm run init", 33 | "start": "cd ./server && npm run start-server && cd .." 34 | } 35 | } -------------------------------------------------------------------------------- /preview/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MacKentoch/react-redux-bootstrap-webpack-starter/0dd39685404d0857b61aef0d3b49e87c2fc1ba76/preview/preview.png -------------------------------------------------------------------------------- /server/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /server/.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /server/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "bracketSpacing": true, 5 | "jsxBracketSameLine": false, 6 | "singleQuote": true, 7 | "overrides": [], 8 | "printWidth": 80, 9 | "useTabs": false, 10 | "tabWidth": 2, 11 | "parser": "typescript" 12 | } 13 | -------------------------------------------------------------------------------- /server/jest.config.js: -------------------------------------------------------------------------------- 1 | const { jsWithBabel: tsjPreset } = require('ts-jest/presets'); 2 | 3 | module.exports = { 4 | preset: 'ts-jest', 5 | transform: { ...tsjPreset.transform }, 6 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', 7 | moduleFileExtensions: ['ts', 'tsx', 'json', 'node'], 8 | globals: { 9 | 'ts-jest': { 10 | babelConfig: false, 11 | }, 12 | }, 13 | testEnvironment: 'node', 14 | verbose: true, 15 | // roots: ['/src/', '/src/test'], 16 | // setupFiles: [], 17 | // setupFilesAfterEnv: ['/src/test/setupTests.js'], 18 | // moduleNameMapper: { 19 | // '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 20 | // '/src/test/__mocks__/fileMock.js', 21 | // '\\.(css|less|sass|scss)$': 'identity-obj-proxy', 22 | // }, 23 | coverageDirectory: './coverage/', 24 | collectCoverage: true, 25 | }; 26 | -------------------------------------------------------------------------------- /server/out/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | var express = require("express"); 4 | var PrettyError = require("pretty-error"); 5 | var expressServer_1 = require("./lib/expressServer"); 6 | // #region constants 7 | var dev = process.env.NODE_ENV !== 'production'; 8 | var pe = new PrettyError(); 9 | // #endregion 10 | (function () { 11 | try { 12 | pe.start(); 13 | var app = express(); 14 | (0, expressServer_1["default"])(app, dev); 15 | } 16 | catch (error) { 17 | console.log('server error: ', error); 18 | } 19 | })(); 20 | -------------------------------------------------------------------------------- /server/out/lib/expressServer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | var express = require("express"); 4 | var path = require("path"); 5 | var chalk = require("chalk"); 6 | var errors_1 = require("../middleware/errors"); 7 | // #region constants 8 | var DOCS_PATH = '../../../docs/'; 9 | var port = process.env.PORT || 8082; 10 | var host = process.env.SERVER_HOST || 'localhost'; 11 | // #endregion 12 | var expressServer = function (app, isDev) { 13 | if (isDev === void 0) { isDev = false; } 14 | if (!app) { 15 | console.log('Server application instance is undefined'); 16 | throw new Error('Server application instance is undefined'); 17 | } 18 | app.set('port', port); 19 | app.set('ipAdress', host); 20 | app.use('/assets', express.static(path.join(__dirname, DOCS_PATH, 'assets/'))); 21 | app.get('/*', function (req, res) { 22 | return res.sendFile(path.join(__dirname, DOCS_PATH, 'index.html')); 23 | }); 24 | app.use(errors_1.error404); 25 | app.use(errors_1.error500); 26 | /* eslint-disable no-console */ 27 | // @ts-ignore 28 | app.listen(port, host, function () { 29 | return console.log("\n =====================================================\n -> Server (".concat(chalk.bgBlue('SPA'), ") \uD83C\uDFC3 (running) on ").concat(chalk.green(host), ":").concat(chalk.green("".concat(port)), "\n =====================================================\n ")); 30 | }); 31 | /* eslint-enable no-console */ 32 | return app; 33 | }; 34 | exports["default"] = expressServer; 35 | -------------------------------------------------------------------------------- /server/out/middleware/errors.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | exports.error500 = exports.error404 = void 0; 4 | var error404 = function (req, res, next) { 5 | console.log('req.url: ', req.url); 6 | var err = new Error('Not found'); 7 | err.status = 404; 8 | next(err); 9 | }; 10 | exports.error404 = error404; 11 | var error500 = function (err, req, res, next) { 12 | if (err.status === 404) { 13 | res.status(404).send('Sorry nothing here for now...'); 14 | } 15 | console.error(err); 16 | res.status(500).send('internal server error'); 17 | }; 18 | exports.error500 = error500; 19 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-bootstrap-webpack-starter-server", 3 | "version": "2.0.0", 4 | "author": "Erwan DATIN (MacKentoch)", 5 | "license": "MIT", 6 | "description": "server side of react js + redux + react router + hot reload + devTools + bootstrap + webpack starter", 7 | "main": "out/index.js", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/MacKentoch/react-redux-bootstrap-webpack-starter.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/MacKentoch/react-redux-bootstrap-webpack-starter/issues" 14 | }, 15 | "engines": { 16 | "node": ">=16", 17 | "npm": ">=8.0.0" 18 | }, 19 | "directories": { 20 | "test": "test" 21 | }, 22 | "scripts": { 23 | "test": "cross-env NODE_ENV=test jest", 24 | "build-server": "tsc src/index.ts --outDir out", 25 | "prestart-server": "cd ../front && npm run prod && cd ../server", 26 | "start-server": "npm run build-server && ts-node src/index.ts" 27 | }, 28 | "keywords": [ 29 | "node", 30 | "TS", 31 | "express", 32 | "react", 33 | "react 16", 34 | "redux", 35 | "react-redux", 36 | "ES6", 37 | "ES7", 38 | "ES2015", 39 | "ES2016", 40 | "ES2017", 41 | "ES2018", 42 | "ES2019", 43 | "ES2020", 44 | "ES2021", 45 | "esnext", 46 | "typescript", 47 | "bootstrap", 48 | "react-router4", 49 | "react-router", 50 | "starter", 51 | "webpack", 52 | "hot-reload", 53 | "redux-devtools-extension", 54 | "devtools", 55 | "webpack4" 56 | ], 57 | "dependencies": { 58 | "body-parser": "^1.19.2", 59 | "chalk": "^4.1.2", 60 | "compression": "^1.7.4", 61 | "convict": "^6.2.4", 62 | "date-fns": "^2.28.0", 63 | "express": "^4.17.3", 64 | "express-promise-router": "^4.1.1", 65 | "express-rate-limit": "^6.2.1", 66 | "pretty-error": "^4.0.0", 67 | "serialize-javascript": "^6.0.0", 68 | "serve-favicon": "^2.5.0" 69 | }, 70 | "devDependencies": { 71 | "@types/body-parser": "^1.19.2", 72 | "@types/convict": "^6.1.1", 73 | "@types/express": "^4.17.13", 74 | "@types/express-promise-router": "^3.0.0", 75 | "@types/express-rate-limit": "^6.0.0", 76 | "@types/fetch-mock": "^7.3.5", 77 | "@types/jest": "^27.4.0", 78 | "@types/node": "^17.0.18", 79 | "fetch-mock": "^9.11.0", 80 | "jest": "^27.5.1", 81 | "jest-localstorage-mock": "^2.4.19", 82 | "node-notifier": "^10.0.1", 83 | "prettier": "^2.5.1", 84 | "rimraf": "^3.0.2", 85 | "ts-jest": "^27.1.3", 86 | "ts-node": "^10.5.0", 87 | "tslib": "^2.3.1", 88 | "tslint-config-prettier": "^1.18.0", 89 | "typescript": "^4.5.5" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /server/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as PrettyError from 'pretty-error'; 3 | import expressServer from './lib/expressServer'; 4 | 5 | // #region constants 6 | const dev = process.env.NODE_ENV !== 'production'; 7 | const pe = new PrettyError(); 8 | // #endregion 9 | 10 | (() => { 11 | try { 12 | pe.start(); 13 | const app = express(); 14 | expressServer(app, dev); 15 | } catch (error) { 16 | console.log('server error: ', error); 17 | } 18 | })(); 19 | -------------------------------------------------------------------------------- /server/src/lib/asyncWrap.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | 3 | // #region constants 4 | const shouldLogErrors = process.env.DEBUG || false; 5 | // #endregion 6 | 7 | /** 8 | * Returns a route handler for Express that calls the passed in function 9 | */ 10 | export default function(fn: Function) { 11 | if (fn.length <= 3) { 12 | return function( 13 | req: express.Request, 14 | res: express.Response, 15 | next: express.NextFunction, 16 | ) { 17 | return fn(req, res, next).catch((error: any) => { 18 | if (shouldLogErrors) { 19 | console.log('middleware error: ', error); 20 | } 21 | next(); 22 | }); 23 | }; 24 | } else { 25 | return function( 26 | err: express.Errback, 27 | req: express.Request, 28 | res: express.Response, 29 | next: express.NextFunction, 30 | ) { 31 | return fn(err, req, res, next).catch((error: any) => { 32 | if (shouldLogErrors) { 33 | console.log('middleware error: ', error); 34 | } 35 | next(); 36 | }); 37 | }; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /server/src/lib/expressServer.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as path from 'path'; 3 | import * as chalk from 'chalk'; 4 | import { error404, error500 } from '../middleware/errors'; 5 | 6 | // #region constants 7 | const DOCS_PATH = '../../../docs/'; 8 | const port = process.env.PORT || 8082; 9 | const host = process.env.SERVER_HOST || 'localhost'; 10 | // #endregion 11 | 12 | const expressServer = (app: express.Application, isDev = false) => { 13 | if (!app) { 14 | console.log('Server application instance is undefined'); 15 | throw new Error('Server application instance is undefined'); 16 | } 17 | 18 | app.set('port', port); 19 | app.set('ipAdress', host); 20 | 21 | app.use( 22 | '/assets', 23 | express.static(path.join(__dirname, DOCS_PATH, 'assets/')), 24 | ); 25 | 26 | app.get('/*', (req, res) => 27 | res.sendFile(path.join(__dirname, DOCS_PATH, 'index.html')), 28 | ); 29 | 30 | app.use(error404); 31 | app.use(error500); 32 | 33 | /* eslint-disable no-console */ 34 | // @ts-ignore 35 | app.listen(port, host, () => 36 | console.log(` 37 | ===================================================== 38 | -> Server (${chalk.bgBlue('SPA')}) 🏃 (running) on ${chalk.green( 39 | host, 40 | )}:${chalk.green(`${port}`)} 41 | ===================================================== 42 | `), 43 | ); 44 | /* eslint-enable no-console */ 45 | 46 | return app; 47 | }; 48 | 49 | export default expressServer; 50 | -------------------------------------------------------------------------------- /server/src/middleware/errors.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | 3 | type ErrorWithStatus = { 4 | status?: number, 5 | message?: string, 6 | }; 7 | 8 | export const error404 = ( 9 | req: express.Request, 10 | res: express.Response, 11 | next: express.NextFunction, 12 | ) => { 13 | console.log('req.url: ', req.url); 14 | 15 | const err: ErrorWithStatus = new Error('Not found'); 16 | err.status = 404; 17 | next(err); 18 | }; 19 | 20 | export const error500 = ( 21 | err: express.Errback & ErrorWithStatus, 22 | req: express.Request, 23 | res: express.Response, 24 | next: express.NextFunction, 25 | ) => { 26 | if (err.status === 404) { 27 | res.status(404).send('Sorry nothing here for now...'); 28 | } 29 | 30 | console.error(err); 31 | res.status(500).send('internal server error'); 32 | }; 33 | -------------------------------------------------------------------------------- /server/test/setupTests.ts: -------------------------------------------------------------------------------- 1 | require('jest-localstorage-mock'); 2 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "noImplicitReturns": true, 5 | "noUnusedLocals": true, 6 | "sourceMap": true, 7 | "strict": true, 8 | "target": "esnext", 9 | "resolveJsonModule": true, 10 | "moduleResolution": "node", 11 | "baseUrl": ".", 12 | "outDir": "out" 13 | }, 14 | "include": ["./src/**/*", "./test/**/*"] 15 | } 16 | --------------------------------------------------------------------------------