├── .babelrc ├── .circleci └── config.yml ├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── .storybook ├── config.js └── webpack.config.js ├── README.md ├── __test__ ├── capsules.controllers.spec.js ├── connect-db.spec.js └── users.controllers.spec.js ├── api ├── cluster.js ├── config │ └── express.js ├── controllers │ ├── Capsules.js │ └── Users.js ├── index.js ├── models │ ├── Capsules.js │ ├── Users.js │ ├── config.js │ └── db.js ├── routers │ ├── checkAuth.js │ └── index.js └── services │ ├── logger.js │ └── memcachedClient.js ├── package-lock.json ├── package.json ├── public ├── .DS_Store └── src │ ├── .DS_Store │ ├── App.js │ ├── RestCSS.js │ ├── actions │ └── actionCreator.js │ ├── components │ ├── authenticated │ │ └── Authenticated.js │ ├── box │ │ ├── Box.js │ │ ├── __snapshots__ │ │ │ └── box.spec.js.snap │ │ ├── box.spec.js │ │ └── box.story.js │ ├── button │ │ ├── Button.js │ │ ├── __snapshots__ │ │ │ └── button.spec.js.snap │ │ ├── button.spec.js │ │ └── button.story.js │ ├── container │ │ ├── Container.js │ │ ├── __snapshots__ │ │ │ └── container.spec.js.snap │ │ ├── container.spec.js │ │ └── container.story.js │ ├── create │ │ └── Create.js │ ├── edit │ │ └── Edit.js │ ├── header │ │ ├── Header.js │ │ └── header.css.js │ ├── history │ │ └── index.js │ ├── input │ │ ├── Input.js │ │ ├── __snapshots__ │ │ │ └── input.spec.js.snap │ │ ├── input.spec.js │ │ └── input.story.js │ ├── list │ │ └── List.js │ ├── login │ │ └── Login.js │ ├── logout │ │ └── Logout.js │ ├── message │ │ ├── Message.js │ │ ├── __snapshots__ │ │ │ └── message.spec.js.snap │ │ ├── message.spec.js │ │ └── message.story.js │ ├── notfound │ │ ├── NotFound.js │ │ ├── __snapshots__ │ │ │ └── notfound.spec.js.snap │ │ ├── notfound.spec.js │ │ └── notfound.story.js │ └── title │ │ ├── Title.js │ │ ├── __snapshots__ │ │ └── title.spec.js.snap │ │ ├── title.spec.js │ │ └── title.story.js │ ├── css │ └── inline.js │ ├── html │ └── template.html │ ├── images │ ├── .DS_Store │ ├── favicon.ico │ └── logo.png │ ├── index.js │ ├── logics │ └── CaffeineApi.js │ └── reducers │ ├── capsules.js │ └── message.js ├── stories └── index.js ├── webpack-start.js └── webpack ├── common.js ├── dev.config.js └── prod.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | "@babel/plugin-proposal-class-properties" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | aliases: 4 | - &restore-node-modules-cache 5 | name: Restore node_modules cache 6 | key: v1-npm-deps-{{ checksum "package-lock.json" }} 7 | 8 | - &save-npm-cache 9 | name: Save npm cache 10 | paths: 11 | - ~/.cache/npm 12 | key: v1-npm-cache 13 | 14 | - &save-node-modules-cache 15 | name: Save node_modules cache 16 | paths: 17 | - node_modules 18 | key: v1-npm-deps-{{ checksum "package-lock.json" }} 19 | 20 | defaults: &defaults 21 | docker: 22 | - image: node:16.6.0-stretch-slim 23 | working_directory: ~/caffeine 24 | 25 | jobs: 26 | build_packages: 27 | <<: *defaults 28 | 29 | steps: 30 | - checkout 31 | - restore_cache: *restore-node-modules-cache 32 | 33 | - run: 34 | name: Install Dependencies 35 | command: npm install --frozen-lockfile 36 | 37 | - run: 38 | name: Build Packages 39 | command: npm run build 40 | 41 | - save_cache: *save-node-modules-cache 42 | - save_cache: *save-npm-cache 43 | - persist_to_workspace: 44 | root: '.' 45 | paths: 46 | - '*' 47 | 48 | lint_test: 49 | <<: *defaults 50 | 51 | steps: 52 | - attach_workspace: 53 | at: ~/caffeine 54 | 55 | - run: 56 | name: Run Lint 57 | command: npm run lint 58 | 59 | unit_test_ui: 60 | <<: *defaults 61 | 62 | steps: 63 | - attach_workspace: 64 | at: ~/caffeine 65 | 66 | - run: 67 | name: Run Unit Tests 68 | command: npm run test:ci:ui 69 | 70 | workflows: 71 | version: 2 72 | 73 | branch-workflow: 74 | jobs: 75 | - build_packages 76 | - lint_test: 77 | requires: 78 | - build_packages 79 | - unit_test_ui: 80 | requires: 81 | - build_packages 82 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2020: true, 5 | node: true, 6 | "jest/globals": true, 7 | }, 8 | extends: ["eslint:recommended", "plugin:react/recommended"], 9 | parserOptions: { 10 | ecmaFeatures: { 11 | jsx: true, 12 | }, 13 | ecmaVersion: 11, 14 | sourceType: "module", 15 | }, 16 | plugins: ["react", "jest"], 17 | rules: { 18 | "react/prop-types": 0, 19 | }, 20 | settings: { 21 | react: { 22 | version: "detect" 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /.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 (http://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 | # dotenv environment variables file 58 | .env 59 | 60 | public/dist 61 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.6.1 2 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | const req = require.context('../public/src/components', true, /\.story\.js$/); 4 | 5 | function loadStories() { 6 | req.keys().forEach(filename => req(filename)); 7 | } 8 | 9 | configure(loadStories, module); 10 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | // you can use this file to add your custom webpack plugins, loaders and anything you like. 2 | // This is just the basic way to add additional webpack configurations. 3 | // For more information refer the docs: https://storybook.js.org/configurations/custom-webpack-config 4 | 5 | // IMPORTANT 6 | // When you add this file, we won't add the default configurations which is similar 7 | // to "React Create App". This only has babel loader to load JavaScript. 8 | const path = require('path'); 9 | 10 | module.exports = { 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.scss$/, 15 | loaders: ["style-loader", "css-loader", "sass-loader"], 16 | include: path.resolve(__dirname, '../') 17 | } 18 | ] 19 | } 20 | } 21 | 22 | // module.exports = { 23 | // plugins: [ 24 | // // your custom plugins 25 | // ], 26 | // module: { 27 | // loaders: [ 28 | // // add your custom loaders. 29 | // ], 30 | // }, 31 | // }; 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Caffeine - Redux 2 | 3 | [![caffeine redux](https://circleci.com/gh/nathpaiva/caffeine-redux.svg)](https://circleci.com/gh/nathpaiva/caffeine-redux) 4 | [![time tracker](https://wakatime.com/badge/github/nathpaiva/caffeine-redux.svg)](https://wakatime.com/badge/github/nathpaiva/caffeine-redux) 5 | 6 | It is a study project of registering coffee capsules, saving the brand, type, price of purchase, how many capsules are cosumed in the week and when you want to be notify when it is at the end of the box. 7 | 8 | The idea is to implement a notification of when you are finishing the capsule for the registration of each registered box. 9 | 10 | ## Project developed with: 11 | 12 | - Node, 13 | - Express, 14 | - Mongodb e 15 | - React 16 | 17 | ## Test developed with: 18 | 19 | - Jest, (back-end e front-end) 20 | - Storybook - Viewing Components 21 | 22 | ## To run the project you need to install: 23 | 24 | - [Node](https://nodejs.org/en/download/) 25 | - NPM (already installed with the node) 26 | - [Mongodb](https://docs.mongodb.com/manual/installation/) 27 | 28 | ## To start it is necessary to rotate the mongo locally: 29 | 30 | - [mongod](https://docs.mongodb.com/manual/tutorial/manage-mongodb-processes/) 31 | 32 | ## Before starting the project you must install the dependencies: 33 | 34 | ```bash 35 | npm install 36 | ``` 37 | 38 | ## To start the project you have to run the commands: 39 | 40 | ```bash 41 | npm start 42 | ``` 43 | 44 | - This command to access to: 45 | 46 | - [http://localhost:2000](http://localhost:2000), starting the front-end 47 | 48 | - [http://localhost:3000](http://localhost:2000), starting the backend 49 | 50 | ## If you want to see the test coverage: 51 | 52 | ```bash 53 | npm run test 54 | ``` 55 | 56 | ## To be able to see the components in Storybook: 57 | 58 | ```bash 59 | npm run storybook 60 | ``` 61 | 62 | - This command to access to: 63 | - [http://localhost:6006/](http://localhost:6006/) 64 | -------------------------------------------------------------------------------- /__test__/capsules.controllers.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const express = require('../api/config/express')(); 3 | const request = require('supertest')(express); 4 | const CapsulesDB = require('../api/models/Capsules'); 5 | require('../api/models/db').connection('test'); 6 | 7 | describe('#Capsules Controller', () => { 8 | 9 | beforeEach(async () => { 10 | await CapsulesDB.remove().exec(); 11 | await request.post('/api/createuser') 12 | .set('Accept', 'application/json') 13 | .send({ 14 | user_name: 'nath', 15 | user_mail: 'nath@nath.com.br', 16 | password: 'teste1' 17 | }); 18 | }); 19 | 20 | it('#Load all capsules by user', async () => { 21 | const login = await request.post('/api/login') 22 | .set('Accept', 'application/json') 23 | .send({ 24 | user_name: 'nath', 25 | password: 'teste1' 26 | }); 27 | 28 | const res = await request.get(`/api/auth/capsules/${login.body.user._id}`) 29 | .set('Accept', 'application/json') 30 | .set('x-access-token', login.body.token); 31 | 32 | const isArrayResult = Array.isArray(res.body.capsules); 33 | expect(isArrayResult).equal(true); 34 | }); 35 | 36 | describe('#Create Capsules', () => { 37 | it('#Create capsule by user', async () => { 38 | const login = await request.post('/api/login') 39 | .set('Accept', 'application/json') 40 | .send({ 41 | user_name: 'nath', 42 | password: 'teste1' 43 | }); 44 | 45 | const res = await request.post(`/api/auth/capsules/${login.body.user._id}`) 46 | .set('Accept', 'application/json') 47 | .set('x-access-token', login.body.token) 48 | .send({ 49 | user_name: login.body.user.user_name, 50 | user_id: login.body.user._id, 51 | type_capsule: 'Intenso', 52 | brand_capsule: 'Nespresso', 53 | notify_enf_capsules: 4, 54 | quantity_capsules_per_week: 5, 55 | price_last_buy: 20 56 | }); 57 | 58 | expect(res.body.success).equal(true); 59 | }); 60 | 61 | it('#Error to create capsule by user, empty user_name', async () => { 62 | const login = await request.post('/api/login') 63 | .set('Accept', 'application/json') 64 | .send({ 65 | user_name: 'nath', 66 | password: 'teste1' 67 | }); 68 | 69 | const res = await request.post(`/api/auth/capsules/${login.body.user._id}`) 70 | .set('Accept', 'application/json') 71 | .set('x-access-token', login.body.token) 72 | .send({ 73 | user_name: '', 74 | user_id: login.body.user._id, 75 | type_capsule: 'Intenso', 76 | brand_capsule: 'Nespresso', 77 | notify_enf_capsules: 4, 78 | quantity_capsules_per_week: 5, 79 | price_last_buy: 20 80 | }); 81 | 82 | expect(res.body.success).equal(false); 83 | }); 84 | 85 | it('#Error to create capsule by user, empty user_id', async () => { 86 | const login = await request.post('/api/login') 87 | .set('Accept', 'application/json') 88 | .send({ 89 | user_name: 'nath', 90 | password: 'teste1' 91 | }); 92 | 93 | const res = await request.post(`/api/auth/capsules/${login.body.user._id}`) 94 | .set('Accept', 'application/json') 95 | .set('x-access-token', login.body.token) 96 | .send({ 97 | user_name: login.body.user.user_name, 98 | user_id: '', 99 | type_capsule: 'Intenso', 100 | brand_capsule: 'Nespresso', 101 | notify_enf_capsules: 4, 102 | quantity_capsules_per_week: 5, 103 | price_last_buy: 20 104 | }); 105 | 106 | expect(res.body.success).equal(false); 107 | }); 108 | 109 | it('#Error to create capsule by user, empty quantity_capsules_per_week', async () => { 110 | const login = await request.post('/api/login') 111 | .set('Accept', 'application/json') 112 | .send({ 113 | user_name: 'nath', 114 | password: 'teste1' 115 | }); 116 | 117 | const res = await request.post(`/api/auth/capsules/${login.body.user._id}`) 118 | .set('Accept', 'application/json') 119 | .set('x-access-token', login.body.token) 120 | .send({ 121 | user_name: login.body.user.user_name, 122 | user_id: login.body.user._id, 123 | type_capsule: 'Intenso', 124 | brand_capsule: 'Nespresso', 125 | notify_enf_capsules: 4, 126 | price_last_buy: 20 127 | }); 128 | 129 | expect(res.body.success).equal(false); 130 | }); 131 | 132 | it('#Error to create capsule by user, empty notify_enf_capsules', async () => { 133 | const login = await request.post('/api/login') 134 | .set('Accept', 'application/json') 135 | .send({ 136 | user_name: 'nath', 137 | password: 'teste1' 138 | }); 139 | 140 | const res = await request.post(`/api/auth/capsules/${login.body.user._id}`) 141 | .set('Accept', 'application/json') 142 | .set('x-access-token', login.body.token) 143 | .send({ 144 | user_name: login.body.user_name, 145 | user_id: login.body.user._id, 146 | type_capsule: 'Intenso', 147 | brand_capsule: 'Nespresso', 148 | quantity_capsules_per_week: 5, 149 | price_last_buy: 20 150 | }); 151 | 152 | expect(res.body.success).equal(false); 153 | }); 154 | }); 155 | 156 | describe('#Edit Capsules', () => { 157 | it('#Edit capsule by id', async () => { 158 | const login = await request.post('/api/login') 159 | .set('Accept', 'application/json') 160 | .send({ 161 | user_name: 'nath', 162 | password: 'teste1' 163 | }); 164 | 165 | const newCapsule = await request.post(`/api/auth/capsules/${login.body.user._id}`) 166 | .set('Accept', 'application/json') 167 | .set('x-access-token', login.body.token) 168 | .send({ 169 | user_name: login.body.user.user_name, 170 | user_id: login.body.user._id, 171 | type_capsule: 'Intenso', 172 | brand_capsule: 'Nespresso', 173 | notify_enf_capsules: 4, 174 | quantity_capsules_per_week: 5, 175 | price_last_buy: 20 176 | }); 177 | 178 | const res = await request.put(`/api/auth/capsule/${newCapsule.body.data._id}`) 179 | .set('Accept', 'application/json') 180 | .set('x-access-token', login.body.token) 181 | .send({ 182 | user_name: login.body.user.user_name, 183 | user_id: login.body.user._id, 184 | type_capsule: 'Intenso', 185 | brand_capsule: 'Nespresso', 186 | notify_enf_capsules: 4, 187 | quantity_capsules_per_week: 9, 188 | price_last_buy: 29 189 | }); 190 | 191 | expect(res.body.success).equal(true); 192 | }); 193 | 194 | it('#Error to edit capsule by id - without user_name', async () => { 195 | const login = await request.post('/api/login') 196 | .set('Accept', 'application/json') 197 | .send({ 198 | user_name: 'nath', 199 | password: 'teste1' 200 | }); 201 | 202 | const newCapsule = await request.post(`/api/auth/capsules/${login.body.user._id}`) 203 | .set('Accept', 'application/json') 204 | .set('x-access-token', login.body.token) 205 | .send({ 206 | user_name: login.body.user.user_name, 207 | user_id: login.body.user._id, 208 | type_capsule: 'Intenso', 209 | brand_capsule: 'Nespresso', 210 | notify_enf_capsules: 4, 211 | quantity_capsules_per_week: 5, 212 | price_last_buy: 20 213 | }); 214 | 215 | const res = await request.put(`/api/auth/capsule/${newCapsule.body.data._id}`) 216 | .set('Accept', 'application/json') 217 | .set('x-access-token', login.body.token) 218 | .send({ 219 | user_id: login.body.user._id, 220 | type_capsule: 'Intenso', 221 | brand_capsule: 'Nespresso', 222 | notify_enf_capsules: 4, 223 | quantity_capsules_per_week: 9, 224 | price_last_buy: 29 225 | }); 226 | 227 | expect(res.body.success).equal(false); 228 | }); 229 | 230 | it('#Error to edit capsule by id - without user_id', async () => { 231 | const login = await request.post('/api/login') 232 | .set('Accept', 'application/json') 233 | .send({ 234 | user_name: 'nath', 235 | password: 'teste1' 236 | }); 237 | 238 | const newCapsule = await request.post(`/api/auth/capsules/${login.body.user._id}`) 239 | .set('Accept', 'application/json') 240 | .set('x-access-token', login.body.token) 241 | .send({ 242 | user_name: login.body.user.user_name, 243 | user_id: login.body.user._id, 244 | type_capsule: 'Intenso', 245 | brand_capsule: 'Nespresso', 246 | notify_enf_capsules: 4, 247 | quantity_capsules_per_week: 5, 248 | price_last_buy: 20 249 | }); 250 | 251 | const res = await request.put(`/api/auth/capsule/${newCapsule.body.data._id}`) 252 | .set('Accept', 'application/json') 253 | .set('x-access-token', login.body.token) 254 | .send({ 255 | user_name: login.body.user.user_name, 256 | type_capsule: 'Intenso', 257 | brand_capsule: 'Nespresso', 258 | notify_enf_capsules: 4, 259 | quantity_capsules_per_week: 9, 260 | price_last_buy: 29 261 | }); 262 | 263 | expect(res.body.success).equal(false); 264 | }); 265 | 266 | it('#Error to edit capsule by id - without notify_enf_capsules', async () => { 267 | const login = await request.post('/api/login') 268 | .set('Accept', 'application/json') 269 | .send({ 270 | user_name: 'nath', 271 | password: 'teste1' 272 | }); 273 | 274 | const newCapsule = await request.post(`/api/auth/capsules/${login.body.user._id}`) 275 | .set('Accept', 'application/json') 276 | .set('x-access-token', login.body.token) 277 | .send({ 278 | user_name: login.body.user.user_name, 279 | user_id: login.body.user._id, 280 | type_capsule: 'Intenso', 281 | brand_capsule: 'Nespresso', 282 | notify_enf_capsules: 4, 283 | quantity_capsules_per_week: 5, 284 | price_last_buy: 20 285 | }); 286 | 287 | const res = await request.put(`/api/auth/capsule/${newCapsule.body.data._id}`) 288 | .set('Accept', 'application/json') 289 | .set('x-access-token', login.body.token) 290 | .send({ 291 | type_capsule: 'Intenso', 292 | brand_capsule: 'Nespresso', 293 | user_name: login.body.user.user_name, 294 | quantity_capsules_per_week: 9, 295 | price_last_buy: 29 296 | }); 297 | 298 | expect(res.body.success).equal(false); 299 | }); 300 | 301 | it('#Error to edit capsule by id - without quantity_capsules_per_week', async () => { 302 | const login = await request.post('/api/login') 303 | .set('Accept', 'application/json') 304 | .send({ 305 | user_name: 'nath', 306 | password: 'teste1' 307 | }); 308 | 309 | const newCapsule = await request.post(`/api/auth/capsules/${login.body.user._id}`) 310 | .set('Accept', 'application/json') 311 | .set('x-access-token', login.body.token) 312 | .send({ 313 | user_name: login.body.user.user_name, 314 | user_id: login.body.user._id, 315 | type_capsule: 'Intenso', 316 | brand_capsule: 'Nespresso', 317 | notify_enf_capsules: 4, 318 | quantity_capsules_per_week: 5, 319 | price_last_buy: 20 320 | }); 321 | 322 | const res = await request.put(`/api/auth/capsule/${newCapsule.body.data._id}`) 323 | .set('Accept', 'application/json') 324 | .set('x-access-token', login.body.token) 325 | .send({ 326 | type_capsule: 'Intenso', 327 | brand_capsule: 'Nespresso', 328 | user_name: login.body.user.user_name, 329 | notify_enf_capsules: 4, 330 | price_last_buy: 29 331 | }); 332 | 333 | expect(res.body.success).equal(false); 334 | }); 335 | }); 336 | 337 | 338 | describe('#Delete Capsules', () => { 339 | 340 | it('#Delete capsule', async () => { 341 | const login = await request.post('/api/login') 342 | .set('Accept', 'application/json') 343 | .send({ 344 | user_name: 'nath', 345 | password: 'teste1' 346 | }); 347 | 348 | const newCapsule = await request.post(`/api/auth/capsules/${login.body.user._id}`) 349 | .set('Accept', 'application/json') 350 | .set('x-access-token', login.body.token) 351 | .send({ 352 | user_name: login.body.user.user_name, 353 | user_id: login.body.user._id, 354 | type_capsule: 'Intenso', 355 | brand_capsule: 'Nespresso', 356 | notify_enf_capsules: 4, 357 | quantity_capsules_per_week: 5, 358 | price_last_buy: 20 359 | }); 360 | 361 | const res = await request.delete(`/api/auth/capsules/${login.body.user._id}/${newCapsule.body.data._id}`) 362 | .set('Accept', 'application/json') 363 | .set('x-access-token', login.body.token); 364 | 365 | expect(res.body.success).equal(true); 366 | }); 367 | 368 | it('#Erro when Delete capsule without id of capsule in url', async () => { 369 | const login = await request.post('/api/login') 370 | .set('Accept', 'application/json') 371 | .send({ 372 | user_name: 'nath', 373 | password: 'teste1' 374 | }); 375 | 376 | const newCapsule = await request.post(`/api/auth/capsules/${login.body.user._id}`) 377 | .set('Accept', 'application/json') 378 | .set('x-access-token', login.body.token) 379 | .send({ 380 | user_name: login.body.user.user_name, 381 | user_id: login.body.user._id, 382 | type_capsule: 'Intenso', 383 | brand_capsule: 'Nespresso', 384 | notify_enf_capsules: 4, 385 | quantity_capsules_per_week: 5, 386 | price_last_buy: 20 387 | }); 388 | 389 | const res = await request.delete(`/api/auth/capsules/${login.body.user._id}/${newCapsule.body._id}`) 390 | .set('Accept', 'application/json') 391 | .set('x-access-token', login.body.token); 392 | 393 | expect(res.body.success).equal(false); 394 | }); 395 | 396 | it('#Erro when Delete capsule without id of user in url', async () => { 397 | const login = await request.post('/api/login') 398 | .set('Accept', 'application/json') 399 | .send({ 400 | user_name: 'nath', 401 | password: 'teste1' 402 | }); 403 | 404 | const newCapsule = await request.post(`/api/auth/capsules/${login.body.user._id}`) 405 | .set('Accept', 'application/json') 406 | .set('x-access-token', login.body.token) 407 | .send({ 408 | user_name: login.body.user.user_name, 409 | user_id: login.body.user._id, 410 | type_capsule: 'Intenso', 411 | brand_capsule: 'Nespresso', 412 | notify_enf_capsules: 4, 413 | quantity_capsules_per_week: 5, 414 | price_last_buy: 20 415 | }); 416 | 417 | const res = await request.delete(`/api/auth/capsules/${login.body._id}/${newCapsule.body.data._id}`) 418 | .set('Accept', 'application/json') 419 | .set('x-access-token', login.body.token); 420 | 421 | expect(res.body.success).equal(false); 422 | }); 423 | }); 424 | }); 425 | -------------------------------------------------------------------------------- /__test__/connect-db.spec.js: -------------------------------------------------------------------------------- 1 | const UsersDB = require('../api/models/Users'); 2 | const connect = require('../api/models/db'); 3 | 4 | const { expect } = require("chai"); 5 | 6 | describe('#Test user controller', () => { 7 | 8 | beforeEach(async () => { 9 | await connect.connection('test'); 10 | await UsersDB.remove().exec(); 11 | }); 12 | 13 | it('should save a user with empty value', async () => { 14 | const user = {}; 15 | 16 | const newUser = new UsersDB(user); 17 | const response = await newUser.save(); 18 | expect(response.user_name).equal(''); 19 | expect(response.user_mail).equal(''); 20 | expect(response.password).equal(''); 21 | expect(response.create_date).not.equal(''); 22 | // expect(response).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /__test__/users.controllers.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const express = require('../api/config/express')(); 3 | const request = require('supertest')(express); 4 | const UsersDB = require('../api/models/Users'); 5 | require('../api/models/db').connection('test'); 6 | 7 | 8 | describe('#User Controller', () => { 9 | 10 | beforeEach(async () => { 11 | await UsersDB.remove().exec(); 12 | }); 13 | 14 | describe('#Show all user', () => { 15 | it('#Get users without auth', async () => { 16 | const res = await request.get('/api/users') 17 | .set('Accept', 'application/json'); 18 | 19 | const isArrayResult = Array.isArray(res.body); 20 | expect(isArrayResult).equal(true); 21 | }); 22 | 23 | 24 | it('#Get users with auth', async () => { 25 | const user = await request.post('/api/createuser') 26 | .set('Accept', 'application/json') 27 | .send({ 28 | user_name: 'nath', 29 | user_mail: 'nath@nath.com.br', 30 | password: 'teste1' 31 | }); 32 | 33 | const login = await request.post('/api/login') 34 | .set('Accept', 'application/json') 35 | .send({ 36 | user_name: user.body.newuser.user_name, 37 | password: 'teste1' 38 | }); 39 | 40 | const res = await request.get('/api/auth/users') 41 | .set('Accept', 'application/json') 42 | .set('x-access-token', login.body.token); 43 | 44 | const isArrayResult = Array.isArray(res.body); 45 | expect(isArrayResult).equal(true); 46 | }); 47 | 48 | it('#Erro to get user without auth', async () => { 49 | const user = await request.post('/api/createuser') 50 | .set('Accept', 'application/json') 51 | .send({ 52 | user_name: 'nath', 53 | user_mail: 'nath@nath.com.br', 54 | password: 'teste1' 55 | }); 56 | 57 | await request.post('/api/login') 58 | .set('Accept', 'application/json') 59 | .send({ 60 | user_name: user.body.newuser.user_name, 61 | password: 'teste1' 62 | }); 63 | 64 | const res = await request.get('/api/auth/users') 65 | .set('Accept', 'application/json') 66 | .set('x-access-token', ''); 67 | 68 | expect(res.body.success).equal(false); 69 | }); 70 | 71 | it('#Erro to get user with wrong auth', async () => { 72 | const user = await request.post('/api/createuser') 73 | .set('Accept', 'application/json') 74 | .send({ 75 | user_name: 'nath', 76 | user_mail: 'nath@nath.com.br', 77 | password: 'teste1' 78 | }); 79 | 80 | const login = await request.post('/api/login') 81 | .set('Accept', 'application/json') 82 | .send({ 83 | user_name: user.body.newuser.user_name, 84 | password: 'teste1' 85 | }); 86 | 87 | const res = await request.get('/api/auth/users') 88 | .set('Accept', 'application/json') 89 | .set('x-access-token', `${login.body.token}a`); 90 | 91 | expect(res.body.success).equal(false); 92 | }); 93 | }); 94 | 95 | describe('#Create user', () => { 96 | it('#New user nonexistent', async () => { 97 | const res = await request.post('/api/createuser') 98 | .set('Accept', 'application/json') 99 | .send({ 100 | user_name: 'nath', 101 | user_mail: 'nath@nath.com.br', 102 | password: 'teste1' 103 | }); 104 | 105 | expect(res.body.success).equal(true); 106 | expect(res.body.newuser.user_name).equal('nath'); 107 | expect(res.body.newuser.user_mail).equal('nath@nath.com.br'); 108 | }); 109 | 110 | it('#New user empty user', async () => { 111 | const res = await request.post('/api/createuser') 112 | .set('Accept', 'application/json') 113 | .send({ 114 | user_name: '', 115 | user_mail: 'nath@nath123.com', 116 | password: 'teste1' 117 | }); 118 | 119 | expect(res.body.errors[0].msg).equal('Nome de usuário é obrigatório'); 120 | }); 121 | 122 | it('#New user empty mail', async () => { 123 | const res = await request.post('/api/createuser') 124 | .set('Accept', 'application/json') 125 | .send({ 126 | user_name: 'lala', 127 | user_mail: '', 128 | password: 'teste1' 129 | }); 130 | 131 | expect(res.body.errors[0].msg).equal('Email é obrigatório'); 132 | }); 133 | 134 | 135 | it('#New user empty mail', async () => { 136 | const res = await request.post('/api/createuser') 137 | .set('Accept', 'application/json') 138 | .send({ 139 | user_name: 'lala', 140 | user_mail: 'lala@lala.com', 141 | password: '' 142 | }); 143 | 144 | expect(res.body.errors[0].msg).equal('Senha é obrigatório'); 145 | }); 146 | 147 | it('#New user existing with the same name different email', async () => { 148 | await request.post('/api/createuser') 149 | .set('Accept', 'application/json') 150 | .send({ 151 | user_name: 'nath', 152 | user_mail: 'nath@nath.com.br', 153 | password: 'teste1' 154 | }); 155 | 156 | const res = await request.post('/api/createuser') 157 | .set('Accept', 'application/json') 158 | .send({ 159 | user_name: 'nath', 160 | user_mail: 'nath@teste.com.br', 161 | password: 'teste1' 162 | }); 163 | 164 | expect(res.body.success).equal(false); 165 | expect(res.body.message).equal('User already exists.'); 166 | }); 167 | 168 | it('#New user existing with the same email and different name', async () => { 169 | await request.post('/api/createuser') 170 | .set('Accept', 'application/json') 171 | .send({ 172 | user_name: 'nath', 173 | user_mail: 'nath@nath.com.br', 174 | password: 'teste1' 175 | }); 176 | 177 | const res = await request.post('/api/createuser') 178 | .set('Accept', 'application/json') 179 | .send({ 180 | user_name: 'nathinha', 181 | user_mail: 'nath@nath.com.br', 182 | password: 'teste1' 183 | }); 184 | 185 | expect(res.body.success).equal(false); 186 | expect(res.body.message).equal('User already exists.'); 187 | }); 188 | }); 189 | 190 | describe('#Login', () => { 191 | it('#Correct login in app', async () => { 192 | const user = await request.post('/api/createuser') 193 | .set('Accept', 'application/json') 194 | .send({ 195 | user_name: 'nath', 196 | user_mail: 'nath@nath.com.br', 197 | password: 'teste1' 198 | }); 199 | 200 | const res = await request.post('/api/login') 201 | .set('Accept', 'application/json') 202 | .send({ 203 | user_name: user.body.newuser.user_name, 204 | password: 'teste1' 205 | }); 206 | 207 | 208 | expect(res.body.success).equal(true); 209 | }); 210 | 211 | it('#Login with wrong pass', async () => { 212 | const user = await request.post('/api/createuser') 213 | .set('Accept', 'application/json') 214 | .send({ 215 | user_name: 'nath', 216 | user_mail: 'nath@nath.com.br', 217 | password: 'teste1' 218 | }); 219 | 220 | const res = await request.post('/api/login') 221 | .set('Accept', 'application/json') 222 | .send({ 223 | user_name: user.body.newuser.user_name, 224 | password: 'test1' 225 | }); 226 | 227 | expect(res.body.success).equal(false); 228 | expect(res.body.message).equal('Authentication failed. Wrong password.'); 229 | }); 230 | 231 | it('#Login with wrong user name', async () => { 232 | await request.post('/api/createuser') 233 | .set('Accept', 'application/json') 234 | .send({ 235 | user_name: 'nath', 236 | user_mail: 'nath@nath.com.br', 237 | password: 'teste1' 238 | }); 239 | 240 | const res = await request.post('/api/login') 241 | .set('Accept', 'application/json') 242 | .send({ 243 | user_name: 'n4th', 244 | password: 'teste1' 245 | }); 246 | 247 | expect(res.body.success).equal(false); 248 | expect(res.body.message).equal('Authentication failed. User not found.'); 249 | }); 250 | }); 251 | 252 | }); 253 | -------------------------------------------------------------------------------- /api/cluster.js: -------------------------------------------------------------------------------- 1 | const cluster = require('cluster'); 2 | const os = require('os'); 3 | const cpus = os.cpus(); 4 | 5 | if (cluster.isMaster) { 6 | console.log('Thread Master'); 7 | cpus.forEach(() => { 8 | cluster.fork(); 9 | }); 10 | 11 | cluster.on('listening', worker => { 12 | console.log(`cluster ${worker.process.pid} conectado`); 13 | }); 14 | 15 | cluster.on('disconnect', worker => { 16 | console.log(`cluster ${worker.process.pid} desconectado`); 17 | }); 18 | 19 | cluster.on('exit', worker => { 20 | console.log(`cluster ${worker.process.pid} perdido`); 21 | cluster.fork(); 22 | }); 23 | } else { 24 | console.log('Thread Slave'); 25 | require('./index'); 26 | } 27 | -------------------------------------------------------------------------------- /api/config/express.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const express = require('express'); 4 | const app = express(); 5 | var router = express.Router(); 6 | 7 | const bodyParse = require('body-parser'); 8 | const morgan = require('morgan'); 9 | const logger = require('../services/logger'); 10 | 11 | const jwt = require('jsonwebtoken'); 12 | const config = require('../models/config'); 13 | 14 | const consign = require('consign')({ 15 | cwd: 'api' 16 | }); 17 | 18 | const path = require('path'); 19 | 20 | 21 | module.exports = () => { 22 | 23 | // ======================= 24 | // configuration ========= 25 | // ======================= 26 | app.set('superSecret', config.secret); // secret variable 27 | 28 | app.use(bodyParse.urlencoded({ 29 | extended: true 30 | })); 31 | app.use(bodyParse.json()); 32 | app.use(express.static('../../public')); 33 | 34 | app.use(morgan('common', { 35 | stream: { 36 | write: (msg) => { 37 | logger.info(msg); 38 | } 39 | } 40 | })); 41 | 42 | app.use((req, res, next) => { 43 | res.header("Access-Control-Allow-Origin", "*"); 44 | res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS'); 45 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, Access-Control-Allow-Credentials, x-access-token"); 46 | res.header("Access-Control-Allow-Credentials", "true"); 47 | next(); 48 | }); 49 | 50 | consign 51 | .include('controllers') 52 | .then('routers') 53 | .into(app); 54 | 55 | return app; 56 | } 57 | -------------------------------------------------------------------------------- /api/controllers/Capsules.js: -------------------------------------------------------------------------------- 1 | const CapsulesDB = require('../models/Capsules'); 2 | const { validationResult } = require('express-validator'); 3 | const jwt = require('jsonwebtoken'); 4 | 5 | const apiCapsules = {}; 6 | 7 | 8 | apiCapsules.loadCapsulesToUser = (req, res) => { 9 | const id = req.params.userId; 10 | CapsulesDB.find({ 11 | user_id: id 12 | }, function (err, capsules) { 13 | if (err) { 14 | res.json({ 15 | success: false, 16 | message: 'Error to load capsules' 17 | }); 18 | } 19 | res.json({ 20 | success: true, 21 | capsules 22 | }); 23 | }); 24 | }; 25 | 26 | apiCapsules.loadOneCapsules = (req, res) => { 27 | const id = req.params.capsId; 28 | CapsulesDB.findOne({ 29 | _id: id 30 | }, function (err, capsules) { 31 | if (err) { 32 | res.json({ 33 | success: false, 34 | message: 'Error to load capsules' 35 | }); 36 | } 37 | res.json({ 38 | success: true, 39 | capsules 40 | }); 41 | }); 42 | }; 43 | 44 | apiCapsules.createNewCapsule = (req, res) => { 45 | let newCapsuleItem = req.body; 46 | 47 | const errors = validationResult(req); 48 | if (errors.errors.length) { 49 | res.status(400).send({ 50 | success: false, 51 | errors 52 | }); 53 | return; 54 | } 55 | 56 | const capsules = new CapsulesDB(newCapsuleItem); 57 | 58 | capsules.save((err, data) => { 59 | res.status(200).json({ 60 | success: true, 61 | data 62 | }); 63 | }); 64 | } 65 | 66 | apiCapsules.updateCapsule = (req, res) => { 67 | let query = { _id: req.params.capsId }, 68 | mod = req.body; 69 | 70 | const errors = validationResult(req); 71 | if (errors.errors.length) { 72 | res.status(400).send({ 73 | success: false, 74 | errors 75 | }); 76 | return; 77 | } 78 | 79 | CapsulesDB.update(query, mod, function (err, data) { 80 | res.status(200).json({ 81 | success: true, 82 | data, 83 | capsules: mod 84 | }); 85 | }); 86 | } 87 | 88 | apiCapsules.deleteCapsule = (req, res) => { 89 | var query = { _id: req.params.capsId }; 90 | 91 | if (req.params.capsId === 'undefined' || req.params.userId === 'undefined') { 92 | res.status(400).json({ 93 | success: false 94 | }); 95 | return; 96 | } 97 | 98 | CapsulesDB.remove(query, function (err, data) { 99 | 100 | const id = req.params.userId; 101 | 102 | CapsulesDB.find({ 103 | user_id: id 104 | }, function (err, capsules) { 105 | if (err) { 106 | res.json({ 107 | success: false, 108 | message: 'Error to load capsules' 109 | }); 110 | } 111 | res.json({ 112 | success: true, 113 | capsules 114 | }); 115 | }); 116 | // res.status(200).json({ 117 | // success: true, 118 | // data 119 | // }); 120 | }); 121 | } 122 | 123 | module.exports = apiCapsules; 124 | -------------------------------------------------------------------------------- /api/controllers/Users.js: -------------------------------------------------------------------------------- 1 | const UsersDB = require('../models/Users'); 2 | const servicesMemcached = require('../services/memcachedClient'); 3 | const jwt = require('jsonwebtoken'); 4 | const { validationResult } = require('express-validator'); 5 | 6 | const _generateToken = (user, secret) => jwt.sign(user.toJSON(), secret, { 7 | // expiresIn: 60 * 60 * 24 8 | // expiresIn: 300 // in seconds 9 | expiresIn: 60 * 60 * 24 // in seconds 10 | }); 11 | 12 | const apiUsers = {}; 13 | 14 | apiUsers.listUsers = (req, res) => { 15 | UsersDB.find({}, function (err, users) { 16 | res.json(users); 17 | }); 18 | }; 19 | 20 | apiUsers.authenticated = (req, res) => { 21 | const getUser = req.body; 22 | 23 | // return; 24 | UsersDB.findOne({ 25 | user_name: getUser.user_name 26 | }, function (err, user) { 27 | 28 | if (err) { 29 | throw err; 30 | } 31 | 32 | if (!user) { 33 | res.status(400).json({ 34 | success: false, 35 | message: 'Authentication failed. User not found.' 36 | }); 37 | } else if (user) { 38 | 39 | // decrypt password to check if is the same what the user use 40 | UsersDB.comparePassword(getUser.password, user.password, (reqq, isMatch) => { 41 | if (!isMatch) { 42 | res.json({ 43 | success: false, 44 | message: 'Authentication failed. Wrong password.' 45 | }); 46 | } else { 47 | // if user is found and password is right 48 | // create a token 49 | const token = _generateToken(user, req.app.get('superSecret')); 50 | // return the information including token as JSON 51 | res.json({ 52 | success: true, 53 | message: 'Enjoy your token!', 54 | token, 55 | user 56 | }); 57 | } 58 | }); 59 | } 60 | }); 61 | }; 62 | 63 | apiUsers.createUser = (req, res) => { 64 | let user = req.body; 65 | 66 | const errors = validationResult(req); 67 | if (errors.errors.length) { 68 | res.status(400).send(errors); 69 | return; 70 | } 71 | // check if have user 72 | UsersDB.find({ 73 | $or: [{ 74 | user_name: user.user_name 75 | }, { 76 | user_mail: user.user_mail 77 | }] 78 | }, (err, data) => { 79 | 80 | if (err || data.length > 0) { 81 | 82 | res.status(400).json({ 83 | success: false, 84 | message: 'User already exists.' 85 | }); 86 | return; 87 | } 88 | 89 | const newUser = new UsersDB(user); 90 | UsersDB.createUser(newUser, (req, newuser) => { 91 | res.status(201).json({ 92 | success: true, 93 | newuser 94 | }); 95 | }); 96 | }); 97 | }; 98 | 99 | module.exports = apiUsers; 100 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const app = require('./config/express')(); 4 | const db = require('./models/db').connection(); 5 | 6 | const port = process.env.PORT || 3000; // used to create, sign, and verify tokens 7 | 8 | app.listen(port, () => { 9 | console.log('===================================='); 10 | console.log('Magic happens at http://localhost:' + port); 11 | console.log('===================================='); 12 | }); 13 | -------------------------------------------------------------------------------- /api/models/Capsules.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | 3 | const capsulesSchema = new mongoose.Schema({ 4 | user_name: { 5 | type: String, 6 | default: '' 7 | }, 8 | user_id: { 9 | type: String, 10 | default: '' 11 | }, 12 | brand_capsule: { 13 | type: String, 14 | default: '' 15 | }, 16 | type_capsule: { 17 | type: String, 18 | default: '' 19 | }, 20 | price_last_buy: { 21 | type: Number, 22 | min: 0, 23 | default: null 24 | }, 25 | quantity_capsules_per_week: { 26 | type: Number, 27 | min: 0, 28 | default: null 29 | }, 30 | notify_enf_capsules: { 31 | type: Number, 32 | min: 0, 33 | default: null 34 | }, 35 | data: { 36 | type: Date, 37 | default: Date.now() 38 | } 39 | }); 40 | 41 | module.exports = mongoose.model('Capsules', capsulesSchema); 42 | -------------------------------------------------------------------------------- /api/models/Users.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const bcrypt = require('bcryptjs'); 3 | 4 | const usersSchema = new mongoose.Schema({ 5 | user_name: { 6 | type: String, 7 | default: '' 8 | }, 9 | user_mail: { 10 | type: String, 11 | default: '' 12 | }, 13 | password: { 14 | type: String, 15 | default: '' 16 | }, 17 | create_date: { 18 | type: Date, 19 | default: Date.now() 20 | } 21 | }); 22 | 23 | module.exports = mongoose.model('Users', usersSchema); 24 | 25 | module.exports.createUser = (newUser, callback) => { 26 | bcrypt.genSalt(10, (err, salt) => { 27 | bcrypt.hash(newUser.password, salt, (err, hash) => { 28 | newUser.password = hash; 29 | newUser.save(callback); 30 | }); 31 | }); 32 | } 33 | 34 | module.exports.comparePassword = (candidatePassword, hash, callback) => { 35 | bcrypt.compare(candidatePassword, hash, (err, isMatch) => { 36 | if (err) throw err; 37 | callback(null, isMatch); 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /api/models/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'secret': 'ilovescotchyscotch' 3 | }; 4 | -------------------------------------------------------------------------------- /api/models/db.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | // const port = process.env.PORT || 3000; // used to create, sign, and verify tokens 3 | let dbURL = 'mongodb://localhost/caffeine'; 4 | 5 | module.exports.connection = (type) => { 6 | if (type === 'test') { 7 | dbURL = 'mongodb://localhost/caffeine_test'; 8 | } 9 | 10 | const db = mongoose.connection; 11 | db.on('error', function (err) { 12 | // console.log('Erro de conexao.', err); 13 | }); 14 | db.on('open', function () { 15 | // console.log('Conexão aberta.'); 16 | }); 17 | db.on('connected', function (err) { 18 | mongoose.Promise = global.Promise; 19 | // console.log('Conectado'); 20 | }); 21 | db.on('disconnected', function (err) { 22 | // console.log('Desconectado'); 23 | }); 24 | 25 | mongoose.Promise = global.Promise; 26 | return mongoose.connect(dbURL); 27 | }; 28 | -------------------------------------------------------------------------------- /api/routers/checkAuth.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | 3 | CheckAuth = (req, res, next) => { 4 | const { body, query, headers, app } = req; 5 | if (body === undefined) return; 6 | 7 | const token = body.token || query.token || headers['x-access-token']; 8 | 9 | // decode token 10 | if (token) { 11 | // verifies secret and checks exp 12 | jwt.verify(token, app.get('superSecret'), function (err, decodedJWT) { 13 | if (err) { 14 | return res.json({ 15 | success: false, 16 | message: 'Failed to authenticate token.' 17 | }); 18 | } 19 | 20 | // if everything is good, save to request for use in other routes 21 | next(); 22 | }); 23 | 24 | } else { 25 | // if there is no token 26 | // return an error 27 | return res.status(403).send({ 28 | success: false, 29 | message: 'No token provided.' 30 | }); 31 | } 32 | } 33 | 34 | module.exports = CheckAuth; 35 | -------------------------------------------------------------------------------- /api/routers/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { check } = require('express-validator'); 3 | 4 | const CheckAuth = require('./checkAuth'); 5 | 6 | module.exports = function (app) { 7 | 8 | const apiRoutes = express.Router(); 9 | const authRoutes = express.Router(); 10 | 11 | apiRoutes.use('/auth', authRoutes); 12 | 13 | apiRoutes.post('/login', app.controllers.Users.authenticated); 14 | apiRoutes.post('/createuser', [ 15 | check('user_name', 'Nome de usuário é obrigatório').notEmpty(), 16 | check('user_mail', 'Email é obrigatório').isEmail(), 17 | check('password', 'Senha é obrigatório').notEmpty() 18 | ], app.controllers.Users.createUser); 19 | apiRoutes.get('/users', app.controllers.Users.listUsers); 20 | 21 | // AUTH 22 | authRoutes.get('/users', CheckAuth, app.controllers.Users.listUsers); 23 | authRoutes.get('/capsules/:userId', CheckAuth, app.controllers.Capsules.loadCapsulesToUser); 24 | authRoutes.post('/capsules/:userId', CheckAuth, [ 25 | check('user_name', 'Nome de usuário é obrigatório').notEmpty(), 26 | check('user_id', 'O id do usuário é obrigatório').notEmpty(), 27 | check('type_capsule', 'Tipode da capsula é obrigatório').notEmpty(), 28 | check('brand_capsule', 'A marca da capsula é obrigatório').notEmpty(), 29 | check('quantity_capsules_per_week', 'Quantidade de cápsulas por semana é obrigatório').notEmpty(), 30 | check('notify_enf_capsules', 'Notificar quando finalizar é obrigatório').notEmpty(), 31 | ], app.controllers.Capsules.createNewCapsule); 32 | authRoutes.get('/capsule/:capsId', CheckAuth, app.controllers.Capsules.loadOneCapsules); 33 | authRoutes.put('/capsule/:capsId', CheckAuth, [ 34 | check('user_name', 'Nome de usuário é obrigatório').notEmpty(), 35 | check('user_id', 'O id do usuário é obrigatório').notEmpty(), 36 | check('quantity_capsules_per_week', 'Quantidade de cápsulas por semana é obrigatório').notEmpty(), 37 | check('notify_enf_capsules', 'Notificar quando finalizar é obrigatório').notEmpty(), 38 | ], app.controllers.Capsules.updateCapsule); 39 | authRoutes.delete('/capsules/:userId/:capsId', CheckAuth, app.controllers.Capsules.deleteCapsule); 40 | // authRoutes.post('/create/capsules/user', CheckAuth, app.controllers.Capsules.createCapsulesToUser); 41 | 42 | app.use('/api', apiRoutes); 43 | }; 44 | -------------------------------------------------------------------------------- /api/services/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const winston = require('winston'); 4 | const fs = require('fs'); 5 | 6 | if (!fs.existsSync('logs')) { 7 | fs.mkdirSync('logs'); 8 | } 9 | 10 | module.exports = winston.createLogger({ 11 | transports: [ 12 | new winston.transports.File({ 13 | level: "info", 14 | filename: "logs/caffeine.log", 15 | maxsize: 1048576, 16 | maxFiles: 10, 17 | colorize: false 18 | }) 19 | ] 20 | }); 21 | -------------------------------------------------------------------------------- /api/services/memcachedClient.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Memcached = require('memcached'); 4 | 5 | function memcachedClient() { 6 | const client = new Memcached('localhost:11211', { 7 | retries: 10, 8 | retry: 10000, 9 | remove: true 10 | }); 11 | 12 | return client; 13 | } 14 | 15 | module.exports = memcachedClient; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "caffeine", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "author": "NathPaiva", 6 | "license": "MIT", 7 | "react-hot-loader": "next", 8 | "engines": { 9 | "node": "v16.6.1" 10 | }, 11 | "scripts": { 12 | "test:api:watch": "mocha ./__test__/** --watch", 13 | "test:api": "mocha ./__test__/** --exit", 14 | "test:ci:api": "npm run test:api", 15 | "test:ui": "jest ./public/src/** --coverage", 16 | "test:ci:ui": "npm run test:ui", 17 | "test": "npm run test:ui && npm run test:api", 18 | "start:api": "nodemon api/index.js", 19 | "start:front": "node webpack-start.js", 20 | "start": "npm-run-all -p start:front start:api", 21 | "build": "cross-env NODE_ENV=production BABEL_ENV=production webpack --progress --config webpack/prod.config -p", 22 | "public": "webpack", 23 | "storybook": "start-storybook -p 6006", 24 | "build-storybook": "build-storybook", 25 | "lint": "eslint ./public/src/", 26 | "lint:fix": "eslint ./public/src --fix" 27 | }, 28 | "devDependencies": { 29 | "@babel/plugin-proposal-class-properties": "7.10.4", 30 | "@babel/preset-env": "7.11.0", 31 | "@babel/runtime": "7.11.2", 32 | "@storybook/react": "5.3.19", 33 | "babel-jest": "26.2.2", 34 | "babel-loader": "8.1.0", 35 | "babel-preset-es2015": "6.24.1", 36 | "babel-preset-stage-0": "6.24.1", 37 | "chai": "4.2.0", 38 | "clean-webpack-plugin": "3.0.0", 39 | "cross-env": "7.0.2", 40 | "detect-browser": "5.1.1", 41 | "eslint": "7.6.0", 42 | "eslint-config-react-app": "5.2.1", 43 | "eslint-plugin-import": "2.18.2", 44 | "eslint-plugin-jsx-a11y": "6.2.3", 45 | "eslint-plugin-react": "7.14.3", 46 | "eslint-plugin-react-hooks": "1.7.0", 47 | "extract-text-webpack-plugin": "3.0.2", 48 | "file-loader": "6.0.0", 49 | "html-webpack-plugin": "4.3.0", 50 | "imagemin-webpack-plugin": "2.4.2", 51 | "jest": "26.0.1", 52 | "jest-cli": "26.0.1", 53 | "mocha": "^10.2.0", 54 | "react-hot-loader": "4.12.21", 55 | "react-test-renderer": "16.13.1", 56 | "semver": "7.3.2", 57 | "supertest-as-promised": "4.0.2", 58 | "webpack": "4.43.0", 59 | "webpack-cli": "3.3.12", 60 | "webpack-dashboard": "3.2.0", 61 | "webpack-dev-server": "3.11.0" 62 | }, 63 | "dependencies": { 64 | "@babel/core": "7.11.1", 65 | "@babel/plugin-transform-runtime": "7.11.0", 66 | "@babel/preset-es2015": "7.0.0-beta.53", 67 | "@babel/preset-react": "7.10.4", 68 | "babel-eslint": "10.0.3", 69 | "babel-plugin-react-transform": "3.0.0", 70 | "bcryptjs": "2.4.3", 71 | "body-parser": "1.19.0", 72 | "consign": "0.1.5", 73 | "eslint-plugin-jest": "23.8.2", 74 | "express": "4.15.3", 75 | "express-validator": "6.6.0", 76 | "immutable": "4.0.0-rc.12", 77 | "jsonwebtoken": "8.5.1", 78 | "memcached": "2.2.2", 79 | "mongoose": "5.9.19", 80 | "morgan": "1.10.0", 81 | "nodemon": "2.0.4", 82 | "npm-run-all": "4.1.5", 83 | "react": "16.13.1", 84 | "react-dom": "16.13.1", 85 | "react-redux": "7.2.1", 86 | "react-router-dom": "5.2.0", 87 | "react-tabs": "3.1.1", 88 | "redux": "4.0.5", 89 | "redux-thunk": "2.3.0", 90 | "rest": "2.0.0", 91 | "restify": "8.5.1", 92 | "soap": "0.32.0", 93 | "styled-components": "5.1.1", 94 | "supertest": "4.0.2", 95 | "winston": "3.3.1" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathpaiva/caffeine-redux/5532ea7e7a9fcf73e1859ecd55c08bb938400412/public/.DS_Store -------------------------------------------------------------------------------- /public/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathpaiva/caffeine-redux/5532ea7e7a9fcf73e1859ecd55c08bb938400412/public/src/.DS_Store -------------------------------------------------------------------------------- /public/src/App.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, { Component } from 'react'; 4 | import { Route, Switch } from 'react-router-dom'; 5 | import history from './components/history'; 6 | 7 | import Authenticated from './components/authenticated/Authenticated' 8 | 9 | import Header from './components/header/Header'; 10 | 11 | import Login from './components/login/Login'; 12 | import Logout from './components/logout/Logout'; 13 | import Edit from './components/edit/Edit'; 14 | import Create from './components/create/Create'; 15 | import List from './components/list/List'; 16 | import NotFound from './components/notfound/NotFound'; 17 | 18 | class App extends Component { 19 | render() { 20 | return ( 21 | <> 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | } 34 | } 35 | 36 | export default App; 37 | -------------------------------------------------------------------------------- /public/src/RestCSS.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | const Reset = createGlobalStyle` 4 | * { 5 | margin: 0; 6 | padding: 0; 7 | border: 0; 8 | font: inherit; 9 | vertical-align: baseline; 10 | box-sizing: border-box; 11 | } 12 | 13 | body { 14 | overflow-x: hidden; 15 | font-size: 10px; 16 | font-family: "Roboto", sans-serif; 17 | line-height: 1; 18 | color: #414141; 19 | font-weight: 400; 20 | background-color: #F1F1F1; 21 | } 22 | 23 | a { 24 | color: inherit; 25 | text-decoration: none; 26 | cursor: pointer; 27 | } 28 | 29 | ol, ul { 30 | list-style: none; 31 | } 32 | 33 | p { 34 | font-size: 1.2em; 35 | padding: 5px 0; 36 | } 37 | 38 | label, input { 39 | font-size: 1.2em; 40 | } 41 | 42 | .container { 43 | width: 100%; 44 | max-width: 1280px; 45 | background-color: orange; 46 | margin: 0 auto; 47 | @media screen and (max-width: 1300px) { 48 | padding: 0 20px; 49 | } 50 | 51 | @media screen and (max-width: 420px) and (orientation: portrait) { 52 | padding: 0 15px; 53 | } 54 | } 55 | .react-tabs__tab-list { 56 | display: flex; 57 | justify-content: space-around; 58 | flex-direction: column; 59 | text-align: center; 60 | > .react-tabs__tab { 61 | cursor: pointer; 62 | margin: 5px 0; 63 | font-size: 12px; 64 | &.react-tabs__tab--selected { 65 | text-decoration: underline; 66 | &:before { 67 | content: "> "; 68 | } 69 | } 70 | } 71 | } 72 | .hide { 73 | display: none; 74 | } 75 | 76 | `; 77 | 78 | export default Reset; 79 | -------------------------------------------------------------------------------- /public/src/actions/actionCreator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export function allCapsules(capsules) { 4 | return { type: 'CAPSULES', capsules }; 5 | } 6 | 7 | export function showMessage(message) { 8 | return { type: 'MESSAGE', message }; 9 | } 10 | -------------------------------------------------------------------------------- /public/src/components/authenticated/Authenticated.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | 4 | const Authenticated = ({ props, component: Component, ...rest }) => ( 5 | { 8 | if (!localStorage.getItem('auth-token')) { 9 | return ; 10 | } else { 11 | return ; 12 | } 13 | }} 14 | /> 15 | ); 16 | 17 | export default Authenticated; 18 | -------------------------------------------------------------------------------- /public/src/components/box/Box.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import Container from '../container/Container'; 5 | import Input from '../input/Input'; 6 | import Button from '../button/Button'; 7 | import Title from '../title/Title'; 8 | 9 | import style from '../../css/inline'; 10 | 11 | const Box = ({ link, title, text, inputs, button, to }) => ( 12 |
13 |
    14 | {!!link && ( 15 |
    16 | {/* card link */} 17 | {link.map((item, i) => { 18 | if (item.to) { 19 | return ( 20 | 21 | {item.text} 22 | 23 | ); 24 | } else { 25 | return ( 26 | 27 | {item.text} 28 | 29 | ); 30 | } 31 | })} 32 |
    33 | )} 34 | {!!title && ( 35 | 36 | 37 | </Container> 38 | )} 39 | {!!text && ( 40 | <Container> 41 | <p>{text}</p> 42 | </Container> 43 | )} 44 | {!!inputs && 45 | inputs.map((input, i) => { 46 | return ( 47 | <Container key={i}> 48 | <Input 49 | type={input.type} 50 | id={input.id} 51 | text={input.text} 52 | inputRef={input.inputRef} 53 | label={input.label} 54 | disabled={input.disabled} 55 | defaultValue={input.defaultValue} 56 | /> 57 | </Container> 58 | ); 59 | })} 60 | {!!button && ( 61 | <Container> 62 | {(!!to && ( 63 | <Link to={to}> 64 | <Button name={button} /> 65 | </Link> 66 | )) || <Button name={button} />} 67 | </Container> 68 | )} 69 | </ul> 70 | </section> 71 | ); 72 | 73 | export default Box; 74 | -------------------------------------------------------------------------------- /public/src/components/box/__snapshots__/box.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`#Test Box Component #Should Box with success 1`] = ` 4 | <section 5 | style={ 6 | Object { 7 | "backgroundColor": "#fff", 8 | "border": "1px solid rgb(90, 74, 105)", 9 | "borderRadius": "2px", 10 | "display": "flex", 11 | "margin": "10px auto auto", 12 | "maxWidth": "500px", 13 | "padding": "10px 0", 14 | "width": "100%", 15 | } 16 | } 17 | > 18 | <ul 19 | style={ 20 | Object { 21 | "padding": "0 10px", 22 | "position": "relative", 23 | "width": "100%", 24 | } 25 | } 26 | > 27 | <li 28 | style={ 29 | Object { 30 | "fontSize": "12px", 31 | "margin": "5px 0", 32 | } 33 | } 34 | > 35 | <h2 36 | style={ 37 | Object { 38 | "color": "#414141", 39 | "fontFamily": "\\"Roboto\\", sans-serif", 40 | "fontSize": "20px", 41 | "textAlign": "center", 42 | } 43 | } 44 | > 45 | Register 46 | </h2> 47 | </li> 48 | <li 49 | style={ 50 | Object { 51 | "fontSize": "12px", 52 | "margin": "5px 0", 53 | } 54 | } 55 | > 56 | <p> 57 | First time using of caffeine. 58 | </p> 59 | </li> 60 | <li 61 | style={ 62 | Object { 63 | "fontSize": "12px", 64 | "margin": "5px 0", 65 | } 66 | } 67 | > 68 | <div> 69 | 70 | <input 71 | defaultValue="" 72 | disabled={false} 73 | id="email" 74 | name="email" 75 | placeholder="email" 76 | style={ 77 | Object { 78 | "border": "1px solid #CCCCCC", 79 | "borderRadius": "2px", 80 | "height": "32px", 81 | "margin": "2px 0", 82 | "padding": "0 5px", 83 | "width": "100%", 84 | } 85 | } 86 | type="email" 87 | /> 88 | </div> 89 | </li> 90 | <li 91 | style={ 92 | Object { 93 | "fontSize": "12px", 94 | "margin": "5px 0", 95 | } 96 | } 97 | > 98 | <div> 99 | 100 | <input 101 | defaultValue="" 102 | disabled={false} 103 | id="login" 104 | name="login" 105 | placeholder="login" 106 | style={ 107 | Object { 108 | "border": "1px solid #CCCCCC", 109 | "borderRadius": "2px", 110 | "height": "32px", 111 | "margin": "2px 0", 112 | "padding": "0 5px", 113 | "width": "100%", 114 | } 115 | } 116 | type="text" 117 | /> 118 | </div> 119 | </li> 120 | <li 121 | style={ 122 | Object { 123 | "fontSize": "12px", 124 | "margin": "5px 0", 125 | } 126 | } 127 | > 128 | <div> 129 | 130 | <input 131 | defaultValue="" 132 | disabled={false} 133 | id="pass" 134 | name="pass" 135 | placeholder="password" 136 | style={ 137 | Object { 138 | "border": "1px solid #CCCCCC", 139 | "borderRadius": "2px", 140 | "height": "32px", 141 | "margin": "2px 0", 142 | "padding": "0 5px", 143 | "width": "100%", 144 | } 145 | } 146 | type="password" 147 | /> 148 | </div> 149 | </li> 150 | <li 151 | style={ 152 | Object { 153 | "fontSize": "12px", 154 | "margin": "5px 0", 155 | } 156 | } 157 | > 158 | <button 159 | style={ 160 | Object { 161 | "backgroundColor": "#5A4A69", 162 | "border": "none", 163 | "borderRadius": "2px", 164 | "color": "#FFFFFF", 165 | "cursor": "pointer", 166 | "fontSize": "12px", 167 | "fontWeight": "bold", 168 | "padding": "10px", 169 | "textAlign": "center", 170 | "textTransform": "uppercase", 171 | "width": "100%", 172 | } 173 | } 174 | > 175 | Create 176 | </button> 177 | </li> 178 | </ul> 179 | </section> 180 | `; 181 | -------------------------------------------------------------------------------- /public/src/components/box/box.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import renderer from 'react-test-renderer'; 5 | 6 | import Box from './Box'; 7 | 8 | describe('#Test Box Component', () => { 9 | it('#Should Box with success', () => { 10 | const tree = renderer 11 | .create( 12 | <Box 13 | title='Register' 14 | text='First time using of caffeine.' 15 | inputs={[ 16 | { 17 | type: 'email', 18 | id: 'email', 19 | text: 'email', 20 | inputRef: (input) => { 21 | var email = input; 22 | return email; 23 | }, 24 | }, 25 | { 26 | type: 'text', 27 | id: 'login', 28 | text: 'login', 29 | inputRef: (input) => { 30 | var login = input; 31 | return login; 32 | }, 33 | }, 34 | { 35 | type: 'password', 36 | id: 'pass', 37 | text: 'password', 38 | inputRef: (input) => { 39 | var password = input; 40 | return password; 41 | }, 42 | }, 43 | ]} 44 | button='Create' 45 | /> 46 | ) 47 | .toJSON(); 48 | expect(tree).toMatchSnapshot(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /public/src/components/box/box.story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { storiesOf } from '@storybook/react'; 4 | 5 | // import '../../css/_reset.scss'; 6 | import Box from './Box'; 7 | 8 | storiesOf('Box', module) 9 | .add('with inputs', () => ( 10 | <Box 11 | title='Register' 12 | text='First time using of caffeine.' 13 | inputs={[ 14 | { 15 | type: 'email', 16 | id: 'email', 17 | text: 'email', 18 | }, 19 | { 20 | type: 'text', 21 | id: 'login', 22 | text: 'login', 23 | }, 24 | { 25 | type: 'password', 26 | id: 'pass', 27 | text: 'password', 28 | }, 29 | ]} 30 | button='Create' 31 | /> 32 | )) 33 | .add('without inputs', () => <Box title='Info caps' button='Add new capsule' />) 34 | .add('with inputs disabled and without button and title', () => ( 35 | <Box 36 | inputs={[ 37 | { 38 | type: 'email', 39 | id: 'email', 40 | text: 'email', 41 | disabled: true, 42 | }, 43 | { 44 | type: 'text', 45 | id: 'login', 46 | text: 'login', 47 | disabled: true, 48 | }, 49 | { 50 | type: 'password', 51 | id: 'pass', 52 | text: 'password', 53 | disabled: true, 54 | }, 55 | ]} 56 | /> 57 | )); 58 | -------------------------------------------------------------------------------- /public/src/components/button/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | var buttonStyle = { 4 | fontSize: '12px', 5 | padding: '10px', 6 | textAlign: 'center', 7 | textTransform: 'uppercase', 8 | cursor: 'pointer', 9 | backgroundColor: '#5A4A69', 10 | color: '#FFFFFF', 11 | fontWeight: 'bold', 12 | width: '100%', 13 | border: 'none', 14 | borderRadius: '2px', 15 | }; 16 | 17 | 18 | const Button = ({ name }) => ( 19 | <button style={buttonStyle}>{name}</button> 20 | ); 21 | 22 | export default Button; 23 | -------------------------------------------------------------------------------- /public/src/components/button/__snapshots__/button.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`#Test Button Component #Should Button to mach snapshot 1`] = ` 4 | <button 5 | style={ 6 | Object { 7 | "backgroundColor": "#5A4A69", 8 | "border": "none", 9 | "borderRadius": "2px", 10 | "color": "#FFFFFF", 11 | "cursor": "pointer", 12 | "fontSize": "12px", 13 | "fontWeight": "bold", 14 | "padding": "10px", 15 | "textAlign": "center", 16 | "textTransform": "uppercase", 17 | "width": "100%", 18 | } 19 | } 20 | > 21 | Entrar 22 | </button> 23 | `; 24 | -------------------------------------------------------------------------------- /public/src/components/button/button.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import renderer from 'react-test-renderer'; 5 | 6 | import Button from './Button'; 7 | 8 | describe('#Test Button Component', () => { 9 | it('#Should Button to mach snapshot', () => { 10 | const tree = renderer.create(<Button name='Entrar' />).toJSON(); 11 | expect(tree).toMatchSnapshot(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /public/src/components/button/button.story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { storiesOf } from '@storybook/react'; 4 | 5 | import Button from './Button'; 6 | 7 | storiesOf('Button', module).add('with text 100% width', () => <Button name='Entrar' />); 8 | -------------------------------------------------------------------------------- /public/src/components/container/Container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const container = { 4 | margin: '5px 0', 5 | fontSize: '12px' 6 | } 7 | 8 | const Container = ({ children }) => ( 9 | <li style={container}> 10 | {children} 11 | </li> 12 | ); 13 | 14 | export default Container; 15 | -------------------------------------------------------------------------------- /public/src/components/container/__snapshots__/container.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`#Test Container Component #Should Container to mach snapshot - li item 1`] = ` 4 | <li 5 | style={ 6 | Object { 7 | "fontSize": "12px", 8 | "margin": "5px 0", 9 | } 10 | } 11 | > 12 | <div> 13 | Oioi 14 | </div> 15 | </li> 16 | `; 17 | -------------------------------------------------------------------------------- /public/src/components/container/container.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import renderer from 'react-test-renderer'; 5 | 6 | import Container from './Container'; 7 | 8 | describe('#Test Container Component', () => { 9 | it('#Should Container to mach snapshot - li item', () => { 10 | const tree = renderer 11 | .create( 12 | <Container> 13 | <div>Oioi</div> 14 | </Container> 15 | ) 16 | .toJSON(); 17 | expect(tree).toMatchSnapshot(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /public/src/components/container/container.story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { storiesOf } from '@storybook/react'; 4 | 5 | import Container from './Container'; 6 | 7 | storiesOf('Container', module).add('without label 100% width', () => ( 8 | <ul> 9 | <Container>List of items</Container> 10 | </ul> 11 | )); 12 | -------------------------------------------------------------------------------- /public/src/components/create/Create.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { createItem } from '../../logics/CaffeineApi'; 5 | 6 | import Message from '../message/Message'; 7 | import Box from '../box/Box'; 8 | 9 | import '../../css/inline'; 10 | 11 | class Create extends Component { 12 | render() { 13 | return ( 14 | <form 15 | onSubmit={(e) => { 16 | e.preventDefault(); 17 | 18 | const user = JSON.parse(localStorage.getItem('user')); 19 | const newCapsule = { 20 | notify_enf_capsules: this.notify.value, 21 | quantity_capsules_per_week: this.weekcapsules.value, 22 | price_last_buy: this.price.value, 23 | brand_capsule: this.brand.value, 24 | type_capsule: this.type.value, 25 | user_id: this.props.match.params.id, 26 | user_name: user.user_name, 27 | }; 28 | this.props.handleClick(newCapsule); 29 | }} 30 | > 31 | <Message className={this.props.typeMesage} msg={this.props.msg} /> 32 | <Box 33 | title='Create capsule' 34 | link={[ 35 | { 36 | text: 'back to list', 37 | to: `/list/${this.props.match.params.id}`, 38 | }, 39 | ]} 40 | inputs={[ 41 | { type: 'text', id: 'brand', text: '', label: 'Capsule brand:', inputRef: (input) => (this.brand = input) }, 42 | { type: 'text', id: 'type', text: '', label: 'Capsule name:', inputRef: (input) => (this.type = input) }, 43 | { type: 'number', id: 'price', text: '', label: 'Last price paied:', inputRef: (input) => (this.price = input) }, 44 | { type: 'number', id: 'weekcapsules', text: '', label: 'Quantity capsules per week:', inputRef: (input) => (this.weekcapsules = input) }, 45 | { type: 'number', id: 'notify', text: '', label: 'Notify before end:', inputRef: (input) => (this.notify = input) }, 46 | ]} 47 | button='Save' 48 | /> 49 | </form> 50 | ); 51 | } 52 | } 53 | 54 | const mapDispatchToProps = (dispatch) => { 55 | return { 56 | handleClick: (capsule) => { 57 | dispatch(createItem(capsule)); 58 | }, 59 | }; 60 | }; 61 | 62 | const mapStateToProps = (state) => { 63 | return { 64 | typeMesage: state.messageReducer.typeMesage, 65 | msg: state.messageReducer.msg, 66 | }; 67 | }; 68 | 69 | const CreateRedux = connect(mapStateToProps, mapDispatchToProps)(Create); 70 | 71 | export default CreateRedux; 72 | -------------------------------------------------------------------------------- /public/src/components/edit/Edit.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { getAllItems, createItem } from '../../logics/CaffeineApi'; 5 | import Message from '../message/Message'; 6 | 7 | import Box from '../box/Box'; 8 | 9 | class Edit extends Component { 10 | 11 | componentDidMount() { 12 | this.props.list(localStorage.getItem('auth-token'), this.props.match.params.id); 13 | } 14 | 15 | render() { 16 | return ( 17 | <form onSubmit={e => { 18 | e.preventDefault(); 19 | 20 | const user = JSON.parse(localStorage.getItem('user')); 21 | const newCapsule = { 22 | "notify_enf_capsules": this.notify.value, 23 | "quantity_capsules_per_week": this.weekcapsules.value, 24 | "price_last_buy": this.price.value, 25 | "brand_capsule": this.brand.value, 26 | "type_capsule": this.type.value, 27 | "capsule_id": this.props.match.params.id, 28 | "user_id": this.props.match.params.user_id, 29 | "user_name": user.user_name 30 | } 31 | this.props.handleClick(newCapsule); 32 | }}> 33 | <Message className={this.props.typeMesage} msg={this.props.msg} /> 34 | {this.props.capsules.map(capsule => <Box title='Info capsule' key={capsule._id} link={[{ 35 | text: 'back to list', 36 | to: `/list/${this.props.match.params.user_id}` 37 | }]} inputs={[ 38 | { type: 'text', id: 'brand', text: capsule.brand_capsule, label: 'Capsule brand:', defaultValue: capsule.brand_capsule, inputRef: (input) => this.brand = input }, 39 | { type: 'text', id: 'type', text: capsule.type_capsule, label: 'Capsule name:', defaultValue: capsule.type_capsule, inputRef: (input) => this.type = input }, 40 | { type: 'number', id: 'price', text: capsule.price_last_buy, label: 'Last price paied:', defaultValue: capsule.price_last_buy, inputRef: (input) => this.price = input }, 41 | { type: 'number', id: 'weekcapsules', text: capsule.quantity_capsules_per_week, label: 'Quantity capsules per week:', defaultValue: capsule.quantity_capsules_per_week, inputRef: (input) => this.weekcapsules = input }, 42 | { type: 'number', id: 'notify', text: capsule.notify_enf_capsules, label: 'Notify end capsules:', defaultValue: capsule.notify_enf_capsules, inputRef: (input) => this.notify = input } 43 | ]} button='Change capsule' /> 44 | )} 45 | </form> 46 | ) 47 | } 48 | } 49 | 50 | const mapDispatchToProps = dispatch => { 51 | return { 52 | list: (token, user_id) => { 53 | dispatch(getAllItems(token, user_id, 'one')); 54 | }, 55 | handleClick: (capsule) => { 56 | dispatch(createItem(capsule, 'one')); 57 | } 58 | } 59 | } 60 | 61 | const mapStateToProps = state => { 62 | return { 63 | capsules: state.capsulesReducer, 64 | typeMesage: state.messageReducer.typeMesage, 65 | msg: state.messageReducer.msg 66 | } 67 | } 68 | 69 | const EditRedux = connect(mapStateToProps, mapDispatchToProps)(Edit); 70 | 71 | export default EditRedux; 72 | -------------------------------------------------------------------------------- /public/src/components/header/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import logo from '../../images/logo.png'; 5 | import style from './header.css'; 6 | 7 | 8 | const user = () => { 9 | return JSON.parse(localStorage.getItem('user')) 10 | }; 11 | 12 | const Header = () => ( 13 | <header style={style.header}> 14 | <div style={localStorage.getItem('user') ? style.container : style.containerLogout}> 15 | <h1><img style={style.img} src={logo} alt='Caffeine' /><span style={style.hide}>Caffeine</span></h1> 16 | {!!user() && <div style={style.utils}> 17 | <span style={style.welcomeblock}>Welcome {user().user_name}</span> 18 | <Link style={style.link} to='/logout'>logout</Link> 19 | </div>} 20 | </div> 21 | </header> 22 | ); 23 | 24 | export default Header; 25 | -------------------------------------------------------------------------------- /public/src/components/header/header.css.js: -------------------------------------------------------------------------------- 1 | const style = { 2 | header: { 3 | width: '100%', 4 | margin: '0 auto', 5 | borderBottom: '1px solid rgb(90, 74, 105)', 6 | backgroundColor: '#fff' 7 | }, 8 | container: { 9 | display: 'flex', 10 | justifyContent: 'space-between', 11 | alignItems: 'center', 12 | maxWidth: '500px', 13 | margin: 'auto' 14 | }, 15 | containerLogout: { 16 | display: 'flex', 17 | justifyContent: 'center', 18 | alignItems: 'center', 19 | maxWidth: '500px', 20 | margin: 'auto' 21 | }, 22 | utils: { 23 | width: '150px', 24 | justifyContent: 'space-between', 25 | display: 'flex', 26 | fontSize: '12px', 27 | alignItems: 'center', 28 | }, 29 | welcomeblock: { 30 | maxWidth: '90px', 31 | display: 'block', 32 | wordWrap: 'break-word', 33 | }, 34 | img: { 35 | width: '90px' 36 | }, 37 | link: { 38 | textDecoration: 'underline', 39 | }, 40 | hide: { 41 | display: 'block', 42 | position: 'absolute', 43 | textIndent: '-9999px' 44 | }, 45 | }; 46 | 47 | export default style; 48 | -------------------------------------------------------------------------------- /public/src/components/history/index.js: -------------------------------------------------------------------------------- 1 | // history.js 2 | import { createBrowserHistory } from 'history'; 3 | 4 | export default createBrowserHistory({ 5 | /* pass a configuration object here if needed */ 6 | }); 7 | -------------------------------------------------------------------------------- /public/src/components/input/Input.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const style = { 4 | styleInputDisabled: { 5 | border: '1px solid #CCCCCC', 6 | padding: '0 5px', 7 | margin: '2px 0', 8 | width: '100%', 9 | height: '32px', 10 | opacity: '.5', 11 | }, 12 | styleInput: { 13 | border: '1px solid #CCCCCC', 14 | padding: '0 5px', 15 | margin: '2px 0', 16 | width: '100%', 17 | borderRadius: '2px', 18 | height: '32px', 19 | }, 20 | styleLabel: { 21 | fontSice: '12px', 22 | fontFamily: '"Roboto", sans-serif', 23 | }, 24 | }; 25 | 26 | const Input = ({ type, id, text, label, disabled, inputRef, defaultValue }) => { 27 | return ( 28 | <div> 29 | {label ? ( 30 | <label style={style.styleLabel} htmlFor={id}> 31 | {label} 32 | </label> 33 | ) : ( 34 | '' 35 | )} 36 | <input 37 | ref={inputRef} 38 | style={!disabled ? style.styleInput : style.styleInputDisabled} 39 | defaultValue={defaultValue ? defaultValue : ''} 40 | disabled={disabled ? disabled : false} 41 | type={type} 42 | id={id} 43 | name={id} 44 | placeholder={text} 45 | /> 46 | </div> 47 | ); 48 | }; 49 | 50 | export default Input; 51 | -------------------------------------------------------------------------------- /public/src/components/input/__snapshots__/input.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`#Test Input Component #Should Input to mach snapshot with label 1`] = ` 4 | <div> 5 | <label 6 | htmlFor="email" 7 | style={ 8 | Object { 9 | "fontFamily": "\\"Roboto\\", sans-serif", 10 | "fontSice": "12px", 11 | } 12 | } 13 | > 14 | Email 15 | </label> 16 | <input 17 | defaultValue="" 18 | disabled={false} 19 | id="email" 20 | name="email" 21 | placeholder="email" 22 | style={ 23 | Object { 24 | "border": "1px solid #CCCCCC", 25 | "borderRadius": "2px", 26 | "height": "32px", 27 | "margin": "2px 0", 28 | "padding": "0 5px", 29 | "width": "100%", 30 | } 31 | } 32 | type="email" 33 | /> 34 | </div> 35 | `; 36 | 37 | exports[`#Test Input Component #Should Input to mach snapshot without label 1`] = ` 38 | <div> 39 | 40 | <input 41 | defaultValue="" 42 | disabled={false} 43 | id="email" 44 | name="email" 45 | placeholder="email" 46 | style={ 47 | Object { 48 | "border": "1px solid #CCCCCC", 49 | "borderRadius": "2px", 50 | "height": "32px", 51 | "margin": "2px 0", 52 | "padding": "0 5px", 53 | "width": "100%", 54 | } 55 | } 56 | type="email" 57 | /> 58 | </div> 59 | `; 60 | -------------------------------------------------------------------------------- /public/src/components/input/input.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import renderer from 'react-test-renderer'; 5 | 6 | import Input from './Input'; 7 | 8 | describe('#Test Input Component', () => { 9 | it('#Should Input to mach snapshot without label', () => { 10 | const tree = renderer.create(<Input type='email' id='email' text='email' value='nath@nath.com.br' />).toJSON(); 11 | expect(tree).toMatchSnapshot(); 12 | }); 13 | 14 | it('#Should Input to mach snapshot with label', () => { 15 | const tree = renderer.create(<Input type='email' id='email' text='email' value='nath@nath.com.br' label='Email' />).toJSON(); 16 | expect(tree).toMatchSnapshot(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /public/src/components/input/input.story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { storiesOf } from '@storybook/react'; 4 | 5 | import Input from './Input'; 6 | 7 | storiesOf('Input', module) 8 | .add('without label 100% width', () => <Input type='email' id='email' text='email' value='nath@nath.com.br' />) 9 | .add('with label 100% width', () => <Input type='email' id='email' text='email' value='nath@nath.com.br' label='Email' />) 10 | .add('disabled without label 100% width', () => <Input type='email' id='email' text='email' value='nath@nath.com.br' disabled='true' />) 11 | .add('disabled with label 100% width', () => <Input type='email' id='email' text='email' value='nath@nath.com.br' label='Email' disabled='true' />); 12 | -------------------------------------------------------------------------------- /public/src/components/list/List.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { getAllItems, removeItem } from '../../logics/CaffeineApi'; 5 | 6 | import Message from '../message/Message'; 7 | import Box from '../box/Box'; 8 | 9 | class List extends Component { 10 | 11 | componentDidMount() { 12 | this.props.list(localStorage.getItem('auth-token'), this.props.match.params.id); 13 | } 14 | 15 | render() { 16 | return ( 17 | <div> 18 | <Message className={this.props.typeMesage} msg={this.props.msg} /> 19 | <Box title='Info capsule' button='Add new capsule' to={`/create/${this.props.match.params.id}`} /> 20 | {this.props.capsules.map(capsule => <Box link={[{ 21 | text: 'edit', 22 | to: `/edit/${this.props.match.params.id}/${capsule._id}` 23 | }, { 24 | text: 'remove', 25 | action: () => { 26 | this.props.removeItem(this.props.match.params.id, capsule._id); 27 | } 28 | }]} key={capsule._id} inputs={[ 29 | { type: 'text', id: 'brand', text: capsule.brand_capsule, label: 'Capsule brand:', disabled: true }, 30 | { type: 'text', id: 'type', text: capsule.type_capsule, label: 'Capsule name:', disabled: true }, 31 | { type: 'number', id: 'price', text: capsule.price_last_buy, label: 'Last price paied:', disabled: true }, 32 | { type: 'number', id: 'weekcapsules', text: capsule.quantity_capsules_per_week, label: 'Quantity capsules per week:', disabled: true }, 33 | { type: 'number', id: 'notify', text: capsule.notify_enf_capsules, label: 'Notify end capsules:', disabled: true } 34 | ]} /> 35 | )} 36 | </div> 37 | ) 38 | } 39 | } 40 | 41 | const mapDispatchToProps = dispatch => { 42 | return { 43 | list: (token, user_id) => { 44 | dispatch(getAllItems(token, user_id)); 45 | }, 46 | removeItem: (user_id, capsule_id) => { 47 | dispatch(removeItem(user_id, capsule_id)); 48 | } 49 | } 50 | } 51 | 52 | const mapStateToProps = state => { 53 | return { 54 | capsules: state.capsulesReducer, 55 | typeMesage: state.messageReducer.typeMesage, 56 | msg: state.messageReducer.msg 57 | } 58 | } 59 | 60 | const ListRedux = connect(mapStateToProps, mapDispatchToProps)(List); 61 | 62 | export default ListRedux; 63 | -------------------------------------------------------------------------------- /public/src/components/login/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import queryString from 'query-string'; 3 | 4 | import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; 5 | 6 | import Message from '../message/Message'; 7 | import Box from '../box/Box'; 8 | 9 | class Login extends Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | msg: queryString.parse(this.props.location.search).msg, 14 | typeMesage: queryString.parse(this.props.location.search).msg ? 'error' : '', 15 | tabIndex: 0, 16 | }; 17 | } 18 | 19 | handleClick(e) { 20 | e.preventDefault(); 21 | 22 | const requestInfo = { 23 | method: 'POST', 24 | body: { user_name: this.login.value, password: this.password.value }, 25 | headers: new Headers({ 26 | 'Content-type': 'application/json', 27 | }), 28 | }; 29 | 30 | let url = 'http://localhost:3000/api/login'; 31 | if (this.email) { 32 | requestInfo.body.user_mail = this.email.value; 33 | url = 'http://localhost:3000/api/createuser'; 34 | } 35 | 36 | requestInfo.body = JSON.stringify(requestInfo.body); 37 | 38 | fetch(url, requestInfo) 39 | .then((response) => response.json()) 40 | .then((response) => { 41 | if (!response.success) { 42 | this.setState({ 43 | msg: response.message, 44 | typeMesage: 'error', 45 | }); 46 | throw new Error(this.state.msg); 47 | } 48 | 49 | if (this.email) { 50 | this.setState({ 51 | msg: 'Usuário criado com sucesso, faça o login no caffeine!!!', 52 | typeMesage: 'success', 53 | tabIndex: 0, 54 | }); 55 | return; 56 | } 57 | 58 | localStorage.setItem('auth-token', JSON.stringify(response.token)); 59 | localStorage.setItem('user', JSON.stringify(response.user)); 60 | this.props.history.push(`/list/${response.user._id}`); 61 | }); 62 | } 63 | 64 | render() { 65 | return ( 66 | <div> 67 | <Message className={this.state.typeMesage} msg={this.state.msg} /> 68 | <form onSubmit={this.handleClick.bind(this)}> 69 | <Tabs selectedIndex={this.state.tabIndex} onSelect={(tabIndex) => this.setState({ tabIndex })}> 70 | <TabPanel> 71 | <Box 72 | title='Login' 73 | text='Im already a caffeine customer.' 74 | inputs={[ 75 | { type: 'text', id: 'login', text: 'login', inputRef: (input) => (this.login = input) }, 76 | { type: 'password', id: 'pass', text: 'password', inputRef: (input) => (this.password = input) }, 77 | ]} 78 | button='Login' 79 | /> 80 | </TabPanel> 81 | <TabPanel> 82 | <Box 83 | title='Register' 84 | text='First time using of caffeine.' 85 | inputs={[ 86 | { type: 'email', id: 'email', text: 'email', inputRef: (input) => (this.email = input) }, 87 | { type: 'text', id: 'login', text: 'login', inputRef: (input) => (this.login = input) }, 88 | { type: 'password', id: 'pass', text: 'password', inputRef: (input) => (this.password = input) }, 89 | ]} 90 | button='Create' 91 | /> 92 | </TabPanel> 93 | <TabList> 94 | <Tab>{`I'm already registered`}</Tab> 95 | <Tab>{`I'm not yet registered`}</Tab> 96 | </TabList> 97 | </Tabs> 98 | </form> 99 | </div> 100 | ); 101 | } 102 | } 103 | 104 | export default Login; 105 | -------------------------------------------------------------------------------- /public/src/components/logout/Logout.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | class Logout extends Component { 4 | // @TODO refactor 5 | UNSAFE_componentWillMount() { 6 | localStorage.removeItem('auth-token'); 7 | localStorage.removeItem('user'); 8 | this.props.history.push('/'); 9 | } 10 | 11 | render() { 12 | return null; 13 | } 14 | } 15 | 16 | export default Logout; 17 | -------------------------------------------------------------------------------- /public/src/components/message/Message.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const style = { 4 | error: { 5 | backgroundColor: '#982d2d', 6 | padding: '15px 0', 7 | textAlign: 'center', 8 | fontSize: '15px', 9 | color: '#fff', 10 | fontFamily: '"Roboto", sans-serif', 11 | }, 12 | success: { 13 | backgroundColor: '#469a46', 14 | padding: '15px 0', 15 | textAlign: 'center', 16 | fontSize: '15px', 17 | color: '#fff', 18 | fontFamily: '"Roboto", sans-serif', 19 | } 20 | } 21 | 22 | const Input = ({ className, msg }) => { 23 | return ( 24 | <div style={style[className]}> 25 | {msg} 26 | </div> 27 | ) 28 | }; 29 | 30 | export default Input; 31 | -------------------------------------------------------------------------------- /public/src/components/message/__snapshots__/message.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`#Test Message Component #Should Message with error 1`] = ` 4 | <div 5 | style={ 6 | Object { 7 | "backgroundColor": "#982d2d", 8 | "color": "#fff", 9 | "fontFamily": "\\"Roboto\\", sans-serif", 10 | "fontSize": "15px", 11 | "padding": "15px 0", 12 | "textAlign": "center", 13 | } 14 | } 15 | > 16 | Error message 17 | </div> 18 | `; 19 | 20 | exports[`#Test Message Component #Should Message with success 1`] = ` 21 | <div 22 | style={ 23 | Object { 24 | "backgroundColor": "#469a46", 25 | "color": "#fff", 26 | "fontFamily": "\\"Roboto\\", sans-serif", 27 | "fontSize": "15px", 28 | "padding": "15px 0", 29 | "textAlign": "center", 30 | } 31 | } 32 | > 33 | Success message 34 | </div> 35 | `; 36 | -------------------------------------------------------------------------------- /public/src/components/message/message.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import renderer from 'react-test-renderer'; 5 | 6 | import Message from './Message'; 7 | 8 | describe('#Test Message Component', () => { 9 | it('#Should Message with success', () => { 10 | const tree = renderer.create(<Message className='success' msg='Success message' />).toJSON(); 11 | expect(tree).toMatchSnapshot(); 12 | }); 13 | 14 | it('#Should Message with error', () => { 15 | const tree = renderer.create(<Message className='error' msg='Error message' />).toJSON(); 16 | expect(tree).toMatchSnapshot(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /public/src/components/message/message.story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { storiesOf } from '@storybook/react'; 4 | 5 | import Message from './Message'; 6 | 7 | storiesOf('Message', module) 8 | .add('success message', () => <Message className='success' msg='Success message' />) 9 | .add('error message', () => <Message className='error' msg='Error message' />); 10 | -------------------------------------------------------------------------------- /public/src/components/notfound/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const style = { 4 | title: { 5 | fontSize: '40px', 6 | textAlign: 'center', 7 | margin: '40px 0', 8 | }, 9 | container: { 10 | maxWidth: '500px', 11 | width: '100%', 12 | display: 'flex', 13 | margin: 'auto', 14 | justifyContent: 'center', 15 | fontSize: '13px', 16 | }, 17 | list: { 18 | margin: '0 5px' 19 | } 20 | } 21 | 22 | const NotFound = () => ( 23 | <div> 24 | <h3 style={style.title}>Page not found :(</h3> 25 | <ul style={style.container}> 26 | <li style={style.list}><a href="/">Login</a></li> 27 | </ul> 28 | </div> 29 | ); 30 | 31 | export default NotFound; 32 | -------------------------------------------------------------------------------- /public/src/components/notfound/__snapshots__/notfound.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`#Test Page NotFound #Should Page NotFound 1`] = ` 4 | <div> 5 | <h3 6 | style={ 7 | Object { 8 | "fontSize": "40px", 9 | "margin": "40px 0", 10 | "textAlign": "center", 11 | } 12 | } 13 | > 14 | Page not found :( 15 | </h3> 16 | <ul 17 | style={ 18 | Object { 19 | "display": "flex", 20 | "fontSize": "13px", 21 | "justifyContent": "center", 22 | "margin": "auto", 23 | "maxWidth": "500px", 24 | "width": "100%", 25 | } 26 | } 27 | > 28 | <li 29 | style={ 30 | Object { 31 | "margin": "0 5px", 32 | } 33 | } 34 | > 35 | <a 36 | href="/" 37 | > 38 | Login 39 | </a> 40 | </li> 41 | </ul> 42 | </div> 43 | `; 44 | -------------------------------------------------------------------------------- /public/src/components/notfound/notfound.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import renderer from 'react-test-renderer'; 5 | 6 | import NotFound from './NotFound'; 7 | 8 | describe('#Test Page NotFound', () => { 9 | it('#Should Page NotFound', () => { 10 | const tree = renderer.create(<NotFound />).toJSON(); 11 | expect(tree).toMatchSnapshot(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /public/src/components/notfound/notfound.story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { storiesOf } from '@storybook/react'; 4 | 5 | import NotFound from './NotFound'; 6 | 7 | storiesOf('NotFound', module).add('without label 100% width', () => <NotFound />); 8 | -------------------------------------------------------------------------------- /public/src/components/title/Title.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const subTitleCenter = { 4 | textAlign: 'center', 5 | fontSize: '20px', 6 | color: '#414141', 7 | fontFamily: '"Roboto", sans-serif' 8 | } 9 | 10 | const Title = ({ title }) => ( 11 | <h2 style={subTitleCenter}>{title}</h2> 12 | ); 13 | 14 | export default Title; 15 | -------------------------------------------------------------------------------- /public/src/components/title/__snapshots__/title.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`#Test Title Component #Should Title to mach snapshot 1`] = ` 4 | <h2 5 | style={ 6 | Object { 7 | "color": "#414141", 8 | "fontFamily": "\\"Roboto\\", sans-serif", 9 | "fontSize": "20px", 10 | "textAlign": "center", 11 | } 12 | } 13 | > 14 | Cadastre-se 15 | </h2> 16 | `; 17 | -------------------------------------------------------------------------------- /public/src/components/title/title.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import renderer from 'react-test-renderer'; 5 | 6 | import Title from './Title'; 7 | 8 | describe('#Test Title Component', () => { 9 | it('#Should Title to mach snapshot', () => { 10 | const tree = renderer.create(<Title title='Cadastre-se' />).toJSON(); 11 | expect(tree).toMatchSnapshot(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /public/src/components/title/title.story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { storiesOf } from '@storybook/react'; 4 | 5 | import Title from './Title'; 6 | 7 | storiesOf('Title on center', module).add('set a title to show', () => <Title title='Cadastre-se' />); 8 | -------------------------------------------------------------------------------- /public/src/css/inline.js: -------------------------------------------------------------------------------- 1 | const style = { 2 | boxform: { 3 | display: 'flex', 4 | width: '100%', 5 | maxWidth: '500px', 6 | margin: '10px auto auto', 7 | border: '1px solid rgb(90, 74, 105)', 8 | borderRadius: '2px', 9 | padding: '10px 0', 10 | backgroundColor: '#fff', 11 | }, 12 | box: { 13 | padding: '0 10px', 14 | width: '100%', 15 | position: 'relative', 16 | }, 17 | containter: { 18 | position: 'relative', 19 | borderBottom: '1px solid #ccc', 20 | margin: '25px 0', 21 | padding: '0 0 15px' 22 | }, 23 | containterLink: { 24 | width: '90%', 25 | right: 0, 26 | position: 'absolute', 27 | textAlign: 'right', 28 | }, 29 | editLink: { 30 | color: '#38387d', 31 | textAlign: 'right', 32 | textTransform: 'uppercase', 33 | margin: '0 15px 0 0', 34 | textDecoration: 'underline', 35 | }, 36 | }; 37 | 38 | export default style; 39 | -------------------------------------------------------------------------------- /public/src/html/template.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | 4 | <head> 5 | <meta charset="UTF-8"> 6 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 | <meta http-equiv="X-UA-Compatible" content="ie=edge"> 8 | <link href="https://fonts.googleapis.com/css?family=Roboto:400,700,900" rel="stylesheet" /> 9 | <title> 10 | <%= htmlWebpackPlugin.options.title %> 11 | 12 | 13 | 14 | 15 |
    16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/src/images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathpaiva/caffeine-redux/5532ea7e7a9fcf73e1859ecd55c08bb938400412/public/src/images/.DS_Store -------------------------------------------------------------------------------- /public/src/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathpaiva/caffeine-redux/5532ea7e7a9fcf73e1859ecd55c08bb938400412/public/src/images/favicon.ico -------------------------------------------------------------------------------- /public/src/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathpaiva/caffeine-redux/5532ea7e7a9fcf73e1859ecd55c08bb938400412/public/src/images/logo.png -------------------------------------------------------------------------------- /public/src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import { render } from 'react-dom'; 5 | import { Provider } from 'react-redux'; 6 | import { BrowserRouter as Router } from 'react-router-dom'; 7 | 8 | import { AppContainer } from 'react-hot-loader'; 9 | import App from './App'; 10 | import ResetCSS from './RestCSS'; 11 | 12 | import { createStore, applyMiddleware, combineReducers } from 'redux'; 13 | import thunkMiddleware from 'redux-thunk'; 14 | import { capsulesReducer } from './reducers/capsules'; 15 | import { messageReducer } from './reducers/message'; 16 | const reducers = combineReducers({ capsulesReducer, messageReducer }) 17 | const store = createStore(reducers, applyMiddleware(thunkMiddleware)); 18 | console.log("store", store) 19 | 20 | 21 | const renderApp = (NextApp) => { 22 | render( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | , 31 | document.querySelector('[data-js="app"]') 32 | ); 33 | }; 34 | 35 | renderApp(App); 36 | 37 | if (module.hot) { 38 | module.hot.accept('./App', () => { 39 | const NextApp = require('./App').default; 40 | renderApp(NextApp); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /public/src/logics/CaffeineApi.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { allCapsules, showMessage } from '../actions/actionCreator'; 4 | 5 | export function getAllItems(token, user_id, type) { 6 | const requests = { 7 | method: 'GET', 8 | headers: new Headers({ 9 | 'Content-type': 'application/json', 10 | 'x-access-token': JSON.parse(token), 11 | }), 12 | }; 13 | let url = `http://localhost:3000/api/auth/capsules/${user_id}`; 14 | if (type === 'one') { 15 | url = `http://localhost:3000/api/auth/capsule/${user_id}`; 16 | } 17 | 18 | return (dispatch) => { 19 | fetch(url, requests) 20 | .then((response) => response.json()) 21 | .then((response) => { 22 | let message = {}; 23 | 24 | if (!response.success) { 25 | message = { 26 | msg: response.message, 27 | typeMesage: 'error', 28 | }; 29 | dispatch(showMessage(message)); 30 | 31 | throw new Error(message.msg); 32 | } 33 | 34 | message = { 35 | msg: '', 36 | typeMesage: '', 37 | }; 38 | 39 | dispatch(showMessage(message)); 40 | let capsules = response.capsules; 41 | if (type === 'one') { 42 | capsules = [capsules]; 43 | } 44 | dispatch(allCapsules(capsules)); 45 | }); 46 | }; 47 | } 48 | 49 | export function createItem(capsule, type) { 50 | let url = `http://localhost:3000/api/auth/capsules/${capsule.user_id}`; 51 | let method = 'POST'; 52 | if (type === 'one') { 53 | method = 'PUT'; 54 | url = `http://localhost:3000/api/auth/capsule/${capsule.capsule_id}`; 55 | } 56 | 57 | delete capsule.capsule_id; 58 | 59 | const request = { 60 | method, 61 | body: JSON.stringify(capsule), 62 | headers: new Headers({ 63 | 'Content-type': 'application/json', 64 | 'x-access-token': JSON.parse(localStorage.getItem('auth-token')), 65 | }), 66 | }; 67 | 68 | return (dispatch) => { 69 | fetch(url, request) 70 | .then((response) => response.json()) 71 | .then((response) => { 72 | let message = {}; 73 | 74 | if (!response.success) { 75 | message = { 76 | msg: 'Error to add capsule', 77 | typeMesage: 'error', 78 | }; 79 | dispatch(showMessage(message)); 80 | 81 | throw new Error(message.msg); 82 | } 83 | 84 | message = { 85 | msg: type === 'one' ? 'Change success!' : 'Create success', 86 | typeMesage: 'success', 87 | }; 88 | dispatch(showMessage(message)); 89 | }); 90 | }; 91 | } 92 | 93 | export function removeItem(user_id, capsule_id) { 94 | const requests = { 95 | method: 'DELETE', 96 | headers: new Headers({ 97 | 'Content-type': 'application/json', 98 | 'x-access-token': JSON.parse(localStorage.getItem('auth-token')), 99 | }), 100 | }; 101 | 102 | const url = `http://localhost:3000/api/auth/capsules/${user_id}/${capsule_id}`; 103 | return (dispatch) => { 104 | fetch(url, requests) 105 | .then((response) => response.json()) 106 | .then((response) => { 107 | let message = {}; 108 | if (!response.success) { 109 | message = { 110 | msg: response.message, 111 | typeMesage: 'error', 112 | }; 113 | dispatch(showMessage(message)); 114 | 115 | throw new Error(message.msg); 116 | } 117 | 118 | message = { 119 | msg: 'Capsula removida com sucesso', 120 | typeMesage: 'success', 121 | }; 122 | 123 | dispatch(showMessage(message)); 124 | dispatch(allCapsules(response.capsules)); 125 | }); 126 | }; 127 | } 128 | -------------------------------------------------------------------------------- /public/src/reducers/capsules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { List } from 'immutable'; 4 | 5 | export function capsulesReducer(state = new List(), action) { 6 | 7 | if (action.type === 'CAPSULES') { 8 | 9 | return new List(action.capsules); 10 | } 11 | 12 | 13 | return state; 14 | } 15 | -------------------------------------------------------------------------------- /public/src/reducers/message.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export function messageReducer(state = {}, action) { 4 | if (action.type === 'MESSAGE') { 5 | return action.message; 6 | } 7 | 8 | return state; 9 | } 10 | -------------------------------------------------------------------------------- /stories/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { storiesOf } from '@storybook/react'; 4 | import { action } from '@storybook/addon-actions'; 5 | import { linkTo } from '@storybook/addon-links'; 6 | 7 | import { Button, Welcome } from '@storybook/react/demo'; 8 | 9 | storiesOf('Welcome', module).add('to Storybook', () => ); 10 | 11 | storiesOf('Button', module) 12 | .add('with text', () => ) 13 | .add('with some emoji', () => ); 14 | -------------------------------------------------------------------------------- /webpack-start.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const webpack = require('webpack'); 4 | const WebpackDevDerver = require('webpack-dev-server'); 5 | const config = require('./webpack/dev.config'); 6 | 7 | new WebpackDevDerver(webpack(config), { 8 | publicPath: config.output.publicPath, 9 | hot: true, 10 | historyApiFallback: true, 11 | stats: { 12 | colors: true 13 | } 14 | }).listen(2000, () => { 15 | console.log('===================================='); 16 | console.log('Servidor rodando na :2000'); 17 | console.log('===================================='); 18 | }); 19 | -------------------------------------------------------------------------------- /webpack/common.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { join } = require('path') 4 | 5 | const paths = { 6 | root: join(__dirname, '..'), 7 | src: join(__dirname, '..', 'public', 'src'), 8 | dist: join(__dirname, '..', 'public', 'dist') 9 | } 10 | 11 | module.exports = { 12 | paths, 13 | 14 | entry: { 15 | main: join(paths.src, 'index') 16 | }, 17 | 18 | output: { 19 | path: paths.dist, 20 | filename: '[name]-[chunkhash].js' 21 | }, 22 | 23 | htmlPluginConfig: { 24 | title: 'Caffeine', 25 | template: join(paths.src, 'html', 'template.html'), 26 | favicon: join(paths.src, 'images', 'favicon.ico') 27 | }, 28 | 29 | jsLoader: { 30 | test: /\.js$/, 31 | include: paths.src, 32 | use: { 33 | loader: 'babel-loader', 34 | options: { 35 | presets: [['@babel/preset-env', { modules: false }], '@babel/preset-react'], 36 | plugins: [ 37 | 'react-hot-loader/babel', 38 | ['@babel/transform-runtime', { 39 | helpers: false, 40 | regenerator: true 41 | }] 42 | ] 43 | } 44 | } 45 | }, 46 | 47 | cssLoader: { 48 | test: /\.scss$/, 49 | include: paths.src, 50 | exclude: /\.useable\.scss$/, 51 | use: [ 52 | { loader: "style-loader" }, 53 | { loader: "css-loader?modules&importLoaders=2&sourceMap&localIdentName=[local]___[hash:base64:5]" }, 54 | { loader: "sass-loader" }] 55 | }, 56 | 57 | cssLoaderUseable: { 58 | test: /\.useable\.scss$/, 59 | use: [ 60 | { loader: "style-loader/useable" }, 61 | { loader: "css-loader?modules&importLoaders=2&sourceMap&localIdentName=[local]___[hash:base64:5]" }, 62 | { loader: "sass-loader" } 63 | ] 64 | }, 65 | 66 | fileLoader: { 67 | test: /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|txt)(\?.*)?$/, 68 | include: paths.src, 69 | use: { 70 | loader: 'file-loader', 71 | options: { 72 | name: 'media/[name].[hash:8].[ext]' 73 | } 74 | } 75 | }, 76 | 77 | urlLoader: { 78 | test: /\.(mp4|webm|wav|mp3|m4a|aac|oga)(\?.*)?$/, 79 | include: paths.src, 80 | use: { 81 | loader: 'url-loader', 82 | options: { 83 | limit: 10000, 84 | name: 'media/[name].[hash:8].[ext]' 85 | } 86 | } 87 | }, 88 | 89 | resolve: { 90 | alias: { 91 | src: paths.src, 92 | components: join(paths.src, 'components'), 93 | utils: join(paths.src, 'utils') 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /webpack/dev.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const webpack = require('webpack') 4 | const common = require('./common') 5 | 6 | const HtmlPlugin = require('html-webpack-plugin') 7 | const DashboardPlugin = require('webpack-dashboard/plugin') 8 | 9 | module.exports = { 10 | devtool: 'source-map', 11 | 12 | entry: [ 13 | 'react-hot-loader/patch', 14 | 'webpack-dev-server/client?http://localhost:2000', 15 | 'webpack/hot/only-dev-server', 16 | common.entry.main 17 | ], 18 | 19 | output: Object.assign({}, common.output, { 20 | filename: '[name].js', 21 | publicPath: '/' 22 | }), 23 | 24 | plugins: [ 25 | new webpack.HotModuleReplacementPlugin(), 26 | new DashboardPlugin(), 27 | 28 | new HtmlPlugin(common.htmlPluginConfig) 29 | ], 30 | 31 | module: { 32 | rules: [ 33 | common.jsLoader, 34 | common.fileLoader, 35 | common.urlLoader 36 | ] 37 | }, 38 | 39 | resolve: common.resolve 40 | } 41 | -------------------------------------------------------------------------------- /webpack/prod.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const webpack = require('webpack'); 4 | const common = require('./common'); 5 | 6 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 8 | 9 | module.exports = { 10 | entry: common.entry, 11 | 12 | output: common.output, 13 | 14 | plugins: [ 15 | new HtmlWebpackPlugin(common.htmlPluginConfig), 16 | new webpack.optimize.OccurrenceOrderPlugin(), 17 | new CleanWebpackPlugin(), 18 | new webpack.DefinePlugin({ 19 | 'process.env': { 20 | 'NODE_ENV': '"production"' 21 | } 22 | }), 23 | ], 24 | 25 | module: { 26 | rules: [ 27 | common.jsLoader, 28 | common.fileLoader, 29 | ] 30 | }, 31 | 32 | resolve: common.resolve 33 | 34 | } 35 | --------------------------------------------------------------------------------