├── .dockerignore ├── .eslintrc ├── .gitignore ├── .gitlab-ci.yml ├── .travis.yml ├── CHANGES.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── nginx └── conf.d │ └── default.conf ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── App.js ├── __test__ │ ├── App.test.js │ └── __snapshots__ │ │ └── App.test.js.snap ├── components │ ├── AppBar │ │ ├── AppBar.js │ │ ├── IconButton.js │ │ ├── Title.js │ │ ├── Toolbar.js │ │ └── __tests__ │ │ │ ├── AppBar.test.js │ │ │ ├── IconButton.test.js │ │ │ ├── Title.test.js │ │ │ ├── Toolbar.test.js │ │ │ └── __snapshots__ │ │ │ ├── AppBar.test.js.snap │ │ │ ├── IconButton.test.js.snap │ │ │ ├── Title.test.js.snap │ │ │ └── Toolbar.test.js.snap │ ├── Home │ │ ├── Home.js │ │ ├── __tests__ │ │ │ ├── Home.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Home.test.js.snap │ │ └── logo.svg │ ├── Layout │ │ ├── Skeleton.js │ │ └── __tests__ │ │ │ ├── Skeleton.test.js │ │ │ └── __snapshots__ │ │ │ └── Skeleton.test.js.snap │ ├── NoWhere │ │ ├── HomeButton.js │ │ ├── NoWhere.js │ │ └── __test__ │ │ │ ├── HomeButton.test.js │ │ │ ├── NoWhere.test.js │ │ │ └── __snapshots__ │ │ │ ├── HomeButton.test.js.snap │ │ │ └── NoWhere.test.js.snap │ ├── Sidebar │ │ ├── Header.js │ │ ├── HomeItem.js │ │ ├── Sidebar.js │ │ └── __test__ │ │ │ ├── Header.test.js │ │ │ ├── HomeItem.test.js │ │ │ ├── Sidebar.test.js │ │ │ └── __snapshots__ │ │ │ ├── Header.test.js.snap │ │ │ ├── HomeItem.test.js.snap │ │ │ └── Sidebar.test.js.snap │ └── UI │ │ └── ListItem │ │ ├── 1.js │ │ └── __test__ │ │ ├── 1.test.js │ │ └── __snapshots__ │ │ └── 1.test.js.snap ├── constants │ ├── __test__ │ │ ├── __snapshots__ │ │ │ └── theme.test.js.snap │ │ └── theme.test.js │ ├── routes.js │ └── theme.js ├── containers │ ├── AppBar │ │ ├── AppBar.js │ │ ├── IconButton.js │ │ └── Title.js │ ├── Layout │ │ ├── Content.js │ │ ├── Layout.js │ │ └── __test__ │ │ │ ├── Content.test.js │ │ │ ├── Layout.test.js │ │ │ └── __snapshots__ │ │ │ ├── Content.test.js.snap │ │ │ └── Layout.test.js.snap │ ├── NoWhere │ │ └── HomeButton.js │ └── Sidebar │ │ ├── Header.js │ │ ├── HomeItem.js │ │ └── Sidebar.js ├── index.js ├── redux │ ├── actions │ │ ├── __tests__ │ │ │ └── ui.test.js │ │ └── ui.js │ ├── enhancers │ │ ├── index.js │ │ ├── middlewares │ │ │ ├── index.js │ │ │ └── router.js │ │ └── reduxDevtools.js │ ├── reducers │ │ ├── data │ │ │ └── index.js │ │ ├── index.js │ │ ├── status │ │ │ └── index.js │ │ └── ui │ │ │ ├── __test__ │ │ │ ├── __snapshots__ │ │ │ │ └── sidebar.test.js.snap │ │ │ └── sidebar.test.js │ │ │ ├── index.js │ │ │ └── sidebar.js │ ├── selectors │ │ ├── data.js │ │ ├── status.js │ │ └── ui.js │ └── store.js ├── setupTests.js ├── tests-utils │ ├── __test__ │ │ └── createShallow.test.js │ └── createShallow.js └── utils │ └── registerServiceWorker.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | # Source code 2 | .git 3 | .gitignore 4 | .dockerignore 5 | Dockerfile* 6 | docker-compose* 7 | .gitlab-ci.yml 8 | STARTING.nd 9 | 10 | # Dependency directory 11 | node_modules 12 | 13 | # Logs 14 | logs 15 | *.log 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dependencies 58 | node_modules 59 | 60 | # testing 61 | coverage 62 | 63 | # production 64 | build 65 | 66 | # misc 67 | .DS_Store 68 | .env.local 69 | .env.development.local 70 | .env.test.local 71 | .env.production.local 72 | 73 | # IDEs 74 | .idea 75 | .vscode 76 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | NAME: web 3 | 4 | stages: 5 | - test 6 | - build 7 | - deploy 8 | 9 | services: 10 | - docker:dind 11 | 12 | test: 13 | stage: test 14 | image: node:10-alpine 15 | script: 16 | - yarn install 17 | - CI=true yarn test -- --coverage 18 | coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/ 19 | only: 20 | refs: 21 | - branches 22 | - tags 23 | 24 | build-on-branch: 25 | image: docker:stable 26 | stage: build 27 | before_script: 28 | - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY 29 | script: 30 | # Build image following http://label-schema.org/rc1/ label convention convention 31 | - docker build --label org.label-schema.schema-version="1.0.0-rc1" --label org.label-schema.build-date=`date -u +"%Y-%m-%dT%H:%M:%SZ"` --label org.label-schema.name="${NAME}" --label org.label-schema.version="dirty" --label org.label-schema.vcs-url="${CI_PROJECT_URL}" --label org.label-schema.vcs-ref="${CI_COMMIT_SHA:0:8}" --label org.label-schema.vendor="ConsenSys France" -t ${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_NAME}:${CI_COMMIT_SHA:0:8} -t ${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_NAME}:latest . 32 | # Push image 33 | - docker push ${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_NAME}:${CI_COMMIT_SHA:0:8} 34 | - docker push ${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_NAME}:latest 35 | after_script: 36 | - docker logout 37 | only: 38 | - /^feature/[a-zA-Z0-9/-]*$/ 39 | - /^fix/[a-zA-Z0-9/-]*$/ 40 | 41 | build-on-master: 42 | image: docker:stable 43 | stage: build 44 | before_script: 45 | - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY 46 | script: 47 | # Build image following http://label-schema.org/rc1/ label convention convention 48 | - docker build --label org.label-schema.schema-version="1.0.0-rc1" --label org.label-schema.build-date=`date -u +"%Y-%m-%dT%H:%M:%SZ"` --label org.label-schema.name="${NAME}" --label org.label-schema.version="dirty" --label org.label-schema.vcs-url="${CI_PROJECT_URL}" --label org.label-schema.vcs-ref="${CI_COMMIT_SHA:0:8}" --label org.label-schema.vendor="ConsenSys France" -t ${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_NAME}:${CI_COMMIT_SHA:0:8} -t ${CI_REGISTRY_IMAGE}/master:latest -t ${CI_REGISTRY_IMAGE}:latest . 49 | # Push image 50 | - docker push ${CI_REGISTRY_IMAGE}/master:${CI_COMMIT_SHA:0:8} 51 | - docker push ${CI_REGISTRY_IMAGE}/master:latest 52 | - docker push ${CI_REGISTRY_IMAGE}:latest 53 | after_script: 54 | - docker logout 55 | only: 56 | - master 57 | 58 | build-on-tag: 59 | image: docker:stable 60 | stage: build 61 | before_script: 62 | - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY 63 | script: 64 | # Build image following http://label-schema.org/rc1/ label convention convention 65 | - docker build --label org.label-schema.schema-version="1.0.0-rc1" --label org.label-schema.build-date=`date -u +"%Y-%m-%dT%H:%M:%SZ"` --label org.label-schema.name="${NAME}" --label org.label-schema.version="${CI_COMMIT_TAG}" --label org.label-schema.vcs-url="${CI_PROJECT_URL}" --label org.label-schema.vcs-ref="${CI_COMMIT_SHA:0:8}" --label org.label-schema.vendor="ConsenSys France" -t ${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG} . 66 | # Push image 67 | - docker push ${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG} 68 | after_script: 69 | - docker logout 70 | only: 71 | refs: 72 | - tags 73 | 74 | deploy-per-branch: 75 | stage: deploy 76 | script: 77 | - |- 78 | curl -X POST -f token=${TRIGGER_TOKEN} -f ref=${TRIGGER_REMOTE_REF} \ 79 | -f "variables[TRIGGER_REGISTRY]=${CI_REGISTRY}" \ 80 | -f "variables[TRIGGER_IMAGE_REPOSITORY]=${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_NAME}" \ 81 | -f "variables[TRIGGER_IMAGE_TAG]=${CI_COMMIT_SHA:0:8}" \ 82 | -f "variables[TRIGGER_REF_NAME]=${CI_COMMIT_REF_NAME}" \ 83 | -f "variables[TRIGGER_REF_SLUG]=${CI_COMMIT_REF_SLUG}" \ 84 | https://gitlab.com/api/v4/projects/${TRIGGER_PROJECT}/trigger/pipeline 85 | only: 86 | - /^feature/[a-zA-Z0-9/-]*$/ 87 | - /^fix/[a-zA-Z0-9/-]*$/ 88 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - test 3 | 4 | sudo: false 5 | os: linux 6 | 7 | language: node_js 8 | node_js: 9 | - 10 10 | 11 | cache: 12 | yarn: true 13 | directories: 14 | - node_modules 15 | 16 | before_install: 17 | - curl -o- -L https://yarnpkg.com/install.sh | bash 18 | - export PATH="$HOME/.yarn/bin:$PATH" 19 | 20 | install: yarn install 21 | 22 | jobs: 23 | include: 24 | - stage: test 25 | script: yarn test -- --coverage 26 | after_script: bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Versions 2 | 3 | ## 0.2.0 4 | ###### *Unreleased* 5 | 6 | ## 0.1.1 7 | ###### *October 6th 2018* 8 | 9 | ### Chore 10 | 11 | - [package.json] Update dependencies 12 | - [Dockerfile] Update node version to 10 13 | - [.gitlab-ci.yml] CI/CD script 14 | 15 | ## 0.1.0 16 | ###### *October 1st 2018* 17 | 18 | ### Features 19 | 20 | #### Components/Containers 21 | 22 | - [ListItem1] Implement *ListItem1* component 23 | - [AppBar] Implement *AppBar* component 24 | - [Sidebar] Implement *Sidebar* component 25 | - [Layout] Implement *Layout* component 26 | - [NoWhere] Implement *NoWhere* component 27 | - [Home] Implement *Home* component 28 | 29 | #### Redux 30 | 31 | ##### Reducer 32 | 33 | - [sidebar] Implement UI reducer for sidebar 34 | 35 | ##### Enhancer 36 | 37 | - [DevTools] Include redux DevTools enhancer 38 | 39 | #### Utils 40 | 41 | - [registerServiceWorker] Implement service worker for progressive Web App 42 | 43 | #### Test-Utils 44 | 45 | - [createShallow] Implement createShallow dealing diving through *withWidth()* 46 | 47 | #### Constants 48 | 49 | - [routes] Define routes 50 | - [theme] Define Material-Ui theme 51 | 52 | ### Chore 53 | 54 | - [Dockerfile] Add production docker configuration with multi-stage build 55 | - [nginx] Production docker image use nginx server 56 | - [docker-compose.yml] Implement dev environment script 57 | - [.travis.yml] CI/CD script 58 | - [.eslintrc] Linting configuration file 59 | 60 | ### Doc 61 | 62 | - [CONTRIBUTING.md] Add contributing guidelines 63 | - [CHANGES.md] Add changelog 64 | - [README.md] Add readme with badges 65 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | ## Table of Contents 4 | 5 | - [Project Structure](#project-structure) 6 | - [Contributing](#contributing) 7 | - [Requirements](#crequirements) 8 | - [Set-up dev environement](#set-up-dev-environement) 9 | - [Start developing](#start-developing) 10 | - [Install new dependencies](#install-new-dependencies) 11 | - [Release a new version](#release-a-new-version) 12 | - [Tests](#test) 13 | - [Commands](#commands) 14 | - [Practices](#practices) 15 | - [Test Utilities](#test-utilities) 16 | - [Code linting](#code-linting) 17 | - [Files organization & file naming convention](#files-organization-file-naming-convention) 18 | - [Redux related files go into *src/redux*](#redux-related-files-go-into-srcredux) 19 | - [Components go into *src/components*](#components-go-into-srccomponents) 20 | - [Containers go into *src/containers*](#containers-go-into-srccontainers) 21 | - [Component & Container separation pattern](#component-container-separation-pattern) 22 | - [Motivation for the pattern](#motivation-for-the-pattern) 23 | - [Components are concerned with how things look](#components-or-presentational-components-are-concerned-with-how-things-look) 24 | - [Containers are concerned with how things work](#containers-are-concerned-with-how-things-work) 25 | - [Others](#others) 26 | - [Material-UI](#material-ui) 27 | - [Audit Application](#audit-application) 28 | - [Conventions](#conventions) 29 | 30 | ## Project Structure 31 | 32 | ``` 33 | . 34 | ├── nginx/ # Nginx server configuration for production 35 | ├── public/ # Static files 36 | ├── src/ # Single page application source script 37 | ├── .dockerignore # .dockerignore 38 | ├── .eslintrc # Eslint configuration 39 | ├── .gitignore # .gitignore 40 | ├── .travis.yml # CI/CD script 41 | ├── CONTRIBUTING.md # Contributing guidelines 42 | ├── docker-compose.yml # Docker compose script to set dev environment 43 | ├── Dockerfile # Docker file 44 | ├── package.json # package.json 45 | ├── README.md # README 46 | └── yarn.lock # yarn.lock 47 | ``` 48 | 49 | ## Contributing 50 | 51 | ### Requirements 52 | 53 | - docker>=17.0.0 54 | - docker-compose>=1.17.0 55 | - node>=9.0.0 56 | - yarn>=1.6.0 57 | 58 | ### Set-up dev environement 59 | 60 | #. If not yet done, clone project locally 61 | 62 | ```bash 63 | git clone && cd 64 | ``` 65 | 66 | #. If not yet done, install node dependencies 67 | 68 | ```bash 69 | yarn install # install node dependencies locally 70 | ``` 71 | 72 | #. Start application 73 | 74 | ```bash 75 | docker-compose up # start application in dev mode 76 | ``` 77 | 78 | You can now access application in dev mode at http://localhost 79 | 80 | ### Start developing 81 | 82 | 1. Create a new branch 83 | 84 | Requirements 85 | 86 | - New branch **MUST** be started from *master* (which is our dev branch) 87 | - Feature branch **MUST** be named ``feature/[a-zA-Z0-9\-]+`` 88 | - Bug fix branch **MUST** be named ``fix/[a-zA-Z0-9\-]+`` 89 | - Branch refering to an open issue **MUST** be named ``(fix|feature)/`` 90 | 91 | 2. Develop on new branch being careful to write test for every change you proceed to 92 | 93 | 3. Push branch 94 | 95 | ```bash 96 | git push origin -u 97 | ``` 98 | 99 | Pushing will trigger a CI pipeline (see section CI/CD) 100 | 101 | 4. Create a pull request 102 | 103 | ### Install new dependencies 104 | 105 | ```bash 106 | yarn add # install package, and update package.json & yarn.lock 107 | ``` 108 | 109 | Command above can sometime error with ``EACCES`` code, this means that *docker* wrote some files (usually cache) in your local ``node_modules`` folder. 110 | To solve it you can change right access of the folder by running command 111 | 112 | ```bash 113 | sudo chown -R $USER:$USER node_modules 114 | ``` 115 | 116 | ### Release a new version 117 | 118 | 1. Create a release branch ``release/x.x.x`` 119 | 120 | 2. Bump to version ``x.x.x`` 121 | - ``package.json``: change version to ``x.x.x`` 122 | - ``CHANGES.md``: ensure release section ``x.x.x`` documentation is exhaustive and set release date 123 | 124 | 3. Commit with message ``bump version to x.x.x`` 125 | 126 | ```bash 127 | git commit -am "bump version to x.x.x" 128 | ``` 129 | 130 | 3. Tag version 131 | 132 | ```bash 133 | git tag -a x.x.x -m "Version x.x.x" 134 | ``` 135 | 136 | 4. Bump to version ``increment(x.x.x)-dev`` 137 | - ``package.json``: change version to ``increment(x.x.x)-dev`` 138 | - ``CHANGES.md``: create empty ``increment(x.x.x)-dev`` section with unreleased status 139 | 140 | 5. Push branch and tags 141 | 142 | ```bash 143 | git push origin -u release/x.x.x --follow-tags 144 | ``` 145 | 146 | 6. Proceed to merge request ``release/x.x.x`` -> ``master`` 147 | 148 | ## Tests 149 | 150 | ### Commands 151 | 152 | Run test 153 | 154 | ```bash 155 | yarn test 156 | ``` 157 | 158 | Run coverage 159 | 160 | ```bash 161 | yarn test -- --coverage 162 | ``` 163 | 164 | ### Practices 165 | 166 | **Test framework** 167 | 168 | We use [Jest](https://facebook.github.io/jest/docs/en/getting-started.html) test framework (directly packed into [create-react-app](https://github.com/facebookincubator/create-react-app) 169 | 170 | We use [Enzyme Shallow rendering](http://airbnb.io/enzyme/) to test component 171 | 172 | **Requirements** 173 | 174 | - Tests **MUST** be written in a ```**/__tests__``` folder located in the same directory as the element tested 175 | - Tests file for ```.js``` **MUST** be named ```.test.js``` 176 | - **SHOULD** use [Jest snapshot testing](https://facebook.github.io/jest/docs/en/snapshot-testing.html) 177 | 178 | **Folder structure** 179 | 180 | ``` 181 | src/ 182 | : 183 | ├── path/to/folder/ 184 | │ ├── __test__/ 185 | │ │ ├── file1.test.js 186 | │ │ └── file2.test.js 187 | │ ├── file1.js 188 | │ └── file2.js 189 | : 190 | ``` 191 | 192 | ### Test Utilities 193 | 194 | TODO: complete this section 195 | 196 | ## Code linting 197 | 198 | **Framework** 199 | 200 | We use ESLint (packed in [create-react-app](https://github.com/facebookincubator/create-react-app)) 201 | 202 | This project use a combination of *husky*, *lint-staged*, *prettier* to format code automatically including .js,.jsx, .json and .css (c.f [create-react-app documentation](https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#formatting-code-automatically). 203 | 204 | Some pre-commit hooks are set-up so whenever you make a commit, *prettier* will format the changed files automatically. 205 | 206 | **Requirements** 207 | 208 | - Code **MUST** respect linting rules defined in ```.eslintrc``` 209 | 210 | ## Files organization & file naming convention 211 | 212 | ### Redux related files go into *src/redux* 213 | 214 | We make active usage of *redux*, if not familiar with *redux* we recommend going through [*redux* documentation](https://redux.js.org/) before going through this section. 215 | 216 | **Requirements** 217 | 218 | - Actions **MUST** go into a into *src/redux/actions* 219 | - Reducers **MUST** go into a into *src/redux/reducers* 220 | - Enhancers **MUST** go into a into *src/redux/enhancers* 221 | 222 | **Folder structure** 223 | 224 | ``` 225 | src/ 226 | : 227 | ├── redux/ 228 | │ ├── actions/ # Actions 229 | │ ├── enhancers/ # Enhancers 230 | │ └── reducers/ # Reducers 231 | : 232 | ``` 233 | 234 | ##### Reducers 235 | 236 | We highly recommend you reading more about [structuring reducer](https://redux.js.org/recipes/structuringreducers/) 237 | 238 | **Requirements** 239 | 240 | - State structure (or state *shape*) **MUST** be defined in terms of *domain data* & *app status*, it **MUST NOT** be defined after your *UI* component tree 241 | - Root reducer (feeded to *createStore()*) **MUST** combine together specialized reducers: 242 | - *data* reducers, handling the data application needs to show, use, or modify (typically information retrieved from some APIs) 243 | - *status* reducers, handling information related to application's behavior (such as "there is a request in progress") 244 | - *ui* reducer, handling how the UI is currently displayed (such as "Sidebar is open") 245 | - Global state *shape* **MUST** reflect *src/redux/reducers/* folder structure. Keys in global state **MUST** be the same as file names *src/redux/reducers/* 246 | - Specialized reducer files **MUST** implement a *reducer* function exported as default 247 | 248 | **Folder structure** 249 | 250 | ``` 251 | src/ 252 | : 253 | ├── redux/ 254 | │ : 255 | │ ├── reducers/ 256 | │ │ ├── data/ 257 | │ │ │ ├── domain1.js # Reducer responsible to handle state slice related to domain 1 data 258 | │ │ │ └── domain2.js # Reducer responsible to handle state slice related to domain 2 data 259 | │ │ ├── status/ 260 | │ │ │ └── bahavior1.js # Reducer responsible to handle state slice related to application status behavior 1 261 | │ │ └── ui/ 262 | │ │ └── element1.js # Reducer responsible to handle state slice related to UI element 1 263 | : : 264 | ``` 265 | 266 | Corresponding state *shape* would be 267 | 268 | ```javascript 269 | { 270 | data : { 271 | domain1: {}, 272 | domain2: {} 273 | }, 274 | status : { 275 | category1: {} 276 | }, 277 | ui : { 278 | element1: {} 279 | } 280 | } 281 | ``` 282 | 283 | **Example** 284 | 285 | ```javascript 286 | // Import action of interest as constants 287 | import { OPEN_SIDEBAR, CLOSE_SIDEBAR } from "../actions/ui"; 288 | 289 | // Define initial state 290 | const initialState = { 291 | open: false 292 | }; 293 | 294 | // Implement "reducer" function with initial state as default state 295 | cexport default (state = initialState, { type }) => { 296 | switch (type) { 297 | case OPEN_SIDEBAR: 298 | return { 299 | ...state, 300 | open: true 301 | }; 302 | 303 | case CLOSE_SIDEBAR: 304 | return { 305 | ...state, 306 | open: false 307 | }; 308 | 309 | default: 310 | return state; 311 | } 312 | }; 313 | ``` 314 | 315 | ##### Actions 316 | 317 | *Actions* are payloads of information that send data from your application to your store. They are the only source of information for the store. You send them to the store using *store.dispatch()*. 318 | 319 | **Requirements** 320 | 321 | - Actions **MUST** be grouped by piece of state they cover (e.g. UI actions are defined into *src/redux/actions/ui.js*) 322 | - Each action type **MUST** be declared as a constant 323 | - Each action **SHOULD** come with an action creator function 324 | - Action types & creator functions **MUST** be implemented in the same file 325 | - Actions **MUST** be serializable (if you think in passing a function into an action for handling by reducers, it means that you actually need a redux middleware) 326 | - Actions **MUST** follow [Flux-Standard-Action] (FSA)(https://github.com/redux-utilities/flux-standard-action#actions) format 327 | - **MUST** be a plain JavaScript object 328 | - **MUST** have a ``type`` property 329 | - **MAY** have an ``error`` property 330 | - **MAY** have an ``payload`` property 331 | - **MAY** have an ``meta`` property 332 | - **MUST NOT** include property other than ``type``, ``error``, ``payload`` and ``meta`` 333 | - There **SHOULD NOT** be a 1-to-1 link between actions and reducers. Typically an action could be reduced by multiple reducers 334 | 335 | **Folder structure** 336 | 337 | ``` 338 | src/ 339 | : 340 | ├── redux/ 341 | │ ├── actions/ # Component file goes into a folder named after the component 342 | │ │ ├── ui.js # Action related to UI modification 343 | │ │ └── status.js # Actions related to status information 344 | : 345 | ``` 346 | 347 | **Example** 348 | 349 | ```javascript 350 | // Declare action type as a constant 351 | export const ADD_TODO = "ADD_TODO"; 352 | // Declare action creator 353 | export const addTodo = (text, category) => ({ 354 | // Respect FSA standard format (type, payload, meta properties) 355 | type: ADD_TODO, 356 | payload: { 357 | text 358 | }, 359 | meta: { 360 | category 361 | }, 362 | }); 363 | ``` 364 | 365 | ##### Enhancers and middlewares 366 | 367 | Store enhancers are higher-order function that composes a store creator to return a new, enhanced store creator. 368 | In our case, we mainly use enhancer to add *redux* middlewares allowing to hook custom behaviors when dispatching actions. 369 | 370 | We highly recommend you reading more about [middlewares in *redux*](https://redux.js.org/advanced/middleware) 371 | 372 | **Requirements** 373 | 374 | - Middlewares **MUST** go into *src/redux/enhancers/middlewares* 375 | 376 | **Folder structure** 377 | 378 | ``` 379 | src/ 380 | : 381 | ├── enhancers/ 382 | │ ├── middlewares/ # Middlewares 383 | │ │ ├── index.js # Combine middlewares into one enhancer 384 | │ │ └── router.js # Middleware managing router 385 | │ ├── reduxDevTools.js # Enhancer to include ReduxDevTool 386 | │ └── index.js # Combine enhancers into on root enhancer 387 | : 388 | ``` 389 | 390 | ### Components go into *src/components* 391 | 392 | **Requirements** 393 | 394 | - All components **MUST** go into *src/components* 395 | - Components **MUST** have a unique name 396 | 397 | #### Module specific components go into *src/components/* 398 | 399 | **Requirements** 400 | 401 | - Component files **MUST** go into a into a folder named after the component in *src/components/* 402 | - Component code **MUST** be split into as many atomic sub components as necessary 403 | - Component **MUST** follow naming pattern *path-based-component-naming*, which consists in naming the component accordingly to its relative path from *src/components/* 404 | 405 | Examples of those components are 406 | - Skeleton elements such as *AppBar*, *Sidebar* 407 | - View pannels such as *Home* 408 | - Layout elements such as *Layout* 409 | 410 | **Folder structure and file naming** 411 | 412 | ``` 413 | src/ 414 | : 415 | ├── components/ 416 | │ : 417 | │ ├── AppBar/ # Component file goes into a folder named after the component 418 | │ │ ├── __tests__/ # Component tests folder 419 | │ │ ├── AppBar.js # Main component file (named as the folder) 420 | │ │ ├── IconButton.js # Sub component file (we do not repeat AppBar in file's name) 421 | │ │ ├── Title.js # Sub component file (we do not repeat AppBar in file's name) 422 | │ │ └── Toolbar.js # Sub component file (we do not repeat AppBar in file's name) 423 | : : : 424 | ``` 425 | 426 | **Component naming** 427 | 428 | Main component 429 | 430 | ```javascript 431 | # src/components/AppBar/AppBar.js 432 | 433 | // Follow path-based-component-naming (not repeating) 434 | const AppBar = () => ( 435 | // Component code comes here 436 | ); 437 | ``` 438 | 439 | Sub component 440 | 441 | ```javascript 442 | # src/components/AppBar/IconButton.js 443 | 444 | // Follow path-based-component-naming 445 | const AppBarIconButton = () => ( 446 | // Component code comes here 447 | ); 448 | ``` 449 | 450 | #### Generic atomic reusable components go into *src/components/UI* 451 | 452 | **Requirements** 453 | 454 | - Generic UI components **MUST NOT** held business logic specific to the application (they actually could be stored on some external *npm* library) 455 | - **MUST** follow naming pattern *path-based-component-naming*, which consists in naming the component accordingly to its relative path from *src/components/UI* 456 | 457 | Examples of those components are: *Button*, *Input*, *Checkbox*, *Select*, *Modal*, etc… 458 | 459 | **Folder structure and file naming** 460 | 461 | ``` 462 | src/ 463 | : 464 | ├── components/ 465 | │ : 466 | │ ├── UI/ 467 | │ │ ├── ListItem/ 468 | │ │ │ ├── __test__/ 469 | │ │ │ ├── 1.js 470 | │ │ │ └── 2.js 471 | │ │ ├── Button/ 472 | │ │ │ ├── __test__/ 473 | │ │ │ └── 1.js 474 | : : : 475 | ``` 476 | 477 | ### Containers go into *src/containers* 478 | 479 | **Requirements** 480 | 481 | - Container **MUST** go into *src/containers* 482 | - Container **MUST** follow same relative path from *src/containers* as the component it wraps from *src/components* 483 | - Container **MUST** have same name as the component it wraps 484 | 485 | **Folder structure** 486 | 487 | ``` 488 | src/ 489 | : 490 | ├── containers/ 491 | │ : 492 | │ ├── AppBar/ 493 | │ │ ├── __tests__/ 494 | │ │ ├── AppBar.js 495 | │ │ ├── IconButton.js 496 | │ │ └── Title.js 497 | : : : 498 | ``` 499 | 500 | ## Component & Container separation pattern 501 | 502 | We respect a separation between presentational components & containers. 503 | 504 | ### Motivation for the pattern 505 | 506 | - Better separation of concerns makes app understandable and better UI writing components this way. 507 | - Better reusability. Same presentational component can be used with completely different state sources, and turn those into separate container components that can be further reused. 508 | - Presentational components are essentially app’s “palette”. It is possible to put them on a single page and let designers tweak all their variations without touching the app’s logic. It is possible to run screenshot regression tests on that page. 509 | - This forces to extract “layout components” such as Sidebar, AppBar, etc. and use *this.props.children* instead of duplicating the same markup and layout in several container components. 510 | 511 | You can read more about it [here](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) 512 | 513 | ### Components (or presentational components) are concerned with how things look 514 | 515 | **Requirements** 516 | 517 | - **SHOULD** implement some DOM markup and styles of their own 518 | - **MAY** contain both presentational components and containers 519 | - **SHOULD** allow containment via this.props.children. 520 | - **SHOULD NOT** have their own state (when they do, it **MUST** be UI state and **MUST NOT** be data) 521 | - **SHOULD** be written as functional components unless they need state, lifecycle hooks, or performance optimizations 522 | - **MUST** receive data and callbacks exclusively via props. 523 | - **MUST NOT** have dependencies with the rest of the app, such as redux actions or stores 524 | - **MUST NOT** specify how the data is loaded or mutated (API calls **MUST NOT** be defined in a component) 525 | - **MUST NOT** define any route 526 | 527 | #### Implement a component 528 | 529 | ##### Set imports 530 | 531 | **Requirements** 532 | 533 | - **MAY** import components from UI libraries typically *Material-UI* 534 | - **MAY** import components and containers from the rest of the application 535 | - **SHOULD NOT** import any resources related to *Redux*, except *compose()* that is sometime convenient to connect multiple marterial-ui wrappers (*withStyles()*, *withTheme()*...) 536 | - **SHOULD NOT** have any dependencies in the rest of the application, except components or containers 537 | - **MUST** be organized in 3 ordered sections: 1. low-level React imports / 2. *Material-UI* imports / 3. intra-application imports 538 | 539 | **Example** 540 | 541 | ```javascript 542 | // Section 1: React low level imports 543 | import React from "react"; 544 | import PropTypes from "prop-types"; 545 | import classNames from "classnames"; 546 | import { compose } from "redux"; 547 | 548 | // Section 2: Material-UI imports 549 | import MuiAppBar from "@material-ui/core/AppBar"; 550 | import { withStyles } from "@material-ui/core/styles"; 551 | 552 | // Section 3: Components & Containers import from the application 553 | import AppBarToolbar from "./Toolbar"; 554 | ``` 555 | 556 | ##### Define styles 557 | 558 | **Requirements** 559 | 560 | - **MUST** be a function taking theme as argument and returning an object 561 | 562 | **Example** 563 | 564 | ```javascript 565 | const styles = theme => ({ 566 | appBar: { 567 | position: "fixed", 568 | top: 0, 569 | zIndex: theme.zIndex.drawer + 1, 570 | transition: theme.transitions.create(["width", "margin"], { 571 | easing: theme.transitions.easing.sharp, 572 | duration: theme.transitions.duration.leavingScreen 573 | }) 574 | }, 575 | appBarShifted: { 576 | marginLeft: 240, 577 | width: `calc(100% - ${240}px)`, 578 | transition: theme.transitions.create(["width", "margin"], { 579 | easing: theme.transitions.easing.sharp, 580 | duration: theme.transitions.duration.enteringScreen 581 | }), 582 | [theme.breakpoints.down("sm")]: { 583 | width: "100%" 584 | } 585 | } 586 | }); 587 | ``` 588 | 589 | ##### Code component 590 | 591 | **Requirements** 592 | 593 | - **SHOULD** be a function taking a props object as an argument (except lifecycle, UI state or some optimization are required) 594 | - **MUST** respect compenent naming convention (see below) 595 | - **MUST** be atomic 596 | - **MUST** be agnostic of the rest of the application, this **MUST** include every variable namings. Think it as it should be able to exist on its own 597 | - **MUST** be documented with PropTypes 598 | 599 | **Example** 600 | 601 | ```javascript 602 | const AppBar = ({ 603 | classes, 604 | shifted, // for agnosticity we use variable name "shifted" over "sidebarOpen" 605 | }) => ( 606 | 613 | 614 | 615 | ); 616 | 617 | // Documentation with PropTypes 618 | AppBar.propTypes = { 619 | classes: PropTypes.object.isRequired, 620 | shifted: PropTypes.bool 621 | }; 622 | ``` 623 | 624 | ##### Connect styles and export 625 | 626 | **Requirements** 627 | 628 | - **MAY** inject styles information using *withStyle()*, *withTheme()*, *withWidth()* 629 | - There **MUST** be one unique export 630 | 631 | **Example** 632 | 633 | Basic: only *withStyle()* 634 | 635 | ```javascript 636 | export default withStyles(styles)(AppBar); 637 | ``` 638 | 639 | Advanced: using *compose()* from *redux* 640 | 641 | ```javascript 642 | export default compose( 643 | withTheme(), 644 | withStyles(styles) 645 | )(AppBar); 646 | ``` 647 | 648 | ### Containers are concerned with how things work 649 | 650 | **Requirements** 651 | 652 | - **MAY** contain both presentational components and containers 653 | - **SHOULD NOT** define DOM markup of their own (except for some wrapping divs) 654 | - **MUST NOT** have styles 655 | - **MAY** provide the data and behavior to presentational or other container components. 656 | - **MAY** organise components/containers using routes 657 | - **MAY** implement redux related elements *mapStateToProp()*, *mapDispatchToProps()*, *mergeProps()* and connect it to presentational component using *connect()* 658 | - **MAY** implement API calls or other side effects 659 | - **MAY** be stateful, as they tend to serve as data sources 660 | 661 | #### Implement a container 662 | 663 | ##### Set imports 664 | 665 | **Requirements** 666 | 667 | - **MAY** import elements from state management libraries elements (redux, react-redux) 668 | - **MAY** import elements from *src/redux* such as actions or selectors 669 | - **MAY** import elements from routing library 670 | - **MAY** import *Material-UI* utilities such as *isWidthUp()* 671 | 672 | **Example** 673 | 674 | ```javascript 675 | // Section 1: React/Reduxd low level imports 676 | import React, { Component } from "react"; 677 | import { connect } from "react-redux"; 678 | 679 | // Section 2: internal imports 680 | import AppbarButton from "../../components/AppBar/Button"; 681 | import LayoutSkeleton from "../../components/Layout/Skeleton"; 682 | import { openSidebar } from "../../redux/actions/ui"; 683 | 684 | import { HOME } from "../../constants/routes"; 685 | ``` 686 | 687 | ##### Code container 688 | 689 | **Requirements** 690 | 691 | - **SHOULD NOT** define DOM markup of their own (except for some wrapping divs) 692 | - **MAY** define route 693 | - **MAY** implement hooks on React lifecycle 694 | - **MUST** respect 695 | 696 | **Example** 697 | 698 | Lifecycle container 699 | 700 | ```javascript 701 | class WithLifecycleHooks extends Component { 702 | componentDidMount() { 703 | // Could perform some API calls here 704 | } 705 | 706 | render() { 707 | // return a presentational component 708 | } 709 | 710 | } 711 | ``` 712 | 713 | Routing container 714 | 715 | ```javascript 716 | const Layout = () => ( 717 | 718 | } /> 719 | 720 | 721 | ); 722 | ``` 723 | 724 | ##### Implement *mapStateToProps(state, [ownProps])*, *mapDispatchToProps(dispatch, [ownProps])*, *mergeProps(stateProps, dispatchProps, ownProps)* 725 | 726 | **Requirements** 727 | 728 | - **MAY** implement *mapStateToProps(state, [ownProps])* 729 | - **MAY** implement *mapDispatchToProps(dispatch, [ownProps])*. If it is only required to map action creators it **MUST** be an object 730 | - **MAY** implement *mergeProps(stateProps, dispatchProps, ownProps)* 731 | - Aggregating *props* **MUST NOT** be performed within container, *ownProps* and *mergeProps()* allow to accomplish it properly out of the component (c.f. https://github.com/reduxjs/react-redux/blob/master/docs/api.md) 732 | 733 | **Example** 734 | 735 | ```javascript 736 | const mapStateToProps = state => ({ 737 | shifted: state.ui.sideBarOpen 738 | }); 739 | 740 | const mapDispatchToProps = { 741 | onClick: openSidebar 742 | }; 743 | ``` 744 | 745 | ##### Connect container & export 746 | 747 | **Requirements** 748 | 749 | - **MAY** connect redux information to a comp 750 | - **MUST** be one unique export 751 | 752 | **Example** 753 | 754 | ```javascript 755 | export default connect(mapStateToProps)(AppBar); 756 | ``` 757 | 758 | ## Others 759 | 760 | ### Material-UI 761 | 762 | #### Theme 763 | 764 | [Material UI theme](https://material-ui.com/customization/themes/) is declared in *./src/constants/theme.js* 765 | 766 | #### CSS classes 767 | 768 | To apply styling on specific components and not in the full app, use [Material-UI overrides](https://material-ui.com/customization/overrides/) 769 | 770 | #### UI width 771 | 772 | To make UI fully responsive, you can use [Material-UI breakpoints](https://material-ui.com/layout/breakpoints/) 773 | 774 | ### Audit application 775 | 776 | - Use [Lighthouse](https://developers.google.com/web/tools/lighthouse/) of Google in order to audit your PWA and see what's missing (icons, https, ...) 777 | - Follow the guidelines [here](https://developers.google.com/web/fundamentals/) (Google opinionated way of building PWA) 778 | 779 | ## Conventions 780 | 781 | ### RFC keywords 782 | 783 | 1. **MUST** This word, or the terms "REQUIRED" or "SHALL", mean that the 784 | definition is an absolute requirement of the specification. 785 | 786 | 2. **MUST NOT** This phrase, or the phrase "SHALL NOT", mean that the 787 | definition is an absolute prohibition of the specification. 788 | 789 | 3. **SHOULD** This word, or the adjective "RECOMMENDED", mean that there 790 | may exist valid reasons in particular circumstances to ignore a 791 | particular item, but the full implications must be understood and 792 | carefully weighed before choosing a different course. 793 | 794 | 4. **SHOULD NOT** This phrase, or the phrase "NOT RECOMMENDED" mean that 795 | there may exist valid reasons in particular circumstances when the 796 | particular behavior is acceptable or even useful, but the full 797 | implications should be understood and the case carefully weighed 798 | before implementing any behavior described with this label. 799 | 800 | 801 | 5. **MAY** This word, or the adjective "OPTIONAL", mean that an item is 802 | truly optional. One vendor may choose to include the item because a 803 | particular marketplace requires it or because the vendor feels that 804 | it enhances the product while another vendor may omit the same item. 805 | An implementation which does not include a particular option MUST be 806 | prepared to interoperate with another implementation which does 807 | include the option, though perhaps with reduced functionality. In the 808 | same vein an implementation which does include a particular option 809 | MUST be prepared to interoperate with another implementation which 810 | does not include the option (except, of course, for the feature the 811 | option provides.) -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-alpine as builder 2 | 3 | # Set NODE_ENV to production 4 | ENV NODE_ENV production 5 | 6 | RUN mkdir -p /usr/src/app 7 | WORKDIR /usr/src/app 8 | 9 | # Install dependencies 10 | COPY package.json yarn.lock /usr/src/app/ 11 | RUN yarn install && yarn cache clean 12 | 13 | # Copy source scripts 14 | COPY . /usr/src/app 15 | 16 | # Build react SPA bundle 17 | RUN GENERATE_SOURCEMAP=false yarn build 18 | 19 | FROM nginx:1.15-alpine 20 | 21 | # Set healthcheck route 22 | RUN apk --no-cache add curl 23 | HEALTHCHECK CMD curl -f http://localhost/healthcheck || exit 1 24 | 25 | # Copy nginx conf 26 | COPY nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf 27 | 28 | # Copy application bundle 29 | RUN mkdir -p /var/www/html 30 | COPY --from=builder /usr/src/app/build /var/www/html/ 31 | 32 | # Runtime 33 | EXPOSE 80 34 | CMD ["nginx", "-g", "daemon off;"] 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 by ConsenSys France and contributors. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://img.shields.io/travis/ConsenSys/boilerplate-react.svg)](https://travis-ci.org/ConsenSys/boilerplate-react) 2 | [![Coverage Status](https://img.shields.io/codecov/c/github/ConsenSys/boilerplate-react/master.svg)](https://codecov.io/gh/ConsenSys/boilerplate-react/branch/master) 3 | ![Code style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg) 4 | [![Dependencies](https://img.shields.io/david/ConsenSys/boilerplate-react.svg)](https://david-dm.org/ConsenSys/boilerplate-react) 5 | [![DevDependencies](https://img.shields.io/david/dev/ConsenSys/boilerplate-react.svg)](https://david-dm.org/ConsenSys/boilerplate-react?type=dev) 6 | [![Demo on Heroku](https://img.shields.io/badge/demo-heroku-brightgreen.svg?style=flat-square)](https://consensys-boilerplate-react.herokuapp.com) 7 | 8 | # Boilerplate-React 9 | 10 | This project is a React boilerplate used by ConsenSys to start new projects. 11 | 12 | It includes 13 | 14 | - a stack of *.js* library that we use on a daily basis 15 | - an easy starting set-up using ``docker`` 16 | - contributing guidelines for new developers 17 | 18 | ## Start your own project from this boilerplate 19 | 20 | 1. Change ```name```, ```version```, ```description```, ```repository```, ```keywords```, ```author```, ```bugs``` in ```./package.json``` if needed 21 | 2. Change the icon in ```./public/favicon.ico``` 22 | 3. Change title in `````` tag of ```./public/index.html``` 23 | 4. Change ```short_name```, ```name``` and possibly ```theme_color``` and ```background_color``` in ```./public/manifest.json``` 24 | 5. Update every line ```README.md``` until this line 25 | 26 | ## Stack 27 | 28 | This application make active usage of 29 | 30 | - [create-react-app](https://github.com/facebookincubator/create-react-app) that packs many utilities 31 | - [redux](https://redux.js.org) for state management 32 | - [react-router v4](https://reacttraining.com/react-router/) for routing 33 | - [connected-react-router](https://github.com/supasate/connected-react-router) to connect router to redux state 34 | - [material-ui v1](https://material-ui.com/) as main visual component library 35 | 36 | ## Start application 37 | 38 | ### Requirements 39 | 40 | - docker>=17.0.0 41 | - docker-compose>=1.17.0 42 | - node>=9.0.0 43 | - yarn>=1.6.0 44 | 45 | ### Install application 46 | 47 | If not yet done, clone project locally and install node dependencies 48 | 49 | ```bash 50 | git clone <project-url> && cd <project-folder> 51 | yarn install # install node dependencies locally 52 | ``` 53 | 54 | ### Start application 55 | 56 | ```bash 57 | docker-compose up # start application in dev mode 58 | ``` 59 | 60 | ## Contributing guidelines 61 | 62 | Refer to [contributing guidelines](CONTRIBUTING.md) -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.2' 2 | 3 | services: 4 | web: 5 | image: node:8.9-alpine 6 | environment: 7 | NODE_ENV: development 8 | volumes: 9 | - .:/usr/src/app 10 | command: yarn start 11 | working_dir: /usr/src/app 12 | ports: 13 | - 80:3000 14 | -------------------------------------------------------------------------------- /nginx/conf.d/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name web; 4 | location / { 5 | root /var/www/html; 6 | index index.html index.hrm; 7 | try_files $uri $uri/ /index.html; 8 | } 9 | 10 | location /healthcheck { 11 | access_log off; 12 | return 200 "healthy\n"; 13 | } 14 | 15 | # Gzip Settings 16 | gzip on; 17 | gzip_http_version 1.1; 18 | gzip_disable "MSIE [4-6]\."; 19 | gzip_min_length 20; 20 | gzip_vary on; 21 | gzip_proxied expired no-cache no-store private auth; 22 | gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript; 23 | gzip_comp_level 9; 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boilerplate-react", 3 | "version": "0.2.0-dev", 4 | "description": "React boilerplate", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+ssh://git@github.com:ConsenSys/boilerplate-react.git" 8 | }, 9 | "keywords": [ 10 | "react", 11 | "boilerplate", 12 | "consensys" 13 | ], 14 | "author": "ConsenSys France", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/ConsenSys/boilerplate-react/issues" 18 | }, 19 | "dependencies": { 20 | "@material-ui/core": "^3.1.0", 21 | "@material-ui/icons": "^3.0.0", 22 | "connected-react-router": "^6.2.2", 23 | "history": "^4.7.2", 24 | "react": "^16.8.2", 25 | "react-dom": "^16.8.2", 26 | "react-redux": "^6.0.0", 27 | "react-router": "^4.3.0", 28 | "react-scripts": "^2.0.0", 29 | "redux": "^4.0.1", 30 | "reselect": "^4.0.0" 31 | }, 32 | "devDependencies": { 33 | "enzyme": "^3.7.0", 34 | "enzyme-adapter-react-16": "^1.6.0", 35 | "enzyme-to-json": "^3.3.0", 36 | "husky": "^1.1.0", 37 | "lint-staged": "^7.3.0", 38 | "prettier": "^1.10.2", 39 | "redux-test-utils": "^0.3.0" 40 | }, 41 | "lint-staged": { 42 | "src/**/*.{js,jsx,json,css}": [ 43 | "prettier --write", 44 | "git add" 45 | ] 46 | }, 47 | "scripts": { 48 | "start": "react-scripts start", 49 | "build": "react-scripts build", 50 | "test": "react-scripts test --env=jsdom", 51 | "eject": "react-scripts eject" 52 | }, 53 | "husky": { 54 | "hooks": { 55 | "pre-commit": "lint-staged" 56 | } 57 | }, 58 | "jest": { 59 | "collectCoverageFrom": [ 60 | "src/**/*.{js,jsx}", 61 | "!**/src/index.js", 62 | "!**/src/registerServiceWorker.js" 63 | ] 64 | }, 65 | "prettier": { 66 | "useTabs": false, 67 | "tabWidth": 4 68 | }, 69 | "browserslist": [ 70 | ">0.2%", 71 | "not dead", 72 | "not ie <= 11", 73 | "not op_mini all" 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Consensys/boilerplate-react/7573266a7c9781c0bb687be12cfb9332917e63cf/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en" style="height: 100%"> 3 | <head> 4 | <meta charset="utf-8"> 5 | <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> 6 | <meta name="theme-color" content="#000000"> 7 | <!-- 8 | manifest.json provides metadata used when your web app is added to the 9 | homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ 10 | --> 11 | <link rel="manifest" href="%PUBLIC_URL%/manifest.json"> 12 | <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> 13 | <!-- 14 | Notice the use of %PUBLIC_URL% in the tags above. 15 | It will be replaced with the URL of the `public` folder during the build. 16 | Only files inside the `public` folder can be referenced from the HTML. 17 | 18 | Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will 19 | work correctly both with client-side routing and a non-root public URL. 20 | Learn how to configure a non-root public URL by running `npm run build`. 21 | --> 22 | <title>ConsenSys React Boilerplate 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "ConsenSys React Boilerplate", 3 | "name": "ConsenSys React Boilerplate", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Provider } from "react-redux"; 3 | import { ConnectedRouter } from "connected-react-router"; 4 | 5 | import { MuiThemeProvider } from "@material-ui/core/styles"; 6 | 7 | import createStore from "./redux/store"; 8 | import { history } from "./redux/enhancers/middlewares/router"; 9 | import Layout from "./containers/Layout/Layout"; 10 | import theme from "./constants/theme"; 11 | 12 | class App extends Component { 13 | render() { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | } 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /src/__test__/App.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | import toJson from "enzyme-to-json"; 4 | 5 | import App from "../App"; 6 | 7 | describe("", () => { 8 | let wrapper; 9 | 10 | beforeEach(() => { 11 | wrapper = shallow(); 12 | }); 13 | 14 | it("matches snapshot", () => { 15 | expect(toJson(wrapper)).toMatchSnapshot(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/__test__/__snapshots__/App.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` matches snapshot 1`] = ` 4 | 15 | 37 | 336 | 337 | 338 | 339 | 340 | `; 341 | -------------------------------------------------------------------------------- /src/components/AppBar/AppBar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | 5 | import MuiAppBar from "@material-ui/core/AppBar"; 6 | import { withStyles } from "@material-ui/core/styles"; 7 | 8 | import AppBarToolbar from "./Toolbar"; 9 | 10 | const styles = theme => ({ 11 | appBar: { 12 | position: "fixed", 13 | top: 0, 14 | zIndex: theme.zIndex.drawer + 1, 15 | transition: theme.transitions.create(["width", "margin"], { 16 | easing: theme.transitions.easing.sharp, 17 | duration: theme.transitions.duration.leavingScreen 18 | }) 19 | }, 20 | appBarShifted: { 21 | marginLeft: 240, 22 | width: `calc(100% - ${240}px)`, 23 | transition: theme.transitions.create(["width", "margin"], { 24 | easing: theme.transitions.easing.sharp, 25 | duration: theme.transitions.duration.enteringScreen 26 | }), 27 | [theme.breakpoints.down("sm")]: { 28 | width: "100%" 29 | } 30 | } 31 | }); 32 | 33 | const AppBar = ({ classes, shifted }) => { 34 | return ( 35 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | AppBar.propTypes = { 48 | classes: PropTypes.object.isRequired, 49 | shifted: PropTypes.bool 50 | }; 51 | 52 | export default withStyles(styles)(AppBar); 53 | -------------------------------------------------------------------------------- /src/components/AppBar/IconButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | 5 | import { withStyles } from "@material-ui/core/styles"; 6 | import IconButton from "@material-ui/core/IconButton"; 7 | import MenuIcon from "@material-ui/icons/Menu"; 8 | 9 | const styles = theme => ({ 10 | menuButton: { 11 | marginLeft: 12, 12 | marginRight: 36 13 | }, 14 | hide: { 15 | [theme.breakpoints.up("sm")]: { 16 | display: "none" 17 | } 18 | } 19 | }); 20 | 21 | const AppbarButton = ({ classes, onClick, shifted }) => ( 22 | 28 | 29 | 30 | ); 31 | 32 | AppbarButton.propTypes = { 33 | onClick: PropTypes.func.isRequired, 34 | classes: PropTypes.object.isRequired, 35 | shifted: PropTypes.bool 36 | }; 37 | 38 | export default withStyles(styles)(AppbarButton); 39 | -------------------------------------------------------------------------------- /src/components/AppBar/Title.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import ButtonBase from "@material-ui/core/ButtonBase"; 5 | import Typography from "@material-ui/core/Typography"; 6 | 7 | const AppBarTitle = ({ onClick }) => ( 8 | 9 | 10 | ConsenSys React Boilerplate 11 | 12 | 13 | ); 14 | 15 | AppBarTitle.propTypes = { 16 | onClick: PropTypes.func.isRequired 17 | }; 18 | 19 | export default AppBarTitle; 20 | -------------------------------------------------------------------------------- /src/components/AppBar/Toolbar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import Toolbar from "@material-ui/core/Toolbar"; 5 | import withWidth, { isWidthUp } from "@material-ui/core/withWidth"; 6 | 7 | import AppBarTitle from "../../containers/AppBar/Title"; 8 | import AppBarIconButton from "../../containers/AppBar/IconButton"; 9 | 10 | const AppBarToolbar = ({ width, shifted }) => ( 11 | 12 | 13 | 14 | 15 | ); 16 | 17 | AppBarToolbar.propTypes = { 18 | width: PropTypes.string.isRequired, 19 | shifted: PropTypes.bool 20 | }; 21 | 22 | export default withWidth()(AppBarToolbar); 23 | -------------------------------------------------------------------------------- /src/components/AppBar/__tests__/AppBar.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import toJson from "enzyme-to-json"; 3 | 4 | import AppBar from "../AppBar"; 5 | import AppBarToolbar from "../Toolbar"; 6 | import createShallow from "../../../tests-utils/createShallow"; 7 | 8 | describe("", () => { 9 | let shallow; 10 | let wrapper; 11 | 12 | beforeAll(() => { 13 | shallow = createShallow({ dive: 2 }); 14 | }); 15 | 16 | beforeEach(() => { 17 | wrapper = shallow(); 18 | }); 19 | 20 | it("has className appBar", () => { 21 | expect(wrapper.props().className).toContain("appBar"); 22 | }); 23 | 24 | it("renders 1 components", () => { 25 | expect(wrapper.find(AppBarToolbar).length).toEqual(1); 26 | }); 27 | 28 | describe("when shifted=true", () => { 29 | beforeEach(() => { 30 | wrapper = shallow(); 31 | }); 32 | 33 | it("matches snapshot", () => { 34 | expect(toJson(wrapper)).toMatchSnapshot(); 35 | }); 36 | 37 | it("has className appBarShifted", () => { 38 | expect(wrapper.props().className).toContain("appBarShifted"); 39 | }); 40 | }); 41 | 42 | describe("when shifted=false", () => { 43 | beforeEach(() => { 44 | wrapper = shallow(); 45 | }); 46 | 47 | it("matches snapshot", () => { 48 | expect(toJson(wrapper)).toMatchSnapshot(); 49 | }); 50 | 51 | it("does not have className appBarShift", () => { 52 | expect(wrapper.props().className).not.toContain("appBarShift"); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/components/AppBar/__tests__/IconButton.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import toJson from "enzyme-to-json"; 3 | 4 | import IconButton from "@material-ui/core/IconButton"; 5 | 6 | import AppBarIconButton from "../IconButton"; 7 | import createShallow from "../../../tests-utils/createShallow"; 8 | 9 | describe("", () => { 10 | let shallow; 11 | let wrapper; 12 | let props = {}; 13 | 14 | beforeAll(() => { 15 | shallow = createShallow({ dive: 1 }); 16 | }); 17 | 18 | beforeEach(() => { 19 | props.onClick = jest.fn(); 20 | wrapper = shallow(); 21 | }); 22 | 23 | it("renders 1 button", () => { 24 | expect(wrapper.find(IconButton).length).toEqual(1); 25 | }); 26 | 27 | it("button is functional", () => { 28 | wrapper 29 | .find(IconButton) 30 | .at(0) 31 | .simulate("click"); 32 | expect(props.onClick.mock.calls.length).toEqual(1); 33 | }); 34 | 35 | describe("when shifted=true", () => { 36 | beforeEach(() => { 37 | wrapper = shallow(); 38 | }); 39 | 40 | it("matches snapshot", () => { 41 | expect(toJson(wrapper)).toMatchSnapshot(); 42 | }); 43 | }); 44 | 45 | describe("when shifted=false", () => { 46 | beforeEach(() => { 47 | wrapper = shallow(); 48 | }); 49 | 50 | it("matches snapshot", () => { 51 | expect(toJson(wrapper)).toMatchSnapshot(); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/components/AppBar/__tests__/Title.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import toJson from "enzyme-to-json"; 3 | import { shallow } from "enzyme"; 4 | 5 | import ButtonBase from "@material-ui/core/ButtonBase"; 6 | 7 | import AppBarTitle from "../Title"; 8 | 9 | describe("", () => { 10 | let wrapper; 11 | let props = {}; 12 | 13 | beforeEach(() => { 14 | props.onClick = jest.fn(); 15 | wrapper = shallow(); 16 | }); 17 | 18 | it("matches snapshot", () => { 19 | expect(toJson(wrapper)).toMatchSnapshot(); 20 | }); 21 | 22 | it("renders 1 button", () => { 23 | expect(wrapper.find(ButtonBase).length).toEqual(1); 24 | }); 25 | 26 | it("button is functional", () => { 27 | wrapper 28 | .find(ButtonBase) 29 | .at(0) 30 | .simulate("click"); 31 | expect(props.onClick.mock.calls.length).toEqual(1); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/components/AppBar/__tests__/Toolbar.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import toJson from "enzyme-to-json"; 3 | 4 | import { MuiThemeProvider, createMuiTheme } from "@material-ui/core/styles"; 5 | 6 | import AppBarToolbar from "../Toolbar"; 7 | import createShallow from "../../../tests-utils/createShallow"; 8 | 9 | describe("", () => { 10 | let shallow; 11 | let wrapper; 12 | 13 | beforeAll(() => { 14 | shallow = createShallow({ dive: 4 }); 15 | }); 16 | 17 | describe("when shifted=true and width is smDown", () => { 18 | beforeEach(() => { 19 | global.window.innerWidth = 959; 20 | wrapper = shallow( 21 | 26 | 27 | 28 | ); 29 | }); 30 | 31 | it("matches snapshot", () => { 32 | expect(toJson(wrapper)).toMatchSnapshot(); 33 | }); 34 | 35 | it("has props disableGutters=false", () => { 36 | expect(wrapper.props().disableGutters).toBeFalsy(); 37 | }); 38 | }); 39 | 40 | describe("when shifted=true andd width is mdUp", () => { 41 | beforeEach(() => { 42 | global.window.innerWidth = 960; 43 | wrapper = shallow( 44 | 49 | 50 | 51 | ); 52 | }); 53 | 54 | it("matches snapshot", () => { 55 | expect(toJson(wrapper)).toMatchSnapshot(); 56 | }); 57 | 58 | it("has props disableGutters=true", () => { 59 | expect(wrapper.props().disableGutters).toBe(true); 60 | }); 61 | }); 62 | 63 | describe("when shifted=false and width is smDown", () => { 64 | beforeEach(() => { 65 | global.window.innerWidth = 959; 66 | wrapper = shallow( 67 | 72 | 73 | 74 | ); 75 | }); 76 | 77 | it("matches snapshot", () => { 78 | expect(toJson(wrapper)).toMatchSnapshot(); 79 | }); 80 | 81 | it("has props disableGutters=true", () => { 82 | expect(wrapper.props().disableGutters).toBe(true); 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /src/components/AppBar/__tests__/__snapshots__/AppBar.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` when shifted=false matches snapshot 1`] = ` 4 | 22 | 23 | 24 | `; 25 | 26 | exports[` when shifted=true matches snapshot 1`] = ` 27 | 45 | 48 | 49 | `; 50 | -------------------------------------------------------------------------------- /src/components/AppBar/__tests__/__snapshots__/IconButton.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` when shifted=false matches snapshot 1`] = ` 4 | 10 | 11 | 12 | `; 13 | 14 | exports[` when shifted=true matches snapshot 1`] = ` 15 | 21 | 22 | 23 | `; 24 | -------------------------------------------------------------------------------- /src/components/AppBar/__tests__/__snapshots__/Title.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` matches snapshot 1`] = ` 4 | 7 | 12 | ConsenSys React Boilerplate 13 | 14 | 15 | `; 16 | -------------------------------------------------------------------------------- /src/components/AppBar/__tests__/__snapshots__/Toolbar.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` when shifted=false and width is smDown matches snapshot 1`] = ` 4 | 16 | 17 | 18 | 19 | `; 20 | 21 | exports[` when shifted=true and width is smDown matches snapshot 1`] = ` 22 | 34 | 35 | 36 | 37 | `; 38 | 39 | exports[` when shifted=true andd width is mdUp matches snapshot 1`] = ` 40 | 52 | 53 | 54 | 55 | `; 56 | -------------------------------------------------------------------------------- /src/components/Home/Home.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import { withStyles } from "@material-ui/core/styles"; 5 | 6 | import logo from "./logo.svg"; 7 | 8 | const styles = theme => ({ 9 | home: { 10 | textAlign: "center" 11 | }, 12 | homeLogo: { 13 | height: "80px" 14 | }, 15 | homeHeader: { 16 | height: "150px", 17 | padding: "20px" 18 | }, 19 | homeTitle: { 20 | fontSize: "1.5em" 21 | }, 22 | homeIntro: { 23 | fontSize: "large" 24 | } 25 | }); 26 | 27 | const Home = ({ classes }) => ( 28 |
29 |
30 | logo 31 |

32 | Welcome to ConsenSys React Boilerplate 33 |

34 |
35 |

36 | To get started, edit src/App.js and save to reload. 37 |

38 |
39 | ); 40 | 41 | Home.propTypes = { 42 | classes: PropTypes.object.isRequired 43 | }; 44 | 45 | export default withStyles(styles)(Home); 46 | -------------------------------------------------------------------------------- /src/components/Home/__tests__/Home.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import toJson from "enzyme-to-json"; 3 | 4 | import Home from "../Home"; 5 | import createShallow from "../../../tests-utils/createShallow"; 6 | 7 | describe("", () => { 8 | let shallow; 9 | let wrapper; 10 | 11 | beforeAll(() => { 12 | shallow = createShallow({ dive: 1 }); 13 | }); 14 | beforeEach(() => { 15 | wrapper = shallow(); 16 | }); 17 | 18 | it("matches snapshot", () => { 19 | expect(toJson(wrapper)).toMatchSnapshot(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/Home/__tests__/__snapshots__/Home.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` matches snapshot 1`] = ` 4 |
7 |
10 | logo 15 |

18 | Welcome to ConsenSys React Boilerplate 19 |

20 |
21 |

24 | To get started, edit 25 | 26 | src/App.js 27 | 28 | and save to reload. 29 |

30 |
31 | `; 32 | -------------------------------------------------------------------------------- /src/components/Home/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Layout/Skeleton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import { withStyles } from "@material-ui/core/styles"; 5 | 6 | import LayoutContent from "../../containers/Layout/Content"; 7 | import Sidebar from "../../containers/Sidebar/Sidebar"; 8 | import AppBar from "../../containers/AppBar/AppBar"; 9 | 10 | const styles = theme => ({ 11 | frame: { 12 | position: "relative", 13 | display: "flex", 14 | width: "100%", 15 | minHeight: "100%" 16 | }, 17 | content: { 18 | width: "100%", 19 | flexGrow: 1, 20 | backgroundColor: theme.palette.background.default, 21 | minHeight: "calc(100% - 56px)", 22 | marginTop: 56, 23 | [theme.breakpoints.up("sm")]: { 24 | minHeight: "calc(100% - 64px)", 25 | marginTop: 64 26 | } 27 | } 28 | }); 29 | 30 | const LayoutSkeleton = ({ classes }) => ( 31 |
32 | 33 | 34 |
35 | 36 |
37 |
38 | ); 39 | 40 | LayoutSkeleton.propTypes = { 41 | classes: PropTypes.object.isRequired 42 | }; 43 | 44 | export default withStyles(styles)(LayoutSkeleton); 45 | -------------------------------------------------------------------------------- /src/components/Layout/__tests__/Skeleton.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import toJson from "enzyme-to-json"; 3 | 4 | import LayoutSkeleton from "../Skeleton"; 5 | import LayoutContent from "../../../containers/Layout/Content"; 6 | import AppBar from "../../../containers/AppBar/AppBar"; 7 | import Sidebar from "../../../containers/Sidebar/Sidebar"; 8 | import createShallow from "../../../tests-utils/createShallow"; 9 | 10 | describe("", () => { 11 | let shallow; 12 | let wrapper; 13 | 14 | beforeAll(() => { 15 | shallow = createShallow({ dive: 1 }); 16 | }); 17 | 18 | beforeEach(() => { 19 | wrapper = shallow(); 20 | }); 21 | 22 | it("renders 1 components", () => { 23 | expect(wrapper.find(LayoutContent).length).toEqual(1); 24 | }); 25 | 26 | it("renders 1 components", () => { 27 | expect(wrapper.find(AppBar).length).toEqual(1); 28 | }); 29 | 30 | it("renders 1 components", () => { 31 | expect(wrapper.find(Sidebar).length).toEqual(1); 32 | }); 33 | 34 | it("matches snapshot", () => { 35 | expect(toJson(wrapper)).toMatchSnapshot(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/components/Layout/__tests__/__snapshots__/Skeleton.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` matches snapshot 1`] = ` 4 |
7 | 8 | 9 |
12 | 13 |
14 |
15 | `; 16 | -------------------------------------------------------------------------------- /src/components/NoWhere/HomeButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import Button from "@material-ui/core/Button"; 5 | import { withStyles } from "@material-ui/core/styles"; 6 | 7 | const styles = theme => ({ 8 | button: { 9 | margin: theme.spacing.unit 10 | } 11 | }); 12 | 13 | const NoWhereHomeButton = ({ classes, onClick }) => ( 14 | 22 | ); 23 | 24 | NoWhereHomeButton.propTypes = { 25 | classes: PropTypes.object.isRequired, 26 | onClick: PropTypes.func.isRequired 27 | }; 28 | 29 | export default withStyles(styles)(NoWhereHomeButton); 30 | -------------------------------------------------------------------------------- /src/components/NoWhere/NoWhere.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import ReportIcon from "@material-ui/icons/Report"; 5 | import { withStyles } from "@material-ui/core/styles"; 6 | import { grey } from "@material-ui/core/colors"; 7 | 8 | import NoWhereHomeButton from "../../containers/NoWhere/HomeButton"; 9 | 10 | const styles = theme => ({ 11 | nowhere: { 12 | textAlign: "center" 13 | }, 14 | nowhereHeader: { 15 | padding: theme.spacing.unit * 3 16 | }, 17 | nowhereTitle: { 18 | fontSize: "1.5em", 19 | color: grey[500] 20 | }, 21 | nowhereIcon: { 22 | height: 80, 23 | width: 80, 24 | color: grey[500] 25 | } 26 | }); 27 | 28 | const NoWhere = ({ classes }) => ( 29 |
30 |
31 | 32 |

33 | Oups... It seems that you are lost! 34 |

35 |
36 | 37 |
38 | ); 39 | 40 | NoWhere.propTypes = { 41 | classes: PropTypes.object.isRequired 42 | }; 43 | 44 | export default withStyles(styles)(NoWhere); 45 | -------------------------------------------------------------------------------- /src/components/NoWhere/__test__/HomeButton.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | import toJson from "enzyme-to-json"; 4 | 5 | import Button from "@material-ui/core/Button"; 6 | 7 | import NoWhereHomeButton from "../HomeButton"; 8 | 9 | describe("", () => { 10 | let wrapper; 11 | let props; 12 | 13 | beforeEach(() => { 14 | props = { 15 | onClick: jest.fn() 16 | }; 17 | wrapper = shallow().dive(); 18 | }); 19 | 20 | it("renders 1 Button", () => { 21 | expect(wrapper.find(Button).length).toEqual(1); 22 | }); 23 | 24 | it("Button is functional", () => { 25 | wrapper 26 | .find(Button) 27 | .at(0) 28 | .simulate("click"); 29 | expect(props.onClick.mock.calls.length).toEqual(1); 30 | }); 31 | 32 | it("matches snapshot", () => { 33 | expect(toJson(wrapper)).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/components/NoWhere/__test__/NoWhere.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import toJson from "enzyme-to-json"; 3 | 4 | import ReportIcon from "@material-ui/icons/Report"; 5 | 6 | import NoWhere from "../NoWhere"; 7 | import NoWhereHomeButton from "../../../containers/NoWhere/HomeButton"; 8 | import createShallow from "../../../tests-utils/createShallow"; 9 | 10 | describe("", () => { 11 | let shallow; 12 | let wrapper; 13 | 14 | beforeAll(() => { 15 | shallow = createShallow({ dive: 1 }); 16 | }); 17 | 18 | beforeEach(() => { 19 | wrapper = shallow(); 20 | }); 21 | 22 | it("renders 1 ReportIcon", () => { 23 | expect(wrapper.find(ReportIcon).length).toEqual(1); 24 | }); 25 | 26 | it("renders 1 BackHomeButton", () => { 27 | expect(wrapper.find(NoWhereHomeButton).length).toEqual(1); 28 | }); 29 | 30 | it("matches snapshot", () => { 31 | expect(toJson(wrapper)).toMatchSnapshot(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/components/NoWhere/__test__/__snapshots__/HomeButton.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` matches snapshot 1`] = ` 4 | 10 | Bring me back home! 11 | 12 | `; 13 | -------------------------------------------------------------------------------- /src/components/NoWhere/__test__/__snapshots__/NoWhere.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` matches snapshot 1`] = ` 4 |
7 |
10 | 13 |

16 | Oups... It seems that you are lost! 17 |

18 |
19 | 20 |
21 | `; 22 | -------------------------------------------------------------------------------- /src/components/Sidebar/Header.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import { withStyles } from "@material-ui/core/styles"; 5 | import IconButton from "@material-ui/core/IconButton"; 6 | import ChevronLeftIcon from "@material-ui/icons/ChevronLeft"; 7 | 8 | const styles = theme => ({ 9 | drawerHeader: { 10 | display: "flex", 11 | alignItems: "center", 12 | justifyContent: "flex-end", 13 | padding: "0 " + theme.spacing.unit + "px", 14 | ...theme.mixins.toolbar 15 | } 16 | }); 17 | 18 | const SidebarHeader = ({ classes, onClick }) => { 19 | return ( 20 |
21 | 22 | 23 | 24 |
25 | ); 26 | }; 27 | 28 | SidebarHeader.propTypes = { 29 | classes: PropTypes.object.isRequired, 30 | selected: PropTypes.bool, 31 | onClick: PropTypes.func.isRequired 32 | }; 33 | 34 | export default withStyles(styles)(SidebarHeader); 35 | -------------------------------------------------------------------------------- /src/components/Sidebar/HomeItem.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import HomeIcon from "@material-ui/icons/Home"; 5 | 6 | import ListItem1 from "../UI/ListItem/1"; 7 | 8 | const SidebarHomeItem = ({ onClick, selected }) => ( 9 | } 11 | text="Home" 12 | onClick={onClick} 13 | selected={selected} 14 | /> 15 | ); 16 | 17 | SidebarHomeItem.propTypes = { 18 | selected: PropTypes.bool, 19 | onClick: PropTypes.func.isRequired 20 | }; 21 | 22 | export default SidebarHomeItem; 23 | -------------------------------------------------------------------------------- /src/components/Sidebar/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { compose } from "redux"; 5 | 6 | import Drawer from "@material-ui/core/Drawer"; 7 | import List from "@material-ui/core/List"; 8 | import withWidth, { isWidthUp } from "@material-ui/core/withWidth"; 9 | import { withStyles } from "@material-ui/core/styles"; 10 | 11 | import SidebarHomeItem from "../../containers/Sidebar/HomeItem"; 12 | import SidebarHeader from "../../containers/Sidebar/Header"; 13 | 14 | const styles = theme => ({ 15 | drawer: { 16 | height: "100vh", 17 | position: "fixed" 18 | }, 19 | drawerPaper: { 20 | position: "relative", 21 | height: "100%", 22 | width: 240, 23 | transition: theme.transitions.create("width", { 24 | easing: theme.transitions.easing.sharp, 25 | duration: theme.transitions.duration.enteringScreen 26 | }) 27 | }, 28 | drawerPaperClose: { 29 | width: 0, 30 | overflowX: "hidden", 31 | transition: theme.transitions.create("width", { 32 | easing: theme.transitions.easing.sharp, 33 | duration: theme.transitions.duration.leavingScreen 34 | }), 35 | [theme.breakpoints.up("sm")]: { 36 | width: 60 37 | } 38 | }, 39 | drawerInner: { 40 | // Make the items inside not wrap when transitioning: 41 | width: 240 42 | } 43 | }); 44 | 45 | const Sidebar = ({ classes, width, variant, open, onClose }) => { 46 | const isLargeScreen = isWidthUp("sm", width); 47 | return ( 48 | 64 |
65 | 66 | 67 | 68 | 69 |
70 |
71 | ); 72 | }; 73 | 74 | Sidebar.propTypes = { 75 | classes: PropTypes.object.isRequired, 76 | width: PropTypes.oneOf(["xs", "sm", "md", "lg", "xl"]), 77 | variant: PropTypes.string, 78 | open: PropTypes.bool, 79 | onClose: PropTypes.func 80 | }; 81 | 82 | export default compose(withWidth(), withStyles(styles))(Sidebar); 83 | -------------------------------------------------------------------------------- /src/components/Sidebar/__test__/Header.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import toJson from "enzyme-to-json"; 3 | 4 | import IconButton from "@material-ui/core/IconButton"; 5 | import ChevronLeftIcon from "@material-ui/icons/ChevronLeft"; 6 | 7 | import SidebarHeader from "../Header"; 8 | import createShallow from "../../../tests-utils/createShallow"; 9 | 10 | describe("", () => { 11 | let shallow; 12 | let wrapper; 13 | let props = {}; 14 | 15 | beforeAll(() => { 16 | shallow = createShallow({ dive: 1 }); 17 | }); 18 | 19 | beforeEach(() => { 20 | props = { 21 | onClick: jest.fn() 22 | }; 23 | wrapper = shallow(); 24 | }); 25 | 26 | it("renders 1 IconButton", () => { 27 | expect(wrapper.find(IconButton).length).toEqual(1); 28 | }); 29 | 30 | it("renders 1 ChevronLeftIcon", () => { 31 | expect(wrapper.find(ChevronLeftIcon).length).toEqual(1); 32 | }); 33 | 34 | it("IconButton button is functional", () => { 35 | wrapper 36 | .find(IconButton) 37 | .at(0) 38 | .simulate("click"); 39 | expect(props.onClick.mock.calls.length).toEqual(1); 40 | }); 41 | 42 | it("matches snapshot", () => { 43 | expect(toJson(wrapper)).toMatchSnapshot(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/components/Sidebar/__test__/HomeItem.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import toJson from "enzyme-to-json"; 3 | import { shallow } from "enzyme"; 4 | 5 | import HomeIcon from "@material-ui/icons/Home"; 6 | 7 | import ListItem1 from "../../UI/ListItem/1"; 8 | import SidebarHomeItem from "../HomeItem"; 9 | 10 | describe("", () => { 11 | let wrapper; 12 | let props = {}; 13 | 14 | beforeEach(() => { 15 | props = { 16 | onClick: jest.fn() 17 | }; 18 | wrapper = shallow(); 19 | }); 20 | 21 | it("renders 1 ListItem1", () => { 22 | expect(wrapper.find(ListItem1).length).toEqual(1); 23 | }); 24 | 25 | it("renders 1 HomeIcon", () => { 26 | expect( 27 | wrapper 28 | .dive() 29 | .dive() 30 | .find(HomeIcon).length 31 | ).toEqual(1); 32 | }); 33 | 34 | it("ListItem1 button is functional", () => { 35 | wrapper 36 | .find(ListItem1) 37 | .at(0) 38 | .simulate("click"); 39 | expect(props.onClick.mock.calls.length).toEqual(1); 40 | }); 41 | 42 | it("matches snapshot", () => { 43 | expect(toJson(wrapper)).toMatchSnapshot(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/components/Sidebar/__test__/Sidebar.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import toJson from "enzyme-to-json"; 3 | 4 | import Drawer from "@material-ui/core/Drawer"; 5 | import { MuiThemeProvider, createMuiTheme } from "@material-ui/core/styles"; 6 | 7 | import Sidebar from "../Sidebar"; 8 | import SidebarHomeItem from "../../../containers/Sidebar/HomeItem"; 9 | import SidebarHeader from "../../../containers/Sidebar/Header"; 10 | import createShallow from "../../../tests-utils/createShallow"; 11 | 12 | describe("", () => { 13 | let shallow; 14 | let wrapper; 15 | let props = {}; 16 | 17 | beforeEach(() => { 18 | props = { 19 | onClose: jest.fn() 20 | }; 21 | shallow = createShallow({ dive: 3 }); 22 | wrapper = shallow(); 23 | }); 24 | 25 | it("renders 1 components", () => { 26 | expect(wrapper.find(Drawer).length).toEqual(1); 27 | }); 28 | 29 | it("renders 1 components", () => { 30 | expect(wrapper.find(SidebarHeader).length).toEqual(1); 31 | }); 32 | 33 | it("renders 1 components", () => { 34 | expect(wrapper.find(SidebarHomeItem).length).toEqual(1); 35 | }); 36 | 37 | it("button is functional", () => { 38 | wrapper 39 | .find(Drawer) 40 | .at(0) 41 | .simulate("close"); 42 | expect(props.onClose.mock.calls.length).toEqual(1); 43 | }); 44 | 45 | describe("when width is smUp", () => { 46 | beforeEach(() => { 47 | global.window.innerWidth = 600; 48 | shallow = createShallow({ dive: 4 }); 49 | wrapper = shallow( 50 | 55 | 56 | 57 | ); 58 | }); 59 | 60 | it("matches snapshot", () => { 61 | expect(toJson(wrapper)).toMatchSnapshot(); 62 | }); 63 | 64 | it("Drawer has class drawer and paper", () => { 65 | expect( 66 | wrapper 67 | .find(Drawer) 68 | .at(0) 69 | .props().classes.docked 70 | ).toBeDefined(); 71 | expect( 72 | wrapper 73 | .find(Drawer) 74 | .at(0) 75 | .props().classes.paper 76 | ).toBeDefined(); 77 | }); 78 | }); 79 | 80 | describe("when width is smDown", () => { 81 | beforeEach(() => { 82 | global.window.innerWidth = 599; 83 | shallow = createShallow({ dive: 4 }); 84 | wrapper = shallow( 85 | 90 | 91 | 92 | ); 93 | }); 94 | 95 | it("matches snapshot", () => { 96 | expect(toJson(wrapper)).toMatchSnapshot(); 97 | }); 98 | 99 | it("Drawer has not class drawer nor paper", () => { 100 | expect( 101 | wrapper 102 | .find(Drawer) 103 | .at(0) 104 | .props().classes.docked 105 | ).toBeFalsy(); 106 | expect( 107 | wrapper 108 | .find(Drawer) 109 | .at(0) 110 | .props().classes.paper 111 | ).toBeFalsy(); 112 | }); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /src/components/Sidebar/__test__/__snapshots__/Header.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` matches snapshot 1`] = ` 4 |
7 | 10 | 11 | 12 |
13 | `; 14 | -------------------------------------------------------------------------------- /src/components/Sidebar/__test__/__snapshots__/HomeItem.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` matches snapshot 1`] = ` 4 | } 6 | onClick={[MockFunction]} 7 | text="Home" 8 | /> 9 | `; 10 | -------------------------------------------------------------------------------- /src/components/Sidebar/__test__/__snapshots__/Sidebar.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` when width is smDown matches snapshot 1`] = ` 4 | 8 |
11 | 12 | 13 | 14 | 15 |
16 |
17 | `; 18 | 19 | exports[` when width is smUp matches snapshot 1`] = ` 20 | 30 |
33 | 34 | 35 | 36 | 37 |
38 |
39 | `; 40 | -------------------------------------------------------------------------------- /src/components/UI/ListItem/1.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import { withStyles } from "@material-ui/core/styles"; 5 | import ListItem from "@material-ui/core/ListItem"; 6 | import ListItemIcon from "@material-ui/core/ListItemIcon"; 7 | import ListItemText from "@material-ui/core/ListItemText"; 8 | 9 | const styles = theme => ({ 10 | iconSelected: { 11 | color: theme.palette.primary.main 12 | } 13 | }); 14 | 15 | const ListItem1 = ({ classes, icon, text, onClick, selected, ...props }) => ( 16 | 17 | 18 | {icon} 19 | 20 | 21 | 22 | ); 23 | 24 | ListItem1.propTypes = { 25 | icon: PropTypes.node.isRequired, 26 | text: PropTypes.string.isRequired, 27 | color: PropTypes.string, 28 | onClick: PropTypes.func.isRequired 29 | }; 30 | 31 | export default withStyles(styles)(ListItem1); 32 | -------------------------------------------------------------------------------- /src/components/UI/ListItem/__test__/1.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import toJson from "enzyme-to-json"; 3 | 4 | import HomeIcon from "@material-ui/icons/Home"; 5 | import ListItem from "@material-ui/core/ListItem"; 6 | import ListItemIcon from "@material-ui/core/ListItemIcon"; 7 | import ListItemText from "@material-ui/core/ListItemText"; 8 | import getClasses from "@material-ui/core/test-utils/getClasses"; 9 | 10 | import ListItem1 from "../1"; 11 | import createShallow from "../../../../tests-utils/createShallow"; 12 | 13 | describe("", () => { 14 | let shallow; 15 | let wrapper; 16 | let classes; 17 | let props = {}; 18 | 19 | beforeAll(() => { 20 | shallow = createShallow({ dive: 1 }); 21 | }); 22 | 23 | beforeEach(() => { 24 | props = { 25 | onClick: jest.fn(), 26 | text: "Test-Text", 27 | icon: 28 | }; 29 | classes = classes = getClasses(); 30 | wrapper = shallow(); 31 | }); 32 | 33 | it("renders 1 ListItem, ListItemIcon, ListItemText", () => { 34 | expect(wrapper.find(ListItem).length).toEqual(1); 35 | expect(wrapper.find(ListItemIcon).length).toEqual(1); 36 | expect(wrapper.find(ListItemText).length).toEqual(1); 37 | }); 38 | 39 | it("renders 1 HomeIcon", () => { 40 | expect(wrapper.find(HomeIcon).length).toEqual(1); 41 | }); 42 | 43 | it("ListItem button is functional", () => { 44 | wrapper 45 | .find(ListItem) 46 | .at(0) 47 | .simulate("click"); 48 | expect(props.onClick.mock.calls.length).toEqual(1); 49 | }); 50 | 51 | describe("when selected=true", () => { 52 | beforeEach(() => { 53 | wrapper = shallow(); 54 | }); 55 | 56 | it("matches snapshot", () => { 57 | expect(toJson(wrapper)).toMatchSnapshot(); 58 | }); 59 | 60 | it("ListItemIcon has class iconSelected", () => { 61 | expect( 62 | wrapper 63 | .find(ListItemIcon) 64 | .at(0) 65 | .hasClass(classes.iconSelected) 66 | ).toBe(true); 67 | }); 68 | }); 69 | 70 | describe("when selected=false", () => { 71 | beforeEach(() => { 72 | wrapper = shallow(); 73 | }); 74 | 75 | it("matches snapshot", () => { 76 | expect(toJson(wrapper)).toMatchSnapshot(); 77 | }); 78 | 79 | it("ListItemIcon has not class iconSelected", () => { 80 | expect( 81 | wrapper 82 | .find(ListItemIcon) 83 | .at(0) 84 | .hasClass(classes.iconSelected) 85 | ).toBe(false); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/components/UI/ListItem/__test__/__snapshots__/1.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` when selected=false matches snapshot 1`] = ` 4 | 8 | 9 | 10 | 11 | 14 | 15 | `; 16 | 17 | exports[` when selected=true matches snapshot 1`] = ` 18 | 22 | 25 | 26 | 27 | 30 | 31 | `; 32 | -------------------------------------------------------------------------------- /src/constants/__test__/__snapshots__/theme.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`materialUiTheme should match snapshot 1`] = ` 4 | Object { 5 | "breakpoints": Object { 6 | "between": [Function], 7 | "down": [Function], 8 | "keys": Array [ 9 | "xs", 10 | "sm", 11 | "md", 12 | "lg", 13 | "xl", 14 | ], 15 | "only": [Function], 16 | "up": [Function], 17 | "values": Object { 18 | "lg": 1280, 19 | "md": 960, 20 | "sm": 600, 21 | "xl": 1920, 22 | "xs": 0, 23 | }, 24 | "width": [Function], 25 | }, 26 | "direction": "ltr", 27 | "mixins": Object { 28 | "gutters": [Function], 29 | "toolbar": Object { 30 | "@media (min-width:0px) and (orientation: landscape)": Object { 31 | "minHeight": 48, 32 | }, 33 | "@media (min-width:600px)": Object { 34 | "minHeight": 64, 35 | }, 36 | "minHeight": 56, 37 | }, 38 | }, 39 | "overrides": Object {}, 40 | "palette": Object { 41 | "action": Object { 42 | "active": "rgba(0, 0, 0, 0.54)", 43 | "disabled": "rgba(0, 0, 0, 0.26)", 44 | "disabledBackground": "rgba(0, 0, 0, 0.12)", 45 | "hover": "rgba(0, 0, 0, 0.08)", 46 | "hoverOpacity": 0.08, 47 | "selected": "rgba(0, 0, 0, 0.14)", 48 | }, 49 | "augmentColor": [Function], 50 | "background": Object { 51 | "default": "#fafafa", 52 | "paper": "#fff", 53 | }, 54 | "common": Object { 55 | "black": "#000", 56 | "white": "#fff", 57 | }, 58 | "contrastThreshold": 3, 59 | "divider": "rgba(0, 0, 0, 0.12)", 60 | "error": Object { 61 | "100": "#ffcdd2", 62 | "200": "#ef9a9a", 63 | "300": "#e57373", 64 | "400": "#ef5350", 65 | "50": "#ffebee", 66 | "500": "#f44336", 67 | "600": "#e53935", 68 | "700": "#d32f2f", 69 | "800": "#c62828", 70 | "900": "#b71c1c", 71 | "A100": "#ff8a80", 72 | "A200": "#ff5252", 73 | "A400": "#ff1744", 74 | "A700": "#d50000", 75 | "contrastText": "#fff", 76 | "dark": "#d32f2f", 77 | "light": "#e57373", 78 | "main": "#f44336", 79 | }, 80 | "getContrastText": [Function], 81 | "grey": Object { 82 | "100": "#f5f5f5", 83 | "200": "#eeeeee", 84 | "300": "#e0e0e0", 85 | "400": "#bdbdbd", 86 | "50": "#fafafa", 87 | "500": "#9e9e9e", 88 | "600": "#757575", 89 | "700": "#616161", 90 | "800": "#424242", 91 | "900": "#212121", 92 | "A100": "#d5d5d5", 93 | "A200": "#aaaaaa", 94 | "A400": "#303030", 95 | "A700": "#616161", 96 | }, 97 | "primary": Object { 98 | "100": "#c5cae9", 99 | "200": "#9fa8da", 100 | "300": "#7986cb", 101 | "400": "#5c6bc0", 102 | "50": "#e8eaf6", 103 | "500": "#3f51b5", 104 | "600": "#3949ab", 105 | "700": "#303f9f", 106 | "800": "#283593", 107 | "900": "#1a237e", 108 | "A100": "#8c9eff", 109 | "A200": "#536dfe", 110 | "A400": "#3d5afe", 111 | "A700": "#304ffe", 112 | "contrastText": "#fff", 113 | "dark": "#303f9f", 114 | "light": "#7986cb", 115 | "main": "#3f51b5", 116 | }, 117 | "secondary": Object { 118 | "100": "#c8e6c9", 119 | "200": "#a5d6a7", 120 | "300": "#81c784", 121 | "400": "#66bb6a", 122 | "50": "#e8f5e9", 123 | "500": "#4caf50", 124 | "600": "#43a047", 125 | "700": "#388e3c", 126 | "800": "#2e7d32", 127 | "900": "#1b5e20", 128 | "A100": "#b9f6ca", 129 | "A200": "#69f0ae", 130 | "A400": "#00e677", 131 | "A700": "#00c853", 132 | "contrastText": "rgba(0, 0, 0, 0.87)", 133 | "dark": "#00c853", 134 | "light": "#69f0ae", 135 | "main": "#00e677", 136 | }, 137 | "text": Object { 138 | "disabled": "rgba(0, 0, 0, 0.38)", 139 | "hint": "rgba(0, 0, 0, 0.38)", 140 | "primary": "rgba(0, 0, 0, 0.87)", 141 | "secondary": "rgba(0, 0, 0, 0.54)", 142 | }, 143 | "tonalOffset": 0.2, 144 | "type": "light", 145 | }, 146 | "props": Object {}, 147 | "shadows": Array [ 148 | "none", 149 | "0px 1px 3px 0px rgba(0, 0, 0, 0.2),0px 1px 1px 0px rgba(0, 0, 0, 0.14),0px 2px 1px -1px rgba(0, 0, 0, 0.12)", 150 | "0px 1px 5px 0px rgba(0, 0, 0, 0.2),0px 2px 2px 0px rgba(0, 0, 0, 0.14),0px 3px 1px -2px rgba(0, 0, 0, 0.12)", 151 | "0px 1px 8px 0px rgba(0, 0, 0, 0.2),0px 3px 4px 0px rgba(0, 0, 0, 0.14),0px 3px 3px -2px rgba(0, 0, 0, 0.12)", 152 | "0px 2px 4px -1px rgba(0, 0, 0, 0.2),0px 4px 5px 0px rgba(0, 0, 0, 0.14),0px 1px 10px 0px rgba(0, 0, 0, 0.12)", 153 | "0px 3px 5px -1px rgba(0, 0, 0, 0.2),0px 5px 8px 0px rgba(0, 0, 0, 0.14),0px 1px 14px 0px rgba(0, 0, 0, 0.12)", 154 | "0px 3px 5px -1px rgba(0, 0, 0, 0.2),0px 6px 10px 0px rgba(0, 0, 0, 0.14),0px 1px 18px 0px rgba(0, 0, 0, 0.12)", 155 | "0px 4px 5px -2px rgba(0, 0, 0, 0.2),0px 7px 10px 1px rgba(0, 0, 0, 0.14),0px 2px 16px 1px rgba(0, 0, 0, 0.12)", 156 | "0px 5px 5px -3px rgba(0, 0, 0, 0.2),0px 8px 10px 1px rgba(0, 0, 0, 0.14),0px 3px 14px 2px rgba(0, 0, 0, 0.12)", 157 | "0px 5px 6px -3px rgba(0, 0, 0, 0.2),0px 9px 12px 1px rgba(0, 0, 0, 0.14),0px 3px 16px 2px rgba(0, 0, 0, 0.12)", 158 | "0px 6px 6px -3px rgba(0, 0, 0, 0.2),0px 10px 14px 1px rgba(0, 0, 0, 0.14),0px 4px 18px 3px rgba(0, 0, 0, 0.12)", 159 | "0px 6px 7px -4px rgba(0, 0, 0, 0.2),0px 11px 15px 1px rgba(0, 0, 0, 0.14),0px 4px 20px 3px rgba(0, 0, 0, 0.12)", 160 | "0px 7px 8px -4px rgba(0, 0, 0, 0.2),0px 12px 17px 2px rgba(0, 0, 0, 0.14),0px 5px 22px 4px rgba(0, 0, 0, 0.12)", 161 | "0px 7px 8px -4px rgba(0, 0, 0, 0.2),0px 13px 19px 2px rgba(0, 0, 0, 0.14),0px 5px 24px 4px rgba(0, 0, 0, 0.12)", 162 | "0px 7px 9px -4px rgba(0, 0, 0, 0.2),0px 14px 21px 2px rgba(0, 0, 0, 0.14),0px 5px 26px 4px rgba(0, 0, 0, 0.12)", 163 | "0px 8px 9px -5px rgba(0, 0, 0, 0.2),0px 15px 22px 2px rgba(0, 0, 0, 0.14),0px 6px 28px 5px rgba(0, 0, 0, 0.12)", 164 | "0px 8px 10px -5px rgba(0, 0, 0, 0.2),0px 16px 24px 2px rgba(0, 0, 0, 0.14),0px 6px 30px 5px rgba(0, 0, 0, 0.12)", 165 | "0px 8px 11px -5px rgba(0, 0, 0, 0.2),0px 17px 26px 2px rgba(0, 0, 0, 0.14),0px 6px 32px 5px rgba(0, 0, 0, 0.12)", 166 | "0px 9px 11px -5px rgba(0, 0, 0, 0.2),0px 18px 28px 2px rgba(0, 0, 0, 0.14),0px 7px 34px 6px rgba(0, 0, 0, 0.12)", 167 | "0px 9px 12px -6px rgba(0, 0, 0, 0.2),0px 19px 29px 2px rgba(0, 0, 0, 0.14),0px 7px 36px 6px rgba(0, 0, 0, 0.12)", 168 | "0px 10px 13px -6px rgba(0, 0, 0, 0.2),0px 20px 31px 3px rgba(0, 0, 0, 0.14),0px 8px 38px 7px rgba(0, 0, 0, 0.12)", 169 | "0px 10px 13px -6px rgba(0, 0, 0, 0.2),0px 21px 33px 3px rgba(0, 0, 0, 0.14),0px 8px 40px 7px rgba(0, 0, 0, 0.12)", 170 | "0px 10px 14px -6px rgba(0, 0, 0, 0.2),0px 22px 35px 3px rgba(0, 0, 0, 0.14),0px 8px 42px 7px rgba(0, 0, 0, 0.12)", 171 | "0px 11px 14px -7px rgba(0, 0, 0, 0.2),0px 23px 36px 3px rgba(0, 0, 0, 0.14),0px 9px 44px 8px rgba(0, 0, 0, 0.12)", 172 | "0px 11px 15px -7px rgba(0, 0, 0, 0.2),0px 24px 38px 3px rgba(0, 0, 0, 0.14),0px 9px 46px 8px rgba(0, 0, 0, 0.12)", 173 | ], 174 | "shape": Object { 175 | "borderRadius": 4, 176 | }, 177 | "spacing": Object { 178 | "unit": 8, 179 | }, 180 | "transitions": Object { 181 | "create": [Function], 182 | "duration": Object { 183 | "complex": 375, 184 | "enteringScreen": 225, 185 | "leavingScreen": 195, 186 | "short": 250, 187 | "shorter": 200, 188 | "shortest": 150, 189 | "standard": 300, 190 | }, 191 | "easing": Object { 192 | "easeIn": "cubic-bezier(0.4, 0, 1, 1)", 193 | "easeInOut": "cubic-bezier(0.4, 0, 0.2, 1)", 194 | "easeOut": "cubic-bezier(0.0, 0, 0.2, 1)", 195 | "sharp": "cubic-bezier(0.4, 0, 0.6, 1)", 196 | }, 197 | "getAutoHeightDuration": [Function], 198 | }, 199 | "typography": Object { 200 | "body1": Object { 201 | "color": "rgba(0, 0, 0, 0.87)", 202 | "fontFamily": "\\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", 203 | "fontSize": "0.875rem", 204 | "fontWeight": 400, 205 | "lineHeight": "1.46429em", 206 | }, 207 | "body2": Object { 208 | "color": "rgba(0, 0, 0, 0.87)", 209 | "fontFamily": "\\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", 210 | "fontSize": "0.875rem", 211 | "fontWeight": 500, 212 | "lineHeight": "1.71429em", 213 | }, 214 | "button": Object { 215 | "color": "rgba(0, 0, 0, 0.87)", 216 | "fontFamily": "\\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", 217 | "fontSize": "0.875rem", 218 | "fontWeight": 500, 219 | "textTransform": "uppercase", 220 | }, 221 | "caption": Object { 222 | "color": "rgba(0, 0, 0, 0.54)", 223 | "fontFamily": "\\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", 224 | "fontSize": "0.75rem", 225 | "fontWeight": 400, 226 | "lineHeight": "1.375em", 227 | }, 228 | "display1": Object { 229 | "color": "rgba(0, 0, 0, 0.54)", 230 | "fontFamily": "\\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", 231 | "fontSize": "2.125rem", 232 | "fontWeight": 400, 233 | "lineHeight": "1.20588em", 234 | }, 235 | "display2": Object { 236 | "color": "rgba(0, 0, 0, 0.54)", 237 | "fontFamily": "\\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", 238 | "fontSize": "2.8125rem", 239 | "fontWeight": 400, 240 | "lineHeight": "1.13333em", 241 | "marginLeft": "-.02em", 242 | }, 243 | "display3": Object { 244 | "color": "rgba(0, 0, 0, 0.54)", 245 | "fontFamily": "\\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", 246 | "fontSize": "3.5rem", 247 | "fontWeight": 400, 248 | "letterSpacing": "-.02em", 249 | "lineHeight": "1.30357em", 250 | "marginLeft": "-.02em", 251 | }, 252 | "display4": Object { 253 | "color": "rgba(0, 0, 0, 0.54)", 254 | "fontFamily": "\\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", 255 | "fontSize": "7rem", 256 | "fontWeight": 300, 257 | "letterSpacing": "-.04em", 258 | "lineHeight": "1.14286em", 259 | "marginLeft": "-.04em", 260 | }, 261 | "fontFamily": "\\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", 262 | "fontSize": 14, 263 | "fontWeightLight": 300, 264 | "fontWeightMedium": 500, 265 | "fontWeightRegular": 400, 266 | "headline": Object { 267 | "color": "rgba(0, 0, 0, 0.87)", 268 | "fontFamily": "\\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", 269 | "fontSize": "1.5rem", 270 | "fontWeight": 400, 271 | "lineHeight": "1.35417em", 272 | }, 273 | "pxToRem": [Function], 274 | "round": [Function], 275 | "subheading": Object { 276 | "color": "rgba(0, 0, 0, 0.87)", 277 | "fontFamily": "\\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", 278 | "fontSize": "1rem", 279 | "fontWeight": 400, 280 | "lineHeight": "1.5em", 281 | }, 282 | "title": Object { 283 | "color": "rgba(0, 0, 0, 0.87)", 284 | "fontFamily": "\\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", 285 | "fontSize": "1.3125rem", 286 | "fontWeight": 500, 287 | "lineHeight": "1.16667em", 288 | }, 289 | }, 290 | "zIndex": Object { 291 | "appBar": 1100, 292 | "drawer": 1200, 293 | "mobileStepper": 1000, 294 | "modal": 1300, 295 | "snackbar": 1400, 296 | "tooltip": 1500, 297 | }, 298 | } 299 | `; 300 | -------------------------------------------------------------------------------- /src/constants/__test__/theme.test.js: -------------------------------------------------------------------------------- 1 | import theme from "../theme"; 2 | 3 | test("materialUiTheme should match snapshot", () => { 4 | expect(theme).toMatchSnapshot(); 5 | }); 6 | -------------------------------------------------------------------------------- /src/constants/routes.js: -------------------------------------------------------------------------------- 1 | // router path 2 | export const HOME = "/home"; 3 | -------------------------------------------------------------------------------- /src/constants/theme.js: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from "@material-ui/core/styles"; 2 | import indigo from "@material-ui/core//colors/indigo"; 3 | import green from "@material-ui/core/colors/green"; 4 | import red from "@material-ui/core//colors/red"; 5 | 6 | const theme = createMuiTheme({ 7 | palette: { 8 | primary: indigo, 9 | secondary: { 10 | ...green, 11 | A400: "#00e677" 12 | }, 13 | error: red 14 | } 15 | }); 16 | 17 | export default theme; 18 | -------------------------------------------------------------------------------- /src/containers/AppBar/AppBar.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | 3 | import AppBar from "../../components/AppBar/AppBar"; 4 | 5 | const mapStateToProps = state => ({ 6 | shifted: state.ui.sidebar.open 7 | }); 8 | 9 | export default connect(mapStateToProps)(AppBar); 10 | -------------------------------------------------------------------------------- /src/containers/AppBar/IconButton.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | 3 | import { openSidebar } from "../../redux/actions/ui"; 4 | import AppBarIconButton from "../../components/AppBar/IconButton"; 5 | 6 | const mapStateToProps = state => ({ 7 | shifted: state.ui.sidebar.open 8 | }); 9 | 10 | const mapDispatchToProps = { 11 | onClick: openSidebar 12 | }; 13 | 14 | export default connect(mapStateToProps, mapDispatchToProps)(AppBarIconButton); 15 | -------------------------------------------------------------------------------- /src/containers/AppBar/Title.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { push } from "connected-react-router"; 3 | 4 | import AppBarTitle from "../../components/AppBar/Title"; 5 | import { HOME } from "../../constants/routes"; 6 | 7 | const mapDispatchToProps = { 8 | onClick: () => push(HOME) 9 | }; 10 | 11 | export default connect(undefined, mapDispatchToProps)(AppBarTitle); 12 | -------------------------------------------------------------------------------- /src/containers/Layout/Content.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Switch } from "react-router"; 3 | 4 | import Home from "../../components/Home/Home"; 5 | import NoWhere from "../../components/NoWhere/NoWhere"; 6 | import { HOME } from "../../constants/routes"; 7 | 8 | const LayoutContent = () => ( 9 | 10 | 11 | 12 | 13 | ); 14 | 15 | export default LayoutContent; 16 | -------------------------------------------------------------------------------- /src/containers/Layout/Layout.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Switch, Route, Redirect } from "react-router"; 3 | 4 | import LayoutSkeleton from "../../components/Layout/Skeleton"; 5 | import { HOME } from "../../constants/routes"; 6 | 7 | const Layout = () => ( 8 | 9 | } /> 10 | 11 | 12 | ); 13 | 14 | export default Layout; 15 | -------------------------------------------------------------------------------- /src/containers/Layout/__test__/Content.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | import toJson from "enzyme-to-json"; 4 | import { Route, Switch } from "react-router"; 5 | 6 | import LayoutContent from "../Content"; 7 | import Home from "../../../components/Home/Home"; 8 | import NoWhere from "../../../components/NoWhere/NoWhere"; 9 | import { HOME } from "../../../constants/routes"; 10 | 11 | describe("", () => { 12 | let wrapper; 13 | 14 | beforeEach(() => { 15 | wrapper = shallow(); 16 | }); 17 | 18 | it("renders 1 components", () => { 19 | expect(wrapper.find(Switch).length).toEqual(1); 20 | }); 21 | 22 | it("renders 2 components", () => { 23 | expect(wrapper.find(Route).length).toEqual(2); 24 | }); 25 | 26 | it("route 1 renders a components", () => { 27 | expect( 28 | wrapper 29 | .find(Route) 30 | .at(0) 31 | .props().path 32 | ).toEqual(HOME); 33 | expect( 34 | wrapper 35 | .find(Route) 36 | .at(0) 37 | .props().component 38 | ).toEqual(Home); 39 | }); 40 | 41 | it("route 2 renders a components", () => { 42 | expect( 43 | wrapper 44 | .find(Route) 45 | .at(1) 46 | .props().component 47 | ).toEqual(NoWhere); 48 | }); 49 | 50 | it("matches snapshot", () => { 51 | expect(toJson(wrapper)).toMatchSnapshot(); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/containers/Layout/__test__/Layout.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | import toJson from "enzyme-to-json"; 4 | import { Route, Switch } from "react-router"; 5 | 6 | import Layout from "../Layout"; 7 | 8 | describe("", () => { 9 | let wrapper; 10 | 11 | beforeEach(() => { 12 | wrapper = shallow(); 13 | }); 14 | 15 | it("renders 1 components", () => { 16 | expect(wrapper.find(Switch).length).toEqual(1); 17 | }); 18 | 19 | it("renders 2 components", () => { 20 | expect(wrapper.find(Route).length).toEqual(2); 21 | }); 22 | 23 | it("matches snapshot", () => { 24 | expect(toJson(wrapper)).toMatchSnapshot(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/containers/Layout/__test__/__snapshots__/Content.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` matches snapshot 1`] = ` 4 | 5 | 10 | 13 | 14 | `; 15 | -------------------------------------------------------------------------------- /src/containers/Layout/__test__/__snapshots__/Layout.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` matches snapshot 1`] = ` 4 | 5 | 10 | 13 | 14 | `; 15 | -------------------------------------------------------------------------------- /src/containers/NoWhere/HomeButton.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { push } from "connected-react-router"; 3 | 4 | import NoWhereHomeButton from "../../components/NoWhere/HomeButton"; 5 | import { HOME } from "../../constants/routes"; 6 | 7 | const mapDispatchToProps = dispatch => ({ 8 | onClick: () => dispatch(push(HOME)) 9 | }); 10 | 11 | export default connect( 12 | undefined, 13 | mapDispatchToProps 14 | )(NoWhereHomeButton); 15 | -------------------------------------------------------------------------------- /src/containers/Sidebar/Header.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | 3 | import { closeSidebar } from "../../redux/actions/ui"; 4 | 5 | import SidebarHeader from "../../components/Sidebar/Header"; 6 | 7 | const mapDispatchToProps = { 8 | onClick: closeSidebar 9 | }; 10 | 11 | export default connect(undefined, mapDispatchToProps)(SidebarHeader); 12 | -------------------------------------------------------------------------------- /src/containers/Sidebar/HomeItem.js: -------------------------------------------------------------------------------- 1 | // Libs 2 | import { connect } from "react-redux"; 3 | import { push } from "connected-react-router"; 4 | // Component 5 | import SidebarHomeItem from "../../components/Sidebar/HomeItem"; 6 | // Constants 7 | import { HOME } from "../../constants/routes"; 8 | // Selectors 9 | import { isItemSelected } from "../../redux/selectors/ui"; 10 | 11 | const mapStateToProps = state => ({ 12 | selected: isItemSelected(state) 13 | }); 14 | 15 | const mapDispatchToProps = { 16 | onClick: () => push(HOME) 17 | }; 18 | 19 | export default connect( 20 | mapStateToProps, 21 | mapDispatchToProps 22 | )(SidebarHomeItem); 23 | -------------------------------------------------------------------------------- /src/containers/Sidebar/Sidebar.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | 3 | import { closeSidebar } from "../../redux/actions/ui"; 4 | 5 | import Sidebar from "../../components/Sidebar/Sidebar"; 6 | 7 | const mapStateToProps = (state, ownProps) => ({ 8 | open: state.ui.sidebar.open 9 | }); 10 | 11 | const mapDispatchToProps = { 12 | onClose: closeSidebar 13 | }; 14 | 15 | export default connect(mapStateToProps, mapDispatchToProps)(Sidebar); 16 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | import App from "./App"; 5 | import registerServiceWorker from "./utils/registerServiceWorker"; 6 | 7 | ReactDOM.render(, document.getElementById("root")); 8 | registerServiceWorker(); 9 | -------------------------------------------------------------------------------- /src/redux/actions/__tests__/ui.test.js: -------------------------------------------------------------------------------- 1 | import { openSidebar, closeSidebar, OPEN_SIDEBAR, CLOSE_SIDEBAR } from "../ui"; 2 | 3 | describe("openSidebar", () => { 4 | it("returns an action of type $`{OPEN_SIDE_BAR}`", () => { 5 | expect(openSidebar().type).toEqual(OPEN_SIDEBAR); 6 | }); 7 | }); 8 | 9 | describe("openSidebar", () => { 10 | it("return an action of type $`{CLOSE_SIDE_BAR}`", () => { 11 | expect(closeSidebar().type).toEqual(CLOSE_SIDEBAR); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/redux/actions/ui.js: -------------------------------------------------------------------------------- 1 | export const OPEN_SIDEBAR = "OPEN_SIDEBAR"; 2 | export const openSidebar = () => ({ 3 | type: OPEN_SIDEBAR 4 | }); 5 | 6 | export const CLOSE_SIDEBAR = "CLOSE_SIDEBAR"; 7 | export const closeSidebar = () => ({ 8 | type: CLOSE_SIDEBAR 9 | }); 10 | -------------------------------------------------------------------------------- /src/redux/enhancers/index.js: -------------------------------------------------------------------------------- 1 | import compose from "./reduxDevtools"; 2 | import middlewares from "./middlewares"; 3 | 4 | export default compose(middlewares); 5 | -------------------------------------------------------------------------------- /src/redux/enhancers/middlewares/index.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware } from "redux"; 2 | 3 | import router from "./router"; 4 | 5 | const middlewares = [ 6 | router 7 | // other middlewares go here 8 | ]; 9 | export default applyMiddleware(...middlewares); 10 | -------------------------------------------------------------------------------- /src/redux/enhancers/middlewares/router.js: -------------------------------------------------------------------------------- 1 | import { routerMiddleware } from "connected-react-router"; 2 | import { createBrowserHistory } from "history"; 3 | 4 | // Configure connected-react-router 5 | export const history = createBrowserHistory(); 6 | 7 | export default routerMiddleware(history); 8 | -------------------------------------------------------------------------------- /src/redux/enhancers/reduxDevtools.js: -------------------------------------------------------------------------------- 1 | import { compose as _compose } from "redux"; 2 | 3 | // Enables redux-devtools extension 4 | const compose = 5 | process.env.NODE_ENV === "development" 6 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || _compose 7 | : _compose; 8 | 9 | export default compose; 10 | -------------------------------------------------------------------------------- /src/redux/reducers/data/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | 3 | export default combineReducers({ 4 | // data reducers go here 5 | }); 6 | -------------------------------------------------------------------------------- /src/redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import { connectRouter } from "connected-react-router"; 3 | 4 | import { history } from "../enhancers/middlewares/router"; 5 | import ui from "./ui"; 6 | // import status from "./status"; 7 | // import data from "./data"; 8 | 9 | export default combineReducers({ 10 | router: connectRouter(history), 11 | ui 12 | // status, 13 | // data 14 | }); 15 | -------------------------------------------------------------------------------- /src/redux/reducers/status/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | 3 | export default combineReducers({ 4 | // status reducers go here 5 | }); 6 | -------------------------------------------------------------------------------- /src/redux/reducers/ui/__test__/__snapshots__/sidebar.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`sidebarReducer On CLOSE_SIDEBAR state matches snapshot 1`] = ` 4 | Object { 5 | "open": false, 6 | } 7 | `; 8 | 9 | exports[`sidebarReducer On OPEN_SIDEBAR state matches snapshot 1`] = ` 10 | Object { 11 | "open": true, 12 | } 13 | `; 14 | 15 | exports[`sidebarReducer On default state matches snapshot 1`] = ` 16 | Object { 17 | "open": false, 18 | } 19 | `; 20 | 21 | exports[`sidebarReducer default state match 1`] = ` 22 | Object { 23 | "open": false, 24 | } 25 | `; 26 | -------------------------------------------------------------------------------- /src/redux/reducers/ui/__test__/sidebar.test.js: -------------------------------------------------------------------------------- 1 | import { OPEN_SIDEBAR, CLOSE_SIDEBAR } from "../../../actions/ui"; 2 | import sidebarReducer from "../sidebar"; 3 | 4 | describe("sidebarReducer", () => { 5 | let state = {}; 6 | 7 | it("default state match", () => { 8 | expect(sidebarReducer(undefined, {})).toMatchSnapshot(); 9 | }); 10 | 11 | describe(`On ${OPEN_SIDEBAR}`, () => { 12 | beforeEach(() => { 13 | state = sidebarReducer({ open: false }, { type: OPEN_SIDEBAR }); 14 | }); 15 | 16 | it("open=true", () => { 17 | expect(state.open).toEqual(true); 18 | }); 19 | 20 | it("state matches snapshot", () => { 21 | expect(state).toMatchSnapshot(); 22 | }); 23 | }); 24 | 25 | describe(`On ${CLOSE_SIDEBAR}`, () => { 26 | beforeEach(() => { 27 | state = sidebarReducer({ open: true }, { type: CLOSE_SIDEBAR }); 28 | }); 29 | 30 | it("open=false", () => { 31 | expect(state.open).toEqual(false); 32 | }); 33 | 34 | it("state matches snapshot", () => { 35 | expect(state).toMatchSnapshot(); 36 | }); 37 | }); 38 | 39 | describe("On default", () => { 40 | beforeEach(() => { 41 | state = sidebarReducer(undefined, { type: undefined }); 42 | }); 43 | 44 | it("state matches snapshot", () => { 45 | expect(state).toMatchSnapshot(); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/redux/reducers/ui/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | 3 | import sidebar from "./sidebar"; 4 | 5 | export default combineReducers({ 6 | sidebar 7 | // other ui reducers go here 8 | }); 9 | -------------------------------------------------------------------------------- /src/redux/reducers/ui/sidebar.js: -------------------------------------------------------------------------------- 1 | import { OPEN_SIDEBAR, CLOSE_SIDEBAR } from "../../actions/ui"; 2 | 3 | const initialState = { 4 | open: false 5 | }; 6 | 7 | export default (state = initialState, { type }) => { 8 | switch (type) { 9 | case OPEN_SIDEBAR: 10 | return { 11 | ...state, 12 | open: true 13 | }; 14 | 15 | case CLOSE_SIDEBAR: 16 | return { 17 | ...state, 18 | open: false 19 | }; 20 | 21 | default: 22 | return state; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/redux/selectors/data.js: -------------------------------------------------------------------------------- 1 | // Libs 2 | import { createSelector } from "reselect"; 3 | -------------------------------------------------------------------------------- /src/redux/selectors/status.js: -------------------------------------------------------------------------------- 1 | // Libs 2 | import { createSelector } from "reselect"; 3 | -------------------------------------------------------------------------------- /src/redux/selectors/ui.js: -------------------------------------------------------------------------------- 1 | // Libs 2 | import { matchPath } from "react-router"; 3 | import { createSelector } from "reselect"; 4 | 5 | // Constants 6 | import { HOME } from "../../constants/routes"; 7 | 8 | // Item selection selector 9 | const getPathName = (state, props) => state.router.location.pathname; 10 | export const isItemSelected = createSelector( 11 | getPathName, 12 | pathname => !!matchPath(pathname, { path: HOME }) 13 | ); 14 | -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | 3 | import _reducer from "./reducers"; 4 | import _enhancer from "./enhancers"; 5 | 6 | export default ( 7 | reducer = _reducer, 8 | preloadedState = undefined, 9 | enhancer = _enhancer 10 | ) => createStore(reducer, preloadedState, enhancer); 11 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // Enzyme setup 2 | import { configure } from "enzyme"; 3 | import Adapter from "enzyme-adapter-react-16"; 4 | 5 | configure({ adapter: new Adapter() }); 6 | -------------------------------------------------------------------------------- /src/tests-utils/__test__/createShallow.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import classNames from "classnames"; 3 | 4 | import { withStyles } from "@material-ui/core/styles"; 5 | import IconButton from "@material-ui/core/IconButton"; 6 | import MenuIcon from "@material-ui/icons/Menu"; 7 | 8 | import createShallow from "../createShallow"; 9 | 10 | const TestComponent = ({ classes }) => ( 11 | 12 | 13 | 14 | ); 15 | 16 | const styles = theme => ({ 17 | iconButton: { 18 | marginLeft: 12, 19 | marginRight: 36 20 | } 21 | }); 22 | 23 | const Component = withStyles(styles)(TestComponent); 24 | 25 | describe("createShallow", () => { 26 | it("does not dive with dive=0", () => { 27 | const shallow = createShallow({ dive: 0 }); 28 | const wrapper = shallow(); 29 | expect(wrapper.name()).toEqual("TestComponent"); 30 | }); 31 | 32 | it("dives with dive=1", () => { 33 | const shallow = createShallow({ dive: 1 }); 34 | const wrapper = shallow(); 35 | expect(wrapper.name()).toEqual("WithStyles(IconButton)"); 36 | }); 37 | 38 | it("dives with dive=2", () => { 39 | const shallow = createShallow({ dive: 2 }); 40 | const wrapper = shallow(); 41 | expect(wrapper.name()).toEqual("IconButton"); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/tests-utils/createShallow.js: -------------------------------------------------------------------------------- 1 | import { shallow as _shallow } from "enzyme"; 2 | 3 | const createShallow = ({ dive }) => { 4 | const shallow = (node, options) => { 5 | let wrapper = _shallow(node, options); 6 | 7 | for (let i = 0; i < dive; i++) { 8 | if (wrapper.name().match(/^WithWidth(.+)$/)) { 9 | // Diving through a Meterial-UI WidthWith component can not be performed 10 | // by default because of React.Fragment used in withWidth 11 | // It is a known issue: https://github.com/airbnb/enzyme/issues/1213 12 | wrapper = wrapper 13 | .dive() 14 | .children() 15 | .at(0); 16 | } else { 17 | wrapper = wrapper.dive(); 18 | } 19 | } 20 | return wrapper; 21 | }; 22 | 23 | return shallow; 24 | }; 25 | 26 | export default createShallow; 27 | -------------------------------------------------------------------------------- /src/utils/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === "localhost" || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === "[::1]" || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener("load", () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | } else { 39 | // Is not local host. Just register service worker 40 | registerValidSW(swUrl); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | function registerValidSW(swUrl) { 47 | navigator.serviceWorker 48 | .register(swUrl) 49 | .then(registration => { 50 | registration.onupdatefound = () => { 51 | const installingWorker = registration.installing; 52 | installingWorker.onstatechange = () => { 53 | if (installingWorker.state === "installed") { 54 | if (navigator.serviceWorker.controller) { 55 | // At this point, the old content will have been purged and 56 | // the fresh content will have been added to the cache. 57 | // It's the perfect time to display a "New content is 58 | // available; please refresh." message in your web app. 59 | console.log( 60 | "New content is available; please refresh." 61 | ); 62 | } else { 63 | // At this point, everything has been precached. 64 | // It's the perfect time to display a 65 | // "Content is cached for offline use." message. 66 | console.log("Content is cached for offline use."); 67 | } 68 | } 69 | }; 70 | }; 71 | }) 72 | .catch(error => { 73 | console.error("Error during service worker registration:", error); 74 | }); 75 | } 76 | 77 | function checkValidServiceWorker(swUrl) { 78 | // Check if the service worker can be found. If it can't reload the page. 79 | fetch(swUrl) 80 | .then(response => { 81 | // Ensure service worker exists, and that we really are getting a JS file. 82 | if ( 83 | response.status === 404 || 84 | response.headers.get("content-type").indexOf("javascript") === 85 | -1 86 | ) { 87 | // No service worker found. Probably a different app. Reload the page. 88 | navigator.serviceWorker.ready.then(registration => { 89 | registration.unregister().then(() => { 90 | window.location.reload(); 91 | }); 92 | }); 93 | } else { 94 | // Service worker found. Proceed as normal. 95 | registerValidSW(swUrl); 96 | } 97 | }) 98 | .catch(() => { 99 | console.log( 100 | "No internet connection found. App is running in offline mode." 101 | ); 102 | }); 103 | } 104 | 105 | export function unregister() { 106 | if ("serviceWorker" in navigator) { 107 | navigator.serviceWorker.ready.then(registration => { 108 | registration.unregister(); 109 | }); 110 | } 111 | } 112 | --------------------------------------------------------------------------------