├── .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 |
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 |
58 |
59 | Filter:
60 |
61 |
62 |
63 |
64 |
65 |
66 | 1
67 | 2
68 | 3
69 | 4
70 | 5
71 | 6
72 | 7
73 | 8 | 3x
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 |
58 |
59 | Filter:
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | File |
70 | |
71 | Statements |
72 | |
73 | Branches |
74 | |
75 | Functions |
76 | |
77 | Lines |
78 | |
79 |
80 |
81 |
82 | WithRightMargin.tsx |
83 |
84 |
85 | |
86 | 100% |
87 | 3/3 |
88 | 100% |
89 | 2/2 |
90 | 100% |
91 | 0/0 |
92 | 100% |
93 | 3/3 |
94 |
95 |
96 |
97 |
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 |
58 |
59 | Filter:
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | File |
70 | |
71 | Statements |
72 | |
73 | Branches |
74 | |
75 | Functions |
76 | |
77 | Lines |
78 | |
79 |
80 |
81 |
82 | BackToTop.tsx |
83 |
84 |
85 | |
86 | 65.11% |
87 | 28/43 |
88 | 16.66% |
89 | 4/24 |
90 | 57.14% |
91 | 4/7 |
92 | 55.88% |
93 | 19/34 |
94 |
95 |
96 |
97 |
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 |
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 |
58 |
59 | Filter:
60 |
61 |
62 |
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 |
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 |
58 |
59 | Filter:
60 |
61 |
62 |
63 |
64 |
65 |
66 | 1
67 | 2
68 | 3
69 | 4 | 5x
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 |
58 |
59 | Filter:
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | File |
70 | |
71 | Statements |
72 | |
73 | Branches |
74 | |
75 | Functions |
76 | |
77 | Lines |
78 | |
79 |
80 |
81 |
82 | FadeInDiv.tsx |
83 |
84 |
85 | |
86 | 100% |
87 | 7/7 |
88 | 100% |
89 | 8/8 |
90 | 100% |
91 | 1/1 |
92 | 100% |
93 | 6/6 |
94 |
95 |
96 |
97 |
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 |
58 |
59 | Filter:
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | File |
70 | |
71 | Statements |
72 | |
73 | Branches |
74 | |
75 | Functions |
76 | |
77 | Lines |
78 | |
79 |
80 |
81 |
82 | index.ts |
83 |
84 |
85 | |
86 | 77.77% |
87 | 7/9 |
88 | 0% |
89 | 0/3 |
90 | 100% |
91 | 3/3 |
92 | 77.77% |
93 | 7/9 |
94 |
95 |
96 |
97 |
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 |
58 |
59 | Filter:
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | File |
70 | |
71 | Statements |
72 | |
73 | Branches |
74 | |
75 | Functions |
76 | |
77 | Lines |
78 | |
79 |
80 |
81 |
82 | ScrollToTop.tsx |
83 |
84 |
85 | |
86 | 100% |
87 | 9/9 |
88 | 100% |
89 | 0/0 |
90 | 100% |
91 | 1/1 |
92 | 100% |
93 | 9/9 |
94 |
95 |
96 |
97 |
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 |
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 |
58 |
59 | Filter:
60 |
61 |
62 |
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 | 7x
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 |
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 |
58 |
59 | Filter:
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | File |
70 | |
71 | Statements |
72 | |
73 | Branches |
74 | |
75 | Functions |
76 | |
77 | Lines |
78 | |
79 |
80 |
81 |
82 | appConfig.ts |
83 |
84 |
85 | |
86 | 100% |
87 | 2/2 |
88 | 100% |
89 | 0/0 |
90 | 100% |
91 | 0/0 |
92 | 100% |
93 | 2/2 |
94 |
95 |
96 |
97 | navigation.ts |
98 |
99 |
100 | |
101 | 100% |
102 | 2/2 |
103 | 100% |
104 | 0/0 |
105 | 100% |
106 | 0/0 |
107 | 100% |
108 | 2/2 |
109 |
110 |
111 |
112 |
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 |
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 |
58 |
59 | Filter:
60 |
61 |
62 |
63 |
64 |
65 |
66 | 1
67 | 2
68 | 3
69 | 4 | 1x
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 |
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 |
58 |
59 | Filter:
60 |
61 |
62 |
63 |
64 |
65 |
66 | 1
67 | 2
68 | 3
69 | 4
70 | 5
71 | 6 | 1x
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 |
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 |
58 |
59 | Filter:
60 |
61 |
62 |
63 |
64 |
65 |
66 | 1
67 | 2
68 | 3
69 | 4
70 | 5
71 | 6
72 | 7
73 | 8
74 | 9 | 1x
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 |
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 |
58 |
59 | Filter:
60 |
61 |
62 |
63 |
64 |
65 |
66 | 1
67 | 2
68 | 3
69 | 4
70 | 5
71 | 6
72 | 7
73 | 8
74 | 9 | 1x
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 |
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 |
58 |
59 | Filter:
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | File |
70 | |
71 | Statements |
72 | |
73 | Branches |
74 | |
75 | Functions |
76 | |
77 | Lines |
78 | |
79 |
80 |
81 |
82 | fetchMiddleware.ts |
83 |
84 |
85 | |
86 | 36.84% |
87 | 14/38 |
88 | 37.5% |
89 | 3/8 |
90 | 100% |
91 | 5/5 |
92 | 36.11% |
93 | 13/36 |
94 |
95 |
96 |
97 |
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 |
58 |
59 | Filter:
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | File |
70 | |
71 | Statements |
72 | |
73 | Branches |
74 | |
75 | Functions |
76 | |
77 | Lines |
78 | |
79 |
80 |
81 |
82 | index.ts |
83 |
84 |
85 | |
86 | 30.55% |
87 | 11/36 |
88 | 0% |
89 | 0/12 |
90 | 16.66% |
91 | 1/6 |
92 | 32.35% |
93 | 11/34 |
94 |
95 |
96 |
97 |
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 |
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 |
58 |
59 | Filter:
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | File |
70 | |
71 | Statements |
72 | |
73 | Branches |
74 | |
75 | Functions |
76 | |
77 | Lines |
78 | |
79 |
80 |
81 |
82 | fetchTools.ts |
83 |
84 |
85 | |
86 | 60% |
87 | 9/15 |
88 | 0% |
89 | 0/4 |
90 | 0% |
91 | 0/2 |
92 | 58.33% |
93 | 7/12 |
94 |
95 |
96 |
97 |
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 |
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 |
58 |
59 | Filter:
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | File |
70 | |
71 | Statements |
72 | |
73 | Branches |
74 | |
75 | Functions |
76 | |
77 | Lines |
78 | |
79 |
80 |
81 |
82 | index.ts |
83 |
84 |
85 | |
86 | 12.37% |
87 | 12/97 |
88 | 1.33% |
89 | 1/75 |
90 | 10% |
91 | 1/10 |
92 | 12.37% |
93 | 12/97 |
94 |
95 |
96 |
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 |
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 |
58 |
59 | Filter:
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | File |
70 | |
71 | Statements |
72 | |
73 | Branches |
74 | |
75 | Functions |
76 | |
77 | Lines |
78 | |
79 |
80 |
81 |
82 | registerServiceWorker.ts |
83 |
84 |
85 | |
86 | 25% |
87 | 3/12 |
88 | 0% |
89 | 0/2 |
90 | 0% |
91 | 0/2 |
92 | 30% |
93 | 3/10 |
94 |
95 |
96 |
97 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------