├── .babelrc ├── .editorconfig ├── .eslintrc ├── .flowconfig ├── .github └── FUNDING.yml ├── .gitignore ├── .jestrc ├── .nvmrc ├── .travis.yml ├── Procfile ├── README.md ├── package.json ├── server.js ├── src ├── actions │ ├── list.js │ └── login.js ├── components │ ├── Header.js │ ├── LoginForm.js │ └── Nav.js ├── config │ ├── PrivateRoute.js │ └── routes.js ├── containers │ ├── App.js │ ├── DevTools.js │ ├── HomePage.js │ ├── LoginPage.js │ ├── NotFoundPage.js │ └── Root.js ├── index.html ├── index.js ├── reducers │ ├── index.js │ ├── initialStates │ │ ├── list.js │ │ └── login.js │ ├── list.js │ └── login.js ├── store │ ├── configureStore.dev.js │ ├── configureStore.js │ └── configureStore.prod.js ├── style │ ├── counter.scss │ └── index.scss └── utils │ ├── auth.js │ ├── fetchApi.js │ ├── list.js │ └── wrapActionCreators.js ├── test ├── __mocks__ │ └── sessionStorage.js ├── __setup__.js ├── actions │ ├── list.spec.js │ └── login.spec.js ├── components │ ├── Header.spec.js │ ├── LoginForm.spec.js │ └── __snapshots__ │ │ ├── Header.spec.js.snap │ │ └── LoginForm.spec.js.snap ├── containers │ ├── HomePage.spec.js │ ├── LoginPage.spec.js │ └── __snapshots__ │ │ ├── HomePage.spec.js.snap │ │ └── LoginPage.spec.js.snap ├── reducers │ ├── list.spec.js │ └── login.spec.js └── utils │ ├── auth.spec.js │ ├── fetchApi.spec.js │ └── list.spec.js ├── webpack.config.js └── webpack.config.production.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "flow", "es2015", "stage-0", "stage-3"], 3 | "plugins": ["transform-decorators-legacy","transform-runtime"], 4 | "env": { 5 | "development": { 6 | "presets": ["react-hmre"], 7 | }, 8 | "test": { 9 | "plugins": ["istanbul"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "jest": true 8 | }, 9 | "rules": { 10 | "react/jsx-no-bind": 0, 11 | "react/sort-comp": 0, 12 | "comma-dangle": [2, "never"], 13 | "no-shadow": 0, 14 | "brace-style": [2, "stroustrup"] 15 | }, 16 | "plugins": [ 17 | "react" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/.* 3 | .*/test/.* 4 | .*/coverage/.* 5 | [include] 6 | ./src/**/.* 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ricardodantas 2 | custom: https://www.buymeacoffee.com/ricardodantas 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules 3 | *.log 4 | *.js.map 5 | *.css.map 6 | coverage/ 7 | .nyc_output 8 | .coveralls.yml 9 | -------------------------------------------------------------------------------- /.jestrc: -------------------------------------------------------------------------------- 1 | { 2 | "testRegex": "(/test/.*|(\\.|/))\\.spec\\.js?$", 3 | "setupTestFrameworkScriptFile": "/test/__setup__.js", 4 | "collectCoverage": true, 5 | "coveragePathIgnorePatterns": [ 6 | "/test", 7 | "/node_modules", 8 | "/coverage", 9 | "/build" 10 | ], 11 | "coverageReporters": ["lcov"], 12 | "coverageThreshold": { 13 | "global": { 14 | "branches": 30, 15 | "functions": 30, 16 | "lines": 30, 17 | "statements": 30 18 | } 19 | }, 20 | "moduleFileExtensions": [ 21 | "js", 22 | "json" 23 | ], 24 | "modulePathIgnorePatterns": [ 25 | "/node_modules", 26 | "/coverage", 27 | "/build" 28 | ], 29 | "moduleNameMapper": { 30 | "^.+\\.(scss)$": "/node_modules/jest-css-modules" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 6.10.3 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | script: 3 | - npm run coveralls 4 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm run production 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [ ![Codeship Status for ricardodantas/sample-react-router-redux-jwt](https://app.codeship.com/projects/a231ed90-1cbb-0135-858e-0ec2c05f0faf/status?branch=master)](https://app.codeship.com/projects/220092) 2 | [![Build Status](https://travis-ci.org/ricardodantas/sample-react-router-redux-jwt.svg?branch=master)](https://travis-ci.org/ricardodantas/sample-react-router-redux-jwt) 3 | [![Coverage Status](https://coveralls.io/repos/github/ricardodantas/sample-react-router-redux-jwt/badge.svg?branch=master)](https://coveralls.io/github/ricardodantas/sample-react-router-redux-jwt?branch=master) 4 | 5 | ## Description: 6 | This project is a sample that shows how to use webpack 2, flow, css-modules, scss, react, react-router v4, redux (with thunk), JWT auth, jest, enzyme, ES6, babel and axios. 7 | 8 | ## Live demo: 9 | [https://sample-react-router-redux-jwt.herokuapp.com/](https://sample-react-router-redux-jwt.herokuapp.com/) 10 | 11 | ## Setup: 12 | 13 | ```sh 14 | npm install 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```sh 20 | $ npm run test # run tests 21 | 22 | $ npm run test:watch # watch tests 23 | 24 | $ npm start #dev mode 25 | 26 | $ node run production #build package and start server 27 | ``` 28 | 29 | ## Deploy 30 | ```sh 31 | $ npm run build #build for production 32 | 33 | $ git push heroku master 34 | ``` 35 | 36 | ## TODO: 37 | * Add tests with [http://codecept.io/](http://codecept.io/) 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample-react-router-redux-jwt", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "production": "node server.js", 8 | "start": "webpack-dev-server --open", 9 | "clean": "rimraf dist", 10 | "lint": "eslint src test", 11 | "test": "cross-env NODE_ENV=test jest --config .jestrc", 12 | "test:watch": "cross-env NODE_ENV=test jest --config .jestrc --watchAll", 13 | "check": "npm run flow && npm run lint && npm test", 14 | "build": "npm run clean && cross-env NODE_ENV=production webpack src/index.js --config webpack.config.production.js", 15 | "preversion": "npm run clean && npm run check", 16 | "postversion": "git push && git push --tags && npm run clean", 17 | "prepublish": "npm run clean && npm run build", 18 | "coveralls": "npm run test && coveralls < coverage/lcov.info" 19 | }, 20 | "devDependencies": { 21 | "babel-cli": "^6.24.1", 22 | "babel-core": "^6.24.1", 23 | "babel-eslint": "^5.0.0", 24 | "babel-jest": "^20.0.1", 25 | "babel-loader": "^6.4.1", 26 | "babel-plugin-istanbul": "^4.1.3", 27 | "babel-plugin-syntax-async-functions": "^6.13.0", 28 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 29 | "babel-plugin-transform-runtime": "^6.23.0", 30 | "babel-polyfill": "^6.23.0", 31 | "babel-preset-es2015": "^6.24.1", 32 | "babel-preset-flow": "^6.23.0", 33 | "babel-preset-react": "^6.22.0", 34 | "babel-preset-react-hmre": "^1.1.0", 35 | "babel-preset-stage-0": "^6.24.1", 36 | "babel-preset-stage-3": "^6.24.1", 37 | "coveralls": "^2.13.1", 38 | "cross-env": "^1.0.7", 39 | "css-loader": "^0.23.1", 40 | "detect-port": "^1.1.1", 41 | "enzyme": "^2.7.1", 42 | "eslint": "^1.10.3", 43 | "eslint-config-airbnb": "^5.0.1", 44 | "eslint-plugin-react": "^3.16.1", 45 | "extract-text-webpack-plugin": "^2.0.0-rc.2", 46 | "flow-bin": "^0.46.0", 47 | "generator-redux-stack": "^9.0.0", 48 | "html-webpack-plugin": "^2.10.0", 49 | "jest": "^19.0.0", 50 | "jest-css-modules": "^1.1.0", 51 | "jest-fetch-mock": "^1.1.1", 52 | "node-sass": "^3.7.0", 53 | "nyc": "^10.3.2", 54 | "portfinder": "^1.0.13", 55 | "react-css-modules": "^3.7.6", 56 | "react-test-renderer": "^15.4.2", 57 | "redux-devtools": "^3.1.1", 58 | "redux-devtools-diff-monitor": "^5.0.5", 59 | "redux-devtools-dock-monitor": "^1.1.1", 60 | "redux-devtools-log-monitor": "^1.2.0", 61 | "redux-import-export-monitor": "^1.0.0", 62 | "redux-logger": "^2.5.2", 63 | "redux-mock-store": "^1.2.3", 64 | "redux-slider-monitor": "^1.0.7", 65 | "regenerator-runtime": "^0.10.5", 66 | "rimraf": "^2.5.2", 67 | "sass-loader": "^3.2.0", 68 | "sinon": "^1.17.3", 69 | "style-loader": "^0.13.0", 70 | "webpack": "^2.2.1", 71 | "webpack-dev-server": "^2.2.0", 72 | "yo": "^1.8.5" 73 | }, 74 | "dependencies": { 75 | "axios": "^0.16.1", 76 | "express": "^4.15.2", 77 | "history": "^4.6.1", 78 | "lodash": "^4.17.4", 79 | "moment": "^2.18.1", 80 | "prop-types": "^15.5.8", 81 | "react": "^15.0.0", 82 | "react-audio-player": "^0.4.1", 83 | "react-dom": "^15.0.0", 84 | "react-redux": "^4.4.0", 85 | "react-router-dom": "^4.1.1", 86 | "react-router-redux": "^5.0.0-alpha.4", 87 | "redux": "^3.3.1", 88 | "redux-actions": "^1.2.1", 89 | "redux-thunk": "^1.0.3", 90 | "semantic-ui-react": "^0.68.3" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | var path = require('path'); 4 | var port = process.env.PORT || 3000; 5 | process.env.PWD = process.cwd(); 6 | 7 | app.use(express.static(path.join(process.env.PWD, 'dist'))); // "public" off of current is root 8 | 9 | app.get('/*', function(req, res){ 10 | res.sendFile(__dirname + '/dist/index.html'); 11 | }); 12 | 13 | app.listen(port); 14 | 15 | console.log('Listening on port '+port); 16 | -------------------------------------------------------------------------------- /src/actions/list.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions' 2 | import apiList from '../utils/list' 3 | import {isEmpty} from 'lodash' 4 | 5 | export const listFetching = createAction('LIST_FETCHING') 6 | export const listError = createAction('LIST_ERROR') 7 | export const listSuccess = createAction('LIST_SUCCESS') 8 | 9 | export function getList() { 10 | return async dispatch => { 11 | dispatch(listFetching()) 12 | try { 13 | const {data} = await apiList.getList() 14 | dispatch(listSuccess(data)) 15 | } catch(e) { 16 | dispatch(listError({message: e.message})) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/actions/login.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions' 2 | import {isEmpty} from 'lodash' 3 | import axios from 'axios' 4 | import apiAuth from '../utils/auth' 5 | export const loginFetching = createAction('LOGIN_FETCHING') 6 | export const loginError = createAction('LOGIN_ERROR') 7 | export const loginSuccess = createAction('LOGIN_SUCCESS') 8 | export const logout = createAction('LOGOUT', () => apiAuth.removeToken()) 9 | 10 | export function login(email, password) { 11 | return async (dispatch) => { 12 | dispatch(loginFetching()) 13 | try { 14 | const {data} = await apiAuth.login(email, password) 15 | dispatch(loginSuccess({data})) 16 | }catch (e) { 17 | dispatch(loginError({message: e.message})) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export default class Header extends Component { 5 | static propTypes = { 6 | children: PropTypes.any.isRequired, 7 | } 8 | 9 | render() { 10 | const { children } = this.props 11 | 12 | return ( 13 |
14 | {children} 15 |
16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | // import cssModules from 'react-css-modules' 4 | // import styles from '../style/counter.scss' 5 | import { Button, Form, Input, Container, Message } from 'semantic-ui-react' 6 | import _ from 'lodash' 7 | 8 | // @cssModules(styles) 9 | 10 | export default class LoginForm extends Component { 11 | 12 | static propTypes = { 13 | // styles: PropTypes.object, 14 | handleChange: PropTypes.func.isRequired, 15 | handleSubmit: PropTypes.func.isRequired, 16 | response: PropTypes.object, 17 | } 18 | 19 | render() { 20 | const { response, handleChange, handleSubmit} = this.props 21 | 22 | return ( 23 | 24 |

Login

25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 38 |
39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Nav.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import { Link } from 'react-router-dom' 5 | import { Container } from 'semantic-ui-react' 6 | import {logout} from '../actions/login' 7 | 8 | class Nav extends Component { 9 | 10 | static propTypes = { 11 | isAuthenticated: PropTypes.bool.isRequired, 12 | logout: PropTypes.func.isRequired, 13 | history: PropTypes.object.isRequired, 14 | } 15 | 16 | logout(event) { 17 | event.preventDefault() 18 | const {dispatch, logout, history } = this.props 19 | dispatch(logout()) 20 | history.push('/login') 21 | } 22 | 23 | render() { 24 | const {isAuthenticated} = this.props 25 | 26 | if(!isAuthenticated) { 27 | return null 28 | } 29 | 30 | return ( 31 |
32 | 33 | Home Page 34 | 39 | 40 |
41 | ) 42 | } 43 | } 44 | 45 | 46 | const mapDispatchToProps = (dispatch) => { 47 | return { 48 | dispatch, 49 | logout 50 | } 51 | } 52 | 53 | const mapStateToProps = (state) => { 54 | return {} 55 | } 56 | 57 | export default connect(mapStateToProps, mapDispatchToProps)(Nav) 58 | -------------------------------------------------------------------------------- /src/config/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route, Redirect } from 'react-router-dom' 3 | 4 | const PrivateRoute = ({ component: Component, ...rest }) => ( 5 | ( 6 | !_.isEmpty(sessionStorage.getItem('token')) ? ( 7 | 8 | ) : ( 9 | 13 | ) 14 | )}/> 15 | ) 16 | 17 | export default PrivateRoute 18 | -------------------------------------------------------------------------------- /src/config/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom' 3 | import LoginPage from '../containers/LoginPage' 4 | import HomePage from '../containers/HomePage' 5 | import NotFoundPage from '../containers/NotFoundPage' 6 | import PrivateRoute from './PrivateRoute' 7 | 8 | export default ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ) 17 | -------------------------------------------------------------------------------- /src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import { withRouter } from 'react-router-dom' 5 | import Header from '../components/Header' 6 | import Nav from '../components/Nav' 7 | import { Container } from 'semantic-ui-react' 8 | // import styles from '../style/index.scss' 9 | // import cssModules from 'react-css-modules' 10 | 11 | 12 | // @cssModules(styles) 13 | 14 | class App extends Component { 15 | static propTypes = { 16 | dispatch: PropTypes.func.isRequired, 17 | children: PropTypes.any.isRequired, 18 | styles: PropTypes.object, 19 | isAuthenticated: PropTypes.bool.isRequired, 20 | match: PropTypes.object.isRequired, 21 | location: PropTypes.object.isRequired, 22 | history: PropTypes.object.isRequired, 23 | } 24 | 25 | render() { 26 | const { children, styles, isAuthenticated, history} = this.props 27 | 28 | return ( 29 |
30 |
31 |
33 | 34 | {children} 35 | 36 |
37 | ) 38 | } 39 | } 40 | 41 | 42 | const mapDispatchToProps = (dispatch) => { 43 | return { 44 | dispatch 45 | } 46 | } 47 | 48 | const mapStateToProps = (state) => { 49 | 50 | return { 51 | isAuthenticated: state.login.isAuthenticated 52 | } 53 | } 54 | 55 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(App)) 56 | -------------------------------------------------------------------------------- /src/containers/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevTools } from 'redux-devtools'; 3 | import DockMonitor from 'redux-devtools-dock-monitor'; 4 | import LogMonitor from 'redux-devtools-log-monitor'; 5 | import DiffMonitor from 'redux-devtools-diff-monitor'; 6 | import SliderMonitor from 'redux-slider-monitor'; 7 | import ImportExportMonitor from 'redux-import-export-monitor'; 8 | 9 | export default createDevTools( 10 | 16 | 17 | 18 | 19 | 20 | ); 21 | 22 | export const ImportExportTool = createDevTools( 23 | 24 | ); 25 | -------------------------------------------------------------------------------- /src/containers/HomePage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import {getList} from '../actions/list' 5 | import { Dimmer, Loader, Card, Rating } from 'semantic-ui-react' 6 | import _ from 'lodash' 7 | import moment from 'moment' 8 | import ReactAudioPlayer from 'react-audio-player'; 9 | 10 | class HomePage extends Component { 11 | static propTypes = { 12 | list: PropTypes.object, 13 | } 14 | 15 | componentDidMount() { 16 | const {dispatch} = this.props 17 | dispatch(getList()) 18 | } 19 | 20 | get items() { 21 | const {list} = this.props 22 | const {items} = list 23 | 24 | if(_.isEmpty(items)) { 25 | return null 26 | } 27 | 28 | return items.map((item) => { 29 | const duration = parseInt(moment.duration(item.duration, 'seconds').asMinutes()) 30 | const created = moment(item.created).from(new Date) 31 | 32 | return ( 33 | 34 | 35 | 36 | 37 | 38 | 39 | Duration: {duration} minutes | Created: {created} 40 | 41 | 42 | {item.final_script} 43 | 44 | 45 | 46 | 47 | 48 | 49 | ) 50 | }) 51 | } 52 | 53 | render() { 54 | const {list} = this.props 55 | 56 | if(list.isFetching) { 57 | return ( 58 | 59 | 60 | 61 | ) 62 | } 63 | 64 | return ( 65 |
66 |

Audio Recordings

67 | 68 | {this.items} 69 | 70 |
71 | ); 72 | } 73 | } 74 | 75 | const mapDispatchToProps = (dispatch) => { 76 | return { 77 | dispatch 78 | } 79 | } 80 | 81 | const mapStateToProps = (state) => { 82 | 83 | return { 84 | list: state.list, 85 | } 86 | } 87 | 88 | export default connect(mapStateToProps, mapDispatchToProps)(HomePage) 89 | -------------------------------------------------------------------------------- /src/containers/LoginPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import {Redirect} from 'react-router-dom' 4 | import { connect } from 'react-redux' 5 | import LoginForm from '../components/LoginForm' 6 | import {login} from '../actions/login' 7 | import { Dimmer, Loader, Header } from 'semantic-ui-react' 8 | 9 | class LoginPage extends Component { 10 | 11 | static propTypes = { 12 | login: PropTypes.func.isRequired, 13 | requestLogin: PropTypes.object.isRequired, 14 | } 15 | 16 | static defaultProps = { 17 | requestLogin: {} 18 | } 19 | 20 | state = { 21 | email: '', 22 | password: '', 23 | } 24 | 25 | handleChange = (e, { name, value }) => this.setState({ [name]: value }) 26 | 27 | handleSubmit = e => { 28 | e.preventDefault() 29 | 30 | const {dispatch, login } = this.props 31 | const { email, password } = this.state 32 | 33 | dispatch(login(email, password)) 34 | } 35 | 36 | render() { 37 | const {requestLogin, location} = this.props 38 | const {isAuthenticated} = requestLogin 39 | 40 | if(isAuthenticated) { 41 | return 42 | } 43 | 44 | return ( 45 |
46 |
47 | 48 | Sample React, Redux, Rouer and JWT Auth 49 | 50 |
51 | 52 | 53 | 54 | 55 |
56 | ) 57 | } 58 | } 59 | 60 | const mapDispatchToProps = (dispatch) => { 61 | return { 62 | login, 63 | dispatch 64 | } 65 | } 66 | 67 | const mapStateToProps = (state) => { 68 | return { 69 | requestLogin: state.login 70 | } 71 | } 72 | 73 | export default connect(mapStateToProps, mapDispatchToProps)(LoginPage) 74 | -------------------------------------------------------------------------------- /src/containers/NotFoundPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class NotFoundPage extends Component { 4 | render() { 5 | return ( 6 |
7 |

404 - Page Not Found

8 |
9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/containers/Root.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Provider } from 'react-redux'; 4 | import { ConnectedRouter } from 'react-router-redux'; 5 | import App from './App'; 6 | import DevTools, { ImportExportTool } from './DevTools'; 7 | import routes from '../config/routes.js'; 8 | 9 | export default class Root extends Component { 10 | static propTypes = { 11 | store: PropTypes.object.isRequired, 12 | history: PropTypes.object.isRequired 13 | }; 14 | 15 | render() { 16 | const { store, history } = this.props; 17 | 18 | const isDevEnv = process.env.NODE_ENV === 'development'; 19 | 20 | return ( 21 | 22 | 23 |
24 | {routes} 25 | 26 | {isDevEnv && 27 |
28 | 29 | 30 |
31 | } 32 |
33 |
34 |
35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | sample-react-router-redux-jwt 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import createHistory from 'history/createBrowserHistory'; 4 | import configureStore from './store/configureStore'; 5 | import Root from './containers/Root'; 6 | 7 | const store = configureStore(); 8 | const history = createHistory(); 9 | 10 | render( 11 | , 12 | document.getElementById('root') 13 | ); 14 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { routerReducer as routing } from 'react-router-redux' 3 | import login from './login' 4 | import list from './list' 5 | 6 | const rootReducer = combineReducers({ 7 | login, 8 | list, 9 | routing 10 | }) 11 | 12 | export default rootReducer 13 | -------------------------------------------------------------------------------- /src/reducers/initialStates/list.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export const initialState: { 3 | isFetching: boolean, 4 | hasFailed: boolean, 5 | hasSuccess: boolean, 6 | items: Array, 7 | } = { 8 | isFetching: false, 9 | hasFailed: false, 10 | hasSuccess: false, 11 | items: [], 12 | } 13 | -------------------------------------------------------------------------------- /src/reducers/initialStates/login.js: -------------------------------------------------------------------------------- 1 | import {isEmpty} from 'lodash' 2 | import apiAuth from '../../utils/auth' 3 | 4 | // @flow 5 | export const initialState: { 6 | isFetching: boolean, 7 | hasFailed: boolean, 8 | hasSuccess: boolean, 9 | response: Object, 10 | } = { 11 | isAuthenticated: apiAuth.isAuthenticated(), 12 | isFetching: false, 13 | hasFailed: false, 14 | hasSuccess: false, 15 | response: {}, 16 | } 17 | -------------------------------------------------------------------------------- /src/reducers/list.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions' 2 | import {listFetching, listSuccess, listError} from '../actions/list' 3 | import {initialState} from './initialStates/list' 4 | // @flow 5 | export default handleActions({ 6 | 7 | [listFetching]: (state: Object, action: Object) => { 8 | return Object.assign({}, state, { 9 | isFetching: true, 10 | hasFailed: false, 11 | hasSuccess: false, 12 | items: [] 13 | }) 14 | }, 15 | [listSuccess]: (state: Object, action: Object) => { 16 | return Object.assign({}, state, { 17 | isFetching: false, 18 | hasFailed: false, 19 | hasSuccess: true, 20 | items: action.payload && action.payload.results ? action.payload.results : [] 21 | }) 22 | }, 23 | [listError]: (state: Object, action: Object) => { 24 | return Object.assign({}, state, { 25 | isFetching: false, 26 | hasFailed: true, 27 | hasSuccess: false, 28 | items: [] 29 | }) 30 | }, 31 | }, initialState: Object) 32 | -------------------------------------------------------------------------------- /src/reducers/login.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions' 2 | import {login, logout, doLogout, loginFetching, loginSuccess, loginError} from '../actions/login' 3 | import {initialState} from './initialStates/login' 4 | // @flow 5 | export default handleActions({ 6 | 7 | [logout]: (state: Object, action: Object) => { 8 | return Object.assign({}, state, { 9 | isAuthenticated: false, 10 | isFetching: false, 11 | hasFailed: false, 12 | hasSuccess: false, 13 | response: {}, 14 | }) 15 | }, 16 | [loginFetching]: (state: Object, action: Object) => { 17 | return Object.assign({}, state, { 18 | isAuthenticated: false, 19 | isFetching: true, 20 | hasSuccess: false, 21 | hasFailed: false, 22 | response: {} 23 | }) 24 | }, 25 | [loginSuccess]: (state: Object, action: Object) => { 26 | return Object.assign({}, state, { 27 | isAuthenticated: true, 28 | isFetching: false, 29 | hasSuccess: true, 30 | hasFailed: false, 31 | response: action.payload || {} 32 | }) 33 | }, 34 | [loginError]: (state: Object, action: Object) => { 35 | return Object.assign({}, state, { 36 | isAuthenticated: false, 37 | isFetching: false, 38 | hasFailed: true, 39 | hasSuccess: false, 40 | response: action.payload || {} 41 | }) 42 | }, 43 | }, initialState: Object) 44 | -------------------------------------------------------------------------------- /src/store/configureStore.dev.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import { routerMiddleware } from 'react-router-redux'; 3 | import thunk from 'redux-thunk'; 4 | import createHistory from 'history/createBrowserHistory'; 5 | import createLogger from 'redux-logger'; 6 | import { persistState } from 'redux-devtools'; 7 | import rootReducer from '../reducers'; 8 | import DevTools from '../containers/DevTools'; 9 | 10 | const logger = createLogger({ 11 | level: 'info', 12 | collapsed: true 13 | }); 14 | 15 | const router = routerMiddleware(createHistory()); 16 | 17 | export default function configureStore(initialState) { 18 | const store = createStore( 19 | rootReducer, 20 | initialState, 21 | compose( 22 | applyMiddleware(thunk, router, logger), 23 | DevTools.instrument(), 24 | persistState( 25 | window.location.href.match( 26 | /[?&]debug_session=([^&]+)\b/ 27 | ) 28 | ) 29 | ) 30 | ); 31 | 32 | if (module.hot) { 33 | module.hot.accept('../reducers', () => { 34 | const nextRootReducer = require('../reducers/index').default; 35 | store.replaceReducer(nextRootReducer); 36 | }); 37 | } 38 | 39 | return store; 40 | } 41 | -------------------------------------------------------------------------------- /src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | const { NODE_ENV } = process.env; 2 | 3 | if (NODE_ENV === 'production' || NODE_ENV === 'test') { 4 | module.exports = require('./configureStore.prod'); 5 | } 6 | else { 7 | module.exports = require('./configureStore.dev'); 8 | } 9 | -------------------------------------------------------------------------------- /src/store/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import { routerMiddleware } from 'react-router-redux'; 3 | import createHistory from 'history/createBrowserHistory'; 4 | import thunk from 'redux-thunk'; 5 | import rootReducer from '../reducers'; 6 | 7 | const router = routerMiddleware(createHistory()); 8 | 9 | export default function configureStore(initialState) { 10 | return createStore( 11 | rootReducer, 12 | initialState, 13 | applyMiddleware(thunk, router) 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/style/counter.scss: -------------------------------------------------------------------------------- 1 | .counter { 2 | min-width: 90px; 3 | display: inline-block; 4 | } 5 | 6 | .button { 7 | margin-left: 5px; 8 | background: #eee; 9 | border: 1px solid #ccc; 10 | transition: background 0.2s; 11 | 12 | &:hover { 13 | background: #ddd; 14 | cursor: pointer; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/style/index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricardodantas/sample-react-router-redux-jwt/74c16a6d5dc7042f263062058735280b2d05f62b/src/style/index.scss -------------------------------------------------------------------------------- /src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import FetchApi from './fetchApi' 2 | import {isEmpty} from 'lodash' 3 | 4 | // @flow 5 | export default class Auth extends FetchApi { 6 | 7 | static isAuthenticated(): boolean { 8 | return !isEmpty(this.getToken()) 9 | } 10 | 11 | static getToken(): string { 12 | return sessionStorage.getItem('token') 13 | } 14 | 15 | static removeToken(): any { 16 | return sessionStorage.clear() 17 | } 18 | 19 | static storeToken(token: string = '') { 20 | if (isEmpty(token)) { 21 | throw new Error("Token is empty.") 22 | } 23 | 24 | sessionStorage.setItem('token', token) 25 | return token 26 | } 27 | 28 | static async login(email: string, password: string) { 29 | try { 30 | const response = await super.post('core/login/', { 31 | email, 32 | password 33 | }) 34 | return this.storeToken(response.data.token) 35 | } catch (e) { 36 | throw new Error(e.message) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/fetchApi.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import {isEmpty} from 'lodash' 3 | 4 | // @flow 5 | export default class FetchApi { 6 | 7 | static baseEndpoint: string = 'https://i2x-challenge.herokuapp.com' 8 | 9 | static config (headers: Object = {}, anyParam: Object = {}): Object { 10 | return { 11 | headers: this.headers(headers), 12 | ...anyParam 13 | } 14 | } 15 | 16 | static headers(headerParams: Object = {}): Object { 17 | return { 18 | "content-type": "application/json", 19 | ...headerParams 20 | } 21 | } 22 | 23 | static setConfig(token: string = ''): Object { 24 | let config = this.config() 25 | 26 | if (!isEmpty(token)) { 27 | config = Object.assign({}, this.config(this.headers({"Authorization": `JWT ${token}`}))) 28 | } 29 | 30 | return config 31 | } 32 | 33 | static async post(url: string= '', data: Object = {}, token: string = '') { 34 | const config = this.setConfig(token) 35 | return await axios.post(`${this.baseEndpoint}/${url}`, data, config) 36 | } 37 | 38 | static async get(url: string = '', token: string = '') { 39 | const config = this.setConfig(token) 40 | return await axios.get(`${this.baseEndpoint}/${url}`, config) 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/list.js: -------------------------------------------------------------------------------- 1 | // import FetchApi from './fetchApi' 2 | import Auth from './auth' 3 | import {isEmpty} from 'lodash' 4 | 5 | export default class List extends Auth { 6 | 7 | static async getList() { 8 | try { 9 | return await super.get('ai/recording/list/', super.getToken()) 10 | } catch (e) { 11 | throw new Error(e.message) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/wrapActionCreators.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | 3 | export default function wrapActionCreators(actionCreators) { 4 | return dispatch => bindActionCreators(actionCreators, dispatch); 5 | } 6 | -------------------------------------------------------------------------------- /test/__mocks__/sessionStorage.js: -------------------------------------------------------------------------------- 1 | var localStorageMock = (function() { 2 | var store = {}; 3 | return { 4 | getItem: function(key) { 5 | return store[key]; 6 | }, 7 | setItem: function(key, value) { 8 | store[key] = value.toString(); 9 | }, 10 | clear: function() { 11 | store = {}; 12 | } 13 | }; 14 | })(); 15 | Object.defineProperty(window, 'localStorage', { value: localStorageMock }); 16 | Object.defineProperty(window, 'sessionStorage', { value: localStorageMock }); 17 | -------------------------------------------------------------------------------- /test/__setup__.js: -------------------------------------------------------------------------------- 1 | require('./__mocks__/sessionStorage') 2 | global.axios = require('axios') 3 | -------------------------------------------------------------------------------- /test/actions/list.spec.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../../src/actions/list' 2 | import thunk from 'redux-thunk' 3 | import configureMockStore from 'redux-mock-store' 4 | 5 | const middlewares = [thunk] 6 | const mockStore = configureMockStore(middlewares) 7 | 8 | describe('list actions', () => { 9 | 10 | it('should listFetching action exists', () => { 11 | expect(actions.listFetching().type).toEqual('LIST_FETCHING') 12 | }) 13 | 14 | it('should listSuccess action exists', () => { 15 | expect(actions.listSuccess().type).toEqual('LIST_SUCCESS') 16 | }) 17 | 18 | it('should listError action exists', () => { 19 | expect(actions.listError().type).toEqual('LIST_ERROR') 20 | }) 21 | 22 | }) 23 | -------------------------------------------------------------------------------- /test/actions/login.spec.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../../src/actions/login' 2 | import thunk from 'redux-thunk' 3 | import configureMockStore from 'redux-mock-store' 4 | 5 | const middlewares = [thunk] 6 | const mockStore = configureMockStore(middlewares) 7 | 8 | describe('Login actions', () => { 9 | 10 | it('should logout action exists', () => { 11 | expect(actions.logout().type).toEqual('LOGOUT') 12 | }) 13 | 14 | it('should loginFetching action exists', () => { 15 | expect(actions.loginFetching().type).toEqual('LOGIN_FETCHING') 16 | }) 17 | 18 | it('should loginSuccess action exists', () => { 19 | expect(actions.loginSuccess().type).toEqual('LOGIN_SUCCESS') 20 | }) 21 | 22 | it('should loginError action exists', () => { 23 | expect(actions.loginError().type).toEqual('LOGIN_ERROR') 24 | }) 25 | 26 | // @TODO: Move to "libs" or "utils" folder structure 27 | // it('should fetchLogin action auhtorized', async () => { 28 | // 29 | // const {data} = await actions.fetchLogin('challenge@i2x.ai', 'pass123') 30 | // expect(data).toEqual(expect.objectContaining({ 31 | // token: expect.any(String) 32 | // })) 33 | // }) 34 | // 35 | // it('should fetchLogin action not be auhtorized', async () => { 36 | // const response = await actions.fetchLogin('awssad', 'asdsad') 37 | // expect(response).toContain('Request failed with status code 400') 38 | // }) 39 | 40 | }) 41 | -------------------------------------------------------------------------------- /test/components/Header.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { spy } from 'sinon' 3 | import { shallow } from 'enzyme' 4 | import renderer from 'react-test-renderer' 5 | import Header from '../../src/components/Header' 6 | import Nav from '../../src/components/Nav' 7 | 8 | describe('
', () => { 9 | 10 | it('should render', () => { 11 | const tree = renderer.create( 12 |
13 | ).toJSON() 14 | 15 | expect(tree).toMatchSnapshot() 16 | }) 17 | 18 | }) 19 | -------------------------------------------------------------------------------- /test/components/LoginForm.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { spy } from 'sinon' 3 | import { shallow } from 'enzyme' 4 | import renderer from 'react-test-renderer' 5 | import LoginForm from '../../src/components/LoginForm' 6 | 7 | describe('', () => { 8 | 9 | beforeEach(() => { 10 | 11 | global.handleChange = spy() 12 | global.handleSubmit = spy() 13 | global.response = { 14 | response: { } 15 | } 16 | }) 17 | 18 | it('should render', () => { 19 | const tree = renderer.create( 20 | 25 | ).toJSON() 26 | 27 | expect(tree).toMatchSnapshot() 28 | }) 29 | 30 | it('should call handleSubmit when form is submited', () => { 31 | const component = shallow( 32 | 37 | ) 38 | 39 | const form = component.find('Form') 40 | form.simulate('submit') 41 | 42 | expect(global.handleSubmit.calledOnce).toEqual(true) 43 | }) 44 | 45 | it('should call handleChange when input is changed', () => { 46 | const component = shallow( 47 | 52 | ) 53 | 54 | component.find('Input[name="email"]').simulate('change') 55 | component.find('Input[name="password"]').simulate('change') 56 | 57 | expect(global.handleChange.calledTwice).toEqual(true) 58 | }) 59 | 60 | it('should show error response', () => { 61 | const response = { 62 | hasFailed: true, 63 | response: { 64 | message: 'Something is wrong.' 65 | } 66 | } 67 | const component = shallow( 68 | 73 | ) 74 | 75 | const errorMessage = component.find('Message') 76 | 77 | expect(errorMessage.props().hidden).toEqual(false) 78 | expect(errorMessage.find('p').text()).toEqual(response.response.message) 79 | }) 80 | 81 | }) 82 | -------------------------------------------------------------------------------- /test/components/__snapshots__/Header.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`
should render 1`] = ` 4 |
5 |
6 |
7 | `; 8 | -------------------------------------------------------------------------------- /test/components/__snapshots__/LoginForm.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` should render 1`] = ` 4 |
7 |

8 | Login 9 |

10 |
14 |
17 |
20 | 26 |
27 |
28 |
31 |
34 | 40 |
41 |
42 | 50 |
51 |
54 |
57 | Ops... 58 |
59 |

60 |

61 |
62 | `; 63 | -------------------------------------------------------------------------------- /test/containers/HomePage.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Provider } from 'react-redux' 3 | import { mount } from 'enzyme' 4 | import renderer from 'react-test-renderer' 5 | import HomePage from '../../src/containers/HomePage' 6 | import Nav from '../../src/components/Nav' 7 | import configureStore from '../../src/store/configureStore' 8 | import {initialState} from '../../src/reducers/initialStates/list' 9 | 10 | describe('', () => { 11 | 12 | 13 | beforeEach(() => { 14 | global.store = configureStore({ list: initialState }) 15 | }) 16 | 17 | it('should display home page', () => { 18 | 19 | const tree = renderer.create( 20 | 21 | 22 | 23 | ).toJSON() 24 | 25 | expect(tree).toMatchSnapshot() 26 | }) 27 | 28 | }) 29 | -------------------------------------------------------------------------------- /test/containers/LoginPage.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Provider } from 'react-redux' 3 | import { mount } from 'enzyme' 4 | import renderer from 'react-test-renderer' 5 | import LoginPage from '../../src/containers/LoginPage' 6 | import configureStore from '../../src/store/configureStore' 7 | import {initialState} from '../../src/reducers/initialStates/login' 8 | 9 | describe('', () => { 10 | 11 | 12 | beforeEach(() => { 13 | global.store = configureStore({ login: initialState }) 14 | }) 15 | 16 | it('should display login page', () => { 17 | 18 | const tree = renderer.create( 19 | 20 | 21 | 22 | ).toJSON() 23 | 24 | expect(tree).toMatchSnapshot() 25 | }) 26 | 27 | it('should display LoginForm component', () => { 28 | const component = mount( 29 | 30 | 31 | 32 | ) 33 | 34 | const loginForm = component.find('LoginForm') 35 | expect(loginForm.exists()).toBe(true) 36 | }) 37 | 38 | it('should display Header component', () => { 39 | const component = mount( 40 | 41 | 42 | 43 | ) 44 | 45 | const header = component.find('Header') 46 | expect(header.exists()).toBe(true) 47 | }) 48 | 49 | }) 50 | -------------------------------------------------------------------------------- /test/containers/__snapshots__/HomePage.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` should display home page 1`] = ` 4 |
8 |
11 |
14 |
17 |
18 |
19 |
20 | `; 21 | -------------------------------------------------------------------------------- /test/containers/__snapshots__/LoginPage.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` should display login page 1`] = ` 4 |
5 |

8 |
11 | Sample React, Redux, Rouer and JWT Auth 12 |
13 |

14 |
17 |

18 | Login 19 |

20 |
24 |
27 |
30 | 36 |
37 |
38 |
41 |
44 | 50 |
51 |
52 | 60 |
61 |
64 |
67 | Ops... 68 |
69 |

70 |

71 |
72 |
76 |
79 |
82 |
85 |
86 |
87 |
88 |
89 | `; 90 | -------------------------------------------------------------------------------- /test/reducers/list.spec.js: -------------------------------------------------------------------------------- 1 | import thunk from 'redux-thunk' 2 | import configureMockStore from 'redux-mock-store' 3 | 4 | import {initialState} from '../../src/reducers/initialStates/list' 5 | import reducer from '../../src/reducers/list' 6 | import * as actions from '../../src/actions/list' 7 | 8 | const middlewares = [thunk] 9 | const mockStore = configureMockStore(middlewares) 10 | 11 | describe('List reducers', () => { 12 | 13 | beforeEach(() => { 14 | global.initialState = initialState 15 | }) 16 | 17 | it('should handle initial state', () => { 18 | expect(reducer(undefined, {})).toEqual(global.initialState) 19 | }) 20 | 21 | it('should handle listFetching', () => { 22 | expect(reducer(global.initialState,{ 23 | type: actions.listFetching().type})).toEqual({ 24 | isFetching: true, 25 | hasFailed: false, 26 | hasSuccess: false, 27 | items: [] 28 | }) 29 | }) 30 | 31 | it('should handle listError', () => { 32 | expect(reducer(global.initialState,{ 33 | type: actions.listError().type})).toEqual({ 34 | isFetching: false, 35 | hasFailed: true, 36 | hasSuccess: false, 37 | items: [] 38 | }) 39 | }) 40 | 41 | it('should handle listSuccess', () => { 42 | expect(reducer(global.initialState,{ 43 | type: actions.listSuccess().type})).toEqual({ 44 | isFetching: false, 45 | hasFailed: false, 46 | hasSuccess: true, 47 | items: [] 48 | }) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /test/reducers/login.spec.js: -------------------------------------------------------------------------------- 1 | import thunk from 'redux-thunk' 2 | import configureMockStore from 'redux-mock-store' 3 | 4 | import {initialState} from '../../src/reducers/initialStates/login' 5 | import reducer from '../../src/reducers/login' 6 | import * as actions from '../../src/actions/login' 7 | 8 | const middlewares = [thunk] 9 | const mockStore = configureMockStore(middlewares) 10 | 11 | describe('Login reducers', () => { 12 | 13 | beforeEach(() => { 14 | global.initialState = initialState 15 | }) 16 | 17 | it('should handle initial state', () => { 18 | expect(reducer(undefined, {})).toEqual(global.initialState) 19 | }) 20 | 21 | it('should handle logout', () => { 22 | expect(reducer(global.initialState,{ 23 | type: actions.logout().type})).toEqual(global.initialState) 24 | }) 25 | 26 | it('should handle loginFetching', () => { 27 | expect(reducer(global.initialState,{ 28 | type: actions.loginFetching().type})).toEqual({ 29 | isAuthenticated: false, 30 | isFetching: true, 31 | hasSuccess: false, 32 | hasFailed: false, 33 | response: {} 34 | }) 35 | }) 36 | 37 | it('should handle loginError', () => { 38 | expect(reducer(global.initialState,{ 39 | type: actions.loginError().type})).toEqual({ 40 | isAuthenticated: false, 41 | isFetching: false, 42 | hasSuccess: false, 43 | hasFailed: true, 44 | response: {} 45 | }) 46 | }) 47 | 48 | it('should handle loginSuccess', () => { 49 | expect(reducer(global.initialState,{ 50 | type: actions.loginSuccess().type})).toEqual({ 51 | isAuthenticated: true, 52 | isFetching: false, 53 | hasSuccess: true, 54 | hasFailed: false, 55 | response: {} 56 | }) 57 | }) 58 | 59 | }) 60 | -------------------------------------------------------------------------------- /test/utils/auth.spec.js: -------------------------------------------------------------------------------- 1 | import Auth from '../../src/utils/auth' 2 | 3 | describe('Auth', () => { 4 | 5 | it('should has isAuthenticated method', () => { 6 | expect(Auth.isAuthenticated).toBeDefined() 7 | }) 8 | 9 | it('should has getToken method', () => { 10 | expect(Auth.getToken).toBeDefined() 11 | }) 12 | 13 | it('should has removeToken method', () => { 14 | expect(Auth.removeToken).toBeDefined() 15 | }) 16 | 17 | it('should has storeToken method', () => { 18 | expect(Auth.storeToken).toBeDefined() 19 | }) 20 | 21 | it('should has login method', () => { 22 | expect(Auth.login).toBeDefined() 23 | }) 24 | 25 | }) 26 | -------------------------------------------------------------------------------- /test/utils/fetchApi.spec.js: -------------------------------------------------------------------------------- 1 | import fetchApi from '../../src/utils/fetchApi' 2 | 3 | describe('fetchApi', () => { 4 | 5 | it('should has method post', () => { 6 | expect(fetchApi.post()).toBeDefined() 7 | }) 8 | 9 | it('should has method get', () => { 10 | expect(fetchApi.get()).toBeDefined() 11 | }) 12 | 13 | it('should has baseEndpoint', () => { 14 | expect(fetchApi.baseEndpoint).toEqual('https://i2x-challenge.herokuapp.com') 15 | }) 16 | 17 | it('should has setConfig not setted authorization header attribute', () => { 18 | expect(fetchApi.setConfig()).toEqual({ 19 | headers: { 20 | "content-type": "application/json" 21 | } 22 | }) 23 | }) 24 | 25 | it('should has setConfig setted authorization header attribute', () => { 26 | const token = 'abcde' 27 | expect(fetchApi.setConfig(token)).toEqual({ 28 | headers: { 29 | "content-type": "application/json", 30 | 'Authorization': `JWT ${token}` 31 | } 32 | }) 33 | }) 34 | 35 | 36 | it('should headers setted new paramenter', () => { 37 | const headerParam = {'abcde':'12345'} 38 | 39 | expect(fetchApi.headers(headerParam)).toEqual({ 40 | ...headerParam, 41 | "content-type": "application/json", 42 | }) 43 | }) 44 | 45 | it('should config changed', () => { 46 | const anyParam = {'abcde':'12345'} 47 | 48 | expect(fetchApi.config({},anyParam)).toEqual({ 49 | ...anyParam, 50 | headers: { 51 | "content-type": "application/json" 52 | } 53 | }) 54 | }) 55 | 56 | }) 57 | -------------------------------------------------------------------------------- /test/utils/list.spec.js: -------------------------------------------------------------------------------- 1 | import List from '../../src/utils/list' 2 | 3 | describe('List', () => { 4 | 5 | it('should has getList method', () => { 6 | expect(List.getList).toBeDefined() 7 | }) 8 | 9 | }) 10 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const detect = require('detect-port'); 5 | const portfinder = require('portfinder'); 6 | 7 | const DEFAULT_PORT = 3000; 8 | 9 | const config = port => ({ 10 | devtool: 'cheap-module-source-map', 11 | entry: { 12 | app: './src/index' 13 | }, 14 | output: { 15 | path: path.join(__dirname, 'dist'), 16 | filename: 'bundle.[hash].js', 17 | publicPath: '/' 18 | }, 19 | devServer: { 20 | port, 21 | hot: true, 22 | historyApiFallback: true, 23 | stats: 'errors-only', 24 | clientLogLevel: 'error' 25 | }, 26 | plugins: [ 27 | new webpack.HotModuleReplacementPlugin(), 28 | new webpack.NamedModulesPlugin(), 29 | new webpack.DefinePlugin({ 30 | 'process.env': { 31 | NODE_ENV: JSON.stringify('development') 32 | } 33 | }), 34 | new HtmlWebpackPlugin({ 35 | template: './src/index.html', 36 | filename: 'index.html' 37 | }) 38 | ], 39 | module: { 40 | rules: [ 41 | { 42 | test: /\.js$/, 43 | use: ['babel-loader'], 44 | exclude: /node_modules/ 45 | }, 46 | { 47 | test: /\.scss$/, 48 | use: [ 49 | 'style-loader?sourceMap', 50 | 'css-loader?sourceMap&modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]!sass-loader' 51 | ] 52 | } 53 | ] 54 | }, 55 | }); 56 | 57 | module.exports = detect(DEFAULT_PORT).then(port => { 58 | if (port === DEFAULT_PORT) { 59 | return config(DEFAULT_PORT); 60 | } 61 | 62 | return portfinder.getPortPromise().then(port => config(port)); 63 | }); 64 | -------------------------------------------------------------------------------- /webpack.config.production.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | 6 | module.exports = { 7 | devtool: 'source-map', 8 | entry: './src/index', 9 | output: { 10 | path: path.join(__dirname, 'dist'), 11 | filename: 'bundle.[hash].js', 12 | publicPath: '/' 13 | }, 14 | plugins: [ 15 | new webpack.DefinePlugin({ 16 | 'process.env': { 17 | NODE_ENV: JSON.stringify('production') 18 | } 19 | }), 20 | new webpack.optimize.UglifyJsPlugin({ 21 | sourceMap: true, 22 | compressor: { 23 | screw_ie8: true, 24 | warnings: false 25 | } 26 | }), 27 | new HtmlWebpackPlugin({ 28 | template: './src/index.html', 29 | filename: 'index.html' 30 | }), 31 | new ExtractTextPlugin({ 32 | filename: 'index.[hash].css', 33 | allChunks: true 34 | }) 35 | ], 36 | module: { 37 | rules: [ 38 | { 39 | test: /\.js$/, 40 | use: ['babel-loader'], 41 | exclude: /node_modules/, 42 | }, 43 | { 44 | test: /\.scss$/, 45 | use: ExtractTextPlugin.extract({ 46 | fallback: 'style-loader', 47 | use: 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!sass-loader' 48 | }) 49 | } 50 | ] 51 | }, 52 | }; 53 | --------------------------------------------------------------------------------