-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Self PR #1
base: self-pr
Are you sure you want to change the base?
Self PR #1
Changes from all commits
163014d
22be0c9
efe8bab
06f757d
b2fa1ec
65c4285
8e338bd
157a248
32bb3ba
452ce05
feff414
6fbdf24
f9c46fd
b81c23d
55bf82e
dde1b34
572b45b
af2aaef
b55eae1
87f1f3a
28ebbf8
3d8aa6c
68eec9c
752fc8d
1eb3b76
4fdc277
7edc9f2
946aa09
ca95a9e
8650936
36b69f1
5c276c6
1e4f587
1e01f94
80e49de
998af3f
8d7887b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# http://editorconfig.org | ||
root = true | ||
|
||
[*] | ||
charset = utf-8 | ||
end_of_line = lf | ||
indent_size = 2 | ||
indent_style = space | ||
insert_final_newline = true | ||
max_line_length = 120 | ||
trim_trailing_whitespace = true | ||
|
||
[*.md] | ||
max_line_length = 0 | ||
trim_trailing_whitespace = false | ||
|
||
[COMMIT_EDITMSG] | ||
max_line_length = 0 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added some external extensions for eslint and also personal lint rules to improve the coding standards. |
||
"extends": [ | ||
"airbnb", | ||
"prettier", | ||
"prettier/react", | ||
"react-app" | ||
], | ||
"rules": { | ||
"react/jsx-one-expression-per-line": [ | ||
0, | ||
{ | ||
"allow": "none" | ||
} | ||
], | ||
"react/jsx-filename-extension": [ | ||
1, | ||
{ | ||
"extensions": [ | ||
".js", | ||
".jsx" | ||
] | ||
} | ||
], | ||
"react/prefer-stateless-function": [0], | ||
"jsx-a11y/label-has-associated-control": [0], | ||
"jsx-a11y/label-has-for": [0], | ||
"no-shadow": [0], | ||
"prettier/prettier": [ | ||
"error", | ||
{ | ||
"trailingComma": "es5", | ||
"singleQuote": true, | ||
"printWidth": 100 | ||
} | ||
] | ||
}, | ||
"plugins": [ | ||
"prettier" | ||
] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
web: node server-heroku.js |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. database for json serve |
||
"users": [{ | ||
"id": 1, | ||
"name": "Manuel Admin", | ||
"email": "[email protected]", | ||
"role": "admin", | ||
"password": "Qwerty1234@" | ||
}, | ||
{ | ||
"id": 2, | ||
"name": "Manuel User", | ||
"email": "[email protected]", | ||
"role": "user", | ||
"password": "Qwerty1234@1" | ||
} | ||
] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,10 +4,16 @@ | |
"private": true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have tried to used less external dependencies as its posible |
||
"dependencies": { | ||
"bootstrap": "^4.1.3", | ||
"connected-react-router": "^4.4.1", | ||
"history": "^4.7.2", | ||
"jwt-decode": "^2.2.0", | ||
"prop-types": "^15.6.2", | ||
"react": "^16.4.2", | ||
"react-bootstrap": "^0.32.3", | ||
"react-dom": "^16.4.2", | ||
"react-paginate": "^5.2.4", | ||
"react-redux": "^5.0.7", | ||
"react-router": "^4.3.1", | ||
"react-router-dom": "^4.3.1", | ||
"react-scripts": "1.1.5", | ||
"redux": "^4.0.0", | ||
|
@@ -19,6 +25,22 @@ | |
"start": "react-scripts start", | ||
"build": "react-scripts build", | ||
"test": "react-scripts test --env=jsdom", | ||
"eject": "react-scripts eject" | ||
"eject": "react-scripts eject", | ||
"start-api": "node server.js", | ||
"postinstall": "react-scripts build" | ||
}, | ||
"devDependencies": { | ||
"eslint": "^4.19.1", | ||
"eslint-config-airbnb": "^17.0.0", | ||
"eslint-config-prettier": "^3.0.1", | ||
"eslint-loader": "^2.1.0", | ||
"eslint-plugin-import": "^2.13.0", | ||
"eslint-plugin-jsx-a11y": "^6.1.1", | ||
"eslint-plugin-prettier": "^2.6.2", | ||
"eslint-plugin-react": "^7.10.0", | ||
"express": "^4.16.3", | ||
"json-server": "^0.14.0", | ||
"jsonwebtoken": "^8.3.0", | ||
"prettier": "^1.14.2" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
const express = require('express'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nodejs with express server to deploy in heroku |
||
const path = require('path'); | ||
const http = require('http'); | ||
const bodyParser = require('body-parser'); | ||
|
||
const app = express(); | ||
|
||
// Parsers for POST data | ||
app.use(bodyParser.json()); | ||
app.use(bodyParser.urlencoded({ extended: false })); | ||
|
||
// Point static path to build | ||
app.use(express.static(path.join(__dirname, 'build'))); | ||
|
||
// Catch all other routes and return the index file | ||
app.get('*', (req, res) => { | ||
res.sendFile(path.join(__dirname, 'build', 'index.html')); | ||
}); | ||
|
||
/** | ||
* Get port from environment and store in Express. | ||
*/ | ||
const port = process.env.PORT || '3000'; | ||
app.set('port', port); | ||
|
||
/** | ||
* Create HTTP server. | ||
*/ | ||
const server = http.createServer(app); | ||
|
||
/** | ||
* Listen on provided port, on all network interfaces. | ||
*/ | ||
server.listen(port, () => console.log(`API running on localhost:${port}`)); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
const fs = require('fs'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. server json with jwt auth There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
const bodyParser = require('body-parser'); | ||
const jsonServer = require('json-server'); | ||
const jwt = require('jsonwebtoken'); | ||
|
||
const server = jsonServer.create(); | ||
const router = jsonServer.router('./db.json'); | ||
const port = process.env.PORT || 3004; | ||
|
||
server.use(bodyParser.urlencoded({ extended: true })); | ||
server.use(bodyParser.json()); | ||
server.use(jsonServer.defaults()); | ||
|
||
const SECRET_KEY = '123456789'; | ||
const expiresIn = '24h'; | ||
|
||
/** | ||
* Create a token from a payload | ||
* @param {Object} payload | ||
*/ | ||
function createToken(payload) { | ||
return jwt.sign(payload, SECRET_KEY, { expiresIn }); | ||
} | ||
|
||
/** | ||
* Verify the token | ||
* @param {String} token | ||
*/ | ||
function verifyToken(token) { | ||
return jwt.verify(token, SECRET_KEY, (err, decode) => decode || err); | ||
} | ||
|
||
/** | ||
* Check if the user exists in database | ||
* @param {String} email | ||
* @param {String} password | ||
* @returns {Boolean} | ||
*/ | ||
function isAuthenticated({ email, password }) { | ||
const userdb = JSON.parse(fs.readFileSync('./db.json', 'UTF-8')); | ||
return userdb.users.find(user => user.email === email && user.password === password); | ||
} | ||
|
||
server.post('/auth/login', (req, res) => { | ||
const { email, password } = req.body; | ||
const user = isAuthenticated({ email, password }); | ||
|
||
if (user) { | ||
const userClone = Object.assign({}, user); | ||
delete userClone.password; | ||
|
||
const access_token = createToken(userClone); | ||
res.status(200).json({ access_token, ...userClone }); | ||
return; | ||
} | ||
const status = 401; | ||
const message = 'Incorrect email or password'; | ||
|
||
res.status(status).json({ status, message }); | ||
}); | ||
|
||
server.use(/^(?!\/auth).*$/, (req, res, next) => { | ||
if (!req.headers.authorization || req.headers.authorization.split(' ')[0] !== 'Bearer') { | ||
const status = 401; | ||
const message = 'Error in authorization format'; | ||
|
||
res.status(status).json({ status, message }); | ||
return; | ||
} | ||
try { | ||
verifyToken(req.headers.authorization.split(' ')[1]); | ||
next(); | ||
} catch (err) { | ||
const status = 401; | ||
const message = 'Error access_token is revoked'; | ||
|
||
res.status(status).json({ status, message }); | ||
} | ||
}); | ||
|
||
server.use( | ||
jsonServer.rewriter({ | ||
'/auth/signup': '/users', | ||
}) | ||
); | ||
|
||
server.use(router); | ||
|
||
server.listen(port, () => { | ||
console.log('Run API Server'); | ||
}); |
This file was deleted.
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import jwtDecode from 'jwt-decode'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Action only Auth user, create separate file in order to gain maintainability and readability |
||
import { | ||
LOGIN_SUCCESS, | ||
LOGIN_REQUEST, | ||
LOGIN_FAILURE, | ||
SIGNUP_SUCCESS, | ||
SIGNUP_REQUEST, | ||
SIGNUP_FAILURE, | ||
LOGOUT_SUCCESS, | ||
ME_REQUEST, | ||
ME_SUCCESS, | ||
ME_FAILURE, | ||
CLEAN_ERRORS, | ||
} from '../constants/actionTypes'; | ||
import { API } from '../constants/endpoints'; | ||
import { CALL_API } from '../constants/variables'; | ||
|
||
/** | ||
* Signup API handler | ||
* @param {String} name | ||
* @param {String} email | ||
* @param {String} password | ||
*/ | ||
export const signup = (name, email, password, role) => ({ | ||
[CALL_API]: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Call to api middleware with object of params in order to create a common structure for it |
||
payload: { name, email, password, role }, | ||
method: 'post', | ||
types: [SIGNUP_REQUEST, SIGNUP_SUCCESS, SIGNUP_FAILURE, { type: 'login', action: login }], | ||
endpoint: API.URL + API.USERS.AUTH.SIGNUP, | ||
validate: true, | ||
}, | ||
}); | ||
|
||
/** | ||
* Login API handler | ||
* @param {String} email | ||
* @param {String} password | ||
*/ | ||
export const login = (email, password) => ({ | ||
[CALL_API]: { | ||
payload: { email, password }, | ||
method: 'post', | ||
types: [LOGIN_REQUEST, LOGIN_SUCCESS, LOGIN_FAILURE], | ||
endpoint: API.URL + API.USERS.AUTH.LOGIN, | ||
validate: true, | ||
}, | ||
}); | ||
|
||
/** | ||
* Logout handler | ||
*/ | ||
export const logout = () => dispatch => { | ||
dispatch({ type: LOGOUT_SUCCESS }); | ||
}; | ||
|
||
/** | ||
* Me handler | ||
*/ | ||
export const me = () => dispatch => { | ||
dispatch({ type: ME_REQUEST }); | ||
|
||
try { | ||
const token = localStorage.getItem('token'); | ||
const user = jwtDecode(token); | ||
|
||
dispatch({ type: ME_SUCCESS, user }); | ||
} catch (err) { | ||
dispatch({ type: ME_FAILURE }); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. try catch in order to avoid errors |
||
}; | ||
|
||
/** | ||
* Clean Errors | ||
*/ | ||
export const cleanErrors = () => dispatch => { | ||
dispatch({ type: CLEAN_ERRORS }); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import { push } from 'connected-react-router'; | ||
import { | ||
GET_USERS_REQUEST, | ||
GET_USERS_SUCCESS, | ||
GET_USERS_FAILURE, | ||
DELETE_USER_REQUEST, | ||
DELETE_USER_SUCCESS, | ||
DELETE_USER_FAILURE, | ||
FILTER_USER_SUCCESS, | ||
GET_USER_REQUEST, | ||
GET_USER_SUCCESS, | ||
GET_USER_FAILURE, | ||
UPDATE_USER_REQUEST, | ||
UPDATE_USER_SUCCESS, | ||
UPDATE_USER_FAILURE, | ||
GET_USERS_COUNT_REQUEST, | ||
GET_USERS_COUNT_SUCCESS, | ||
GET_USERS_COUNT_FAILURE, | ||
} from '../constants/actionTypes'; | ||
import { API } from '../constants/endpoints'; | ||
import { CALL_API } from '../constants/variables'; | ||
import { logout } from './auth.actions'; | ||
|
||
/** | ||
* GetUsers API handler | ||
* | ||
* @param {Number} page | ||
*/ | ||
export const getUsers = (page = 1) => ({ | ||
[CALL_API]: { | ||
method: 'get', | ||
types: [GET_USERS_REQUEST, GET_USERS_SUCCESS, GET_USERS_FAILURE], | ||
endpoint: `${API.URL + API.USERS.GET}?_page=${page}`, | ||
}, | ||
}); | ||
|
||
/** | ||
* GetUsers count API handler for pagination | ||
* | ||
* @param {Number} page | ||
*/ | ||
export const getUsersCount = () => ({ | ||
[CALL_API]: { | ||
method: 'get', | ||
types: [GET_USERS_COUNT_REQUEST, GET_USERS_COUNT_SUCCESS, GET_USERS_COUNT_FAILURE], | ||
endpoint: API.URL + API.USERS.GET, | ||
}, | ||
}); | ||
|
||
/** | ||
* Get single API handler | ||
* @param {Number} id | ||
*/ | ||
export const getUser = id => ({ | ||
[CALL_API]: { | ||
method: 'get', | ||
types: [GET_USER_REQUEST, GET_USER_SUCCESS, GET_USER_FAILURE], | ||
endpoint: API.URL + API.USERS.GET + id, | ||
}, | ||
}); | ||
|
||
/** | ||
* GetUsers API handler | ||
* @params {Number} id | ||
* @params {Boolean} isCurrent | ||
*/ | ||
export const deleteUser = (id, isCurrent) => { | ||
const action = isCurrent | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is it the current user have to be logout action |
||
? { type: 'logout', action: logout } | ||
: { type: 'getUsers', action: getUsers }; | ||
|
||
return { | ||
[CALL_API]: { | ||
method: 'delete', | ||
types: [DELETE_USER_REQUEST, DELETE_USER_SUCCESS, DELETE_USER_FAILURE, action], | ||
endpoint: API.URL + API.USERS.DELETE + id, | ||
}, | ||
}; | ||
}; | ||
|
||
/** | ||
* Filter User handler handler | ||
* @param {String} name | ||
*/ | ||
export const filterUser = name => dispatch => { | ||
dispatch({ type: FILTER_USER_SUCCESS, name }); | ||
}; | ||
|
||
/** | ||
* Update User API handler | ||
* @param {Number} id | ||
* @param {String} name | ||
* @param {String} email | ||
* @param {String} password | ||
* @param {Boolean} isCurrentUserRoleChange | ||
*/ | ||
export const updateUser = (id, name, email, password, role, isCurrentUserRoleChange) => { | ||
const actions = [UPDATE_USER_REQUEST, UPDATE_USER_SUCCESS, UPDATE_USER_FAILURE]; | ||
if (isCurrentUserRoleChange) { | ||
actions.push({ type: 'logout', action: logout }); | ||
} else { | ||
actions.push({ type: 'redirect', action: push('/users') }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if the current user change the role, have to logout , otherwise redirect to user listing page |
||
} | ||
|
||
return { | ||
[CALL_API]: { | ||
payload: { id, name, email, password, role }, | ||
method: 'put', | ||
types: actions, | ||
endpoint: API.URL + API.USERS.UPDATE + id, | ||
validate: true, | ||
}, | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import React from 'react'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. dummy home page to work with different roles |
||
|
||
export class Home extends React.PureComponent { | ||
render() { | ||
return ( | ||
<div> | ||
<h1>Home Role Users</h1> | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
export default Home; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import React from 'react'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Created composition model, In order to reuse code between components, better to maintain. |
||
import PropTypes from 'prop-types'; | ||
|
||
class Input extends React.PureComponent { | ||
static propTypes = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. work with proptypes help to control props and create a stable and solid component. |
||
label: PropTypes.string.isRequired, | ||
type: PropTypes.string.isRequired, | ||
name: PropTypes.string.isRequired, | ||
placeholder: PropTypes.string.isRequired, | ||
value: PropTypes.string, | ||
}; | ||
|
||
static defaultProps = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use default props helps also to create solid components |
||
value: '', | ||
}; | ||
|
||
componentWillMount() { | ||
const { name, value } = this.props; | ||
|
||
this.setState({ | ||
[name]: value, | ||
}); | ||
} | ||
|
||
/** | ||
* Handler change form | ||
*/ | ||
handleChange = event => { | ||
const { name, value } = event.target; | ||
|
||
this.setState({ [name]: value }); | ||
}; | ||
|
||
render() { | ||
const { label, type, placeholder, name } = this.props; | ||
|
||
return ( | ||
<React.Fragment> | ||
<div className="form-group"> | ||
<label>{label}</label> | ||
<input | ||
className="form-control" | ||
name={name} | ||
type={type} | ||
placeholder={placeholder} | ||
value={this.state[name]} | ||
onChange={this.handleChange} | ||
/> | ||
</div> | ||
</React.Fragment> | ||
); | ||
} | ||
} | ||
export default Input; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import React from 'react'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Created composition model, In order to reuse code between components, better to maintain. |
||
import PropTypes from 'prop-types'; | ||
|
||
class Select extends React.PureComponent { | ||
static propTypes = { | ||
label: PropTypes.string.isRequired, | ||
name: PropTypes.string.isRequired, | ||
value: PropTypes.string, | ||
}; | ||
|
||
static defaultProps = { | ||
value: '', | ||
}; | ||
|
||
componentWillMount() { | ||
const { name, value } = this.props; | ||
|
||
this.setState({ | ||
[name]: value, | ||
}); | ||
} | ||
|
||
/** | ||
* Handler change form | ||
*/ | ||
handleChange = event => { | ||
const { name, value } = event.target; | ||
|
||
this.setState({ [name]: value }); | ||
}; | ||
|
||
render() { | ||
const { label, name } = this.props; | ||
|
||
return ( | ||
<React.Fragment> | ||
<div className="form-group"> | ||
<label>{label}</label> | ||
<select | ||
value={this.state[name]} | ||
name="role" | ||
className="form-control" | ||
onChange={this.handleChange} | ||
> | ||
<option value="admin">Admin</option> | ||
<option value="user">User</option> | ||
</select> | ||
</div> | ||
</React.Fragment> | ||
); | ||
} | ||
} | ||
export default Select; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
export const SIGNUP_REQUEST = 'SIGNUP_REQUEST'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Created a separated file with const actions, better to read, maintain. |
||
export const SIGNUP_SUCCESS = 'SIGNUP_SUCCESS'; | ||
export const SIGNUP_FAILURE = 'SIGNUP_FAILURE'; | ||
|
||
export const LOGIN_REQUEST = 'LOGIN_REQUEST'; | ||
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'; | ||
export const LOGIN_FAILURE = 'LOGIN_FAILURE'; | ||
|
||
export const LOGOUT_REQUEST = 'LOGOUT_REQUEST'; | ||
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'; | ||
export const LOGOUT_FAILURE = 'LOGOUT_FAILURE'; | ||
|
||
export const ME_REQUEST = 'ME_REQUEST'; | ||
export const ME_SUCCESS = 'ME_SUCCESS'; | ||
export const ME_FAILURE = 'ME_FAILURE'; | ||
|
||
export const GET_USERS_REQUEST = 'GET_USERS_REQUEST'; | ||
export const GET_USERS_SUCCESS = 'GET_USERS_SUCCESS'; | ||
export const GET_USERS_FAILURE = 'GET_USERS_FAILURE'; | ||
|
||
export const GET_USERS_COUNT_REQUEST = 'GET_USERS_COUNT_REQUEST'; | ||
export const GET_USERS_COUNT_SUCCESS = 'GET_USERS_COUNT_SUCCESS'; | ||
export const GET_USERS_COUNT_FAILURE = 'GET_USERS_COUNT_FAILURE'; | ||
|
||
export const GET_USER_REQUEST = 'GET_USER_REQUEST'; | ||
export const GET_USER_SUCCESS = 'GET_USER_SUCCESS'; | ||
export const GET_USER_FAILURE = 'GET_USER_FAILURE'; | ||
|
||
export const UPDATE_USER_REQUEST = 'UPDATE_USER_REQUEST'; | ||
export const UPDATE_USER_SUCCESS = 'UPDATE_USER_SUCCESS'; | ||
export const UPDATE_USER_FAILURE = 'GET_USER_FAILURE'; | ||
|
||
export const DELETE_USER_REQUEST = 'DELETE_USER_REQUEST'; | ||
export const DELETE_USER_SUCCESS = 'DELETE_USER_SUCCESS'; | ||
export const DELETE_USER_FAILURE = 'DELETE_USERS_FAILURE'; | ||
|
||
export const FILTER_USER_REQUEST = 'FILTER_USER_REQUEST'; | ||
export const FILTER_USER_SUCCESS = 'FILTER_USER_SUCCESS'; | ||
export const FILTER_USER_FAILURE = 'FILTER_USER_FAILURE'; | ||
|
||
export const CLEAN_ERRORS = 'CLEAN_ERRORS'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
export const API = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Created a separated file with const with all relative to api, better to read, maintain. |
||
URL: process.env.NODE_ENV === 'production' ? 'https://crud-users.herokuapp.com' : 'http://localhost:3004', | ||
USERS: { | ||
AUTH: { | ||
LOGIN: '/auth/login', | ||
SIGNUP: '/auth/signup', | ||
}, | ||
CREATE: '/users', | ||
DELETE: '/users/', | ||
UPDATE: '/users/', | ||
GET: '/users/', | ||
}, | ||
}; | ||
|
||
export default API; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export const emailPattern = /^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$/; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Created a separated file with common const, better to read, maintain. |
||
export const passwordPattern = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{6,}$/; | ||
export const namePattern = /^[a-zA-z ,.'-]+$/; | ||
export const adminRole = 'admin'; | ||
export const userRole = 'user'; | ||
export const userRoles = ['user']; | ||
export const adminRoles = ['admin']; | ||
export const CALL_API = 'Call API'; | ||
export const limitUsers = 10; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import React from 'react'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Created composition model, In order to reuse code between components, better to maintain. |
||
import PropTypes from 'prop-types'; | ||
import { connect } from 'react-redux'; | ||
import { cleanErrors } from '../../actions/auth.actions'; | ||
|
||
class ErrorFormMessage extends React.PureComponent { | ||
static propTypes = { | ||
errors: PropTypes.arrayOf(PropTypes.string).isRequired, | ||
cleanErrors: PropTypes.func.isRequired, | ||
}; | ||
|
||
componentWillUnmount() { | ||
const { cleanErrors } = this.props; | ||
|
||
cleanErrors(); | ||
} | ||
|
||
render() { | ||
const { errors } = this.props; | ||
|
||
return ( | ||
<div className="formErrors"> | ||
<div> | ||
<ul> | ||
{errors.map(item => ( | ||
<div key={item}> | ||
<li> | ||
<p>{item}</p> | ||
</li> | ||
</div> | ||
))} | ||
</ul> | ||
<div className="alert alert-danger">Form invalid, please check again</div> | ||
</div> | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
const mapStateToProps = state => ({ | ||
user: state.auth, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not used anymore, should be deleted |
||
}); | ||
|
||
const mapDispatchToProps = { | ||
cleanErrors, | ||
}; | ||
|
||
export default connect( | ||
mapStateToProps, | ||
mapDispatchToProps | ||
)(ErrorFormMessage); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import React from 'react'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Login component using other small component and redux to validate form and login action |
||
import PropTypes from 'prop-types'; | ||
import { connect } from 'react-redux'; | ||
import { login } from '../../actions/auth.actions'; | ||
import Input from '../../components/input/Input'; | ||
import ErrorFormMessage from '../errorFormMessage/ErrorFormMessage'; | ||
|
||
export class Login extends React.PureComponent { | ||
static propTypes = { | ||
user: PropTypes.shape({}).isRequired, | ||
login: PropTypes.func.isRequired, | ||
}; | ||
|
||
/** | ||
* Submit handler | ||
*/ | ||
handleSubmit = e => { | ||
e.preventDefault(); | ||
|
||
const { | ||
email: { value: email }, | ||
password: { value: password }, | ||
} = e.target; | ||
const { login } = this.props; | ||
|
||
login(email, password); | ||
}; | ||
|
||
render() { | ||
const { user } = this.props; | ||
|
||
return ( | ||
<div className="container"> | ||
<h1 className="text-center m-2">Login</h1> | ||
<form name="form" onSubmit={this.handleSubmit}> | ||
<Input label="Email" type="email" placeholder="Email" name="email" /> | ||
<Input label="Password" type="password" placeholder="Password" name="password" /> | ||
<div className="form-group text-center"> | ||
<button type="submit" className="btn btn-primary"> | ||
Submit | ||
</button> | ||
</div> | ||
{user.errors && <ErrorFormMessage errors={user.errors} />} | ||
</form> | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
const mapStateToProps = state => ({ | ||
user: state.auth, | ||
}); | ||
|
||
const mapDispatchToProps = { | ||
login, | ||
}; | ||
|
||
export default connect( | ||
mapStateToProps, | ||
mapDispatchToProps | ||
)(Login); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import React from 'react'; | ||
import { Link } from 'react-router-dom'; | ||
import { ConnectedRouter } from 'connected-react-router'; | ||
import { connect } from 'react-redux'; | ||
import PropTypes from 'prop-types'; | ||
import { logout, me } from '../../actions/auth.actions'; | ||
import routes from '../../routes'; | ||
import history from '../../history'; | ||
|
||
class App extends React.PureComponent { | ||
static propTypes = { | ||
user: PropTypes.shape({}).isRequired, | ||
logout: PropTypes.func.isRequired, | ||
me: PropTypes.func.isRequired, | ||
}; | ||
|
||
componentWillMount() { | ||
this.handlerMe(); | ||
} | ||
|
||
/** | ||
* Handler user data from token saved on localStorage | ||
*/ | ||
handlerMe = () => { | ||
const { me } = this.props; | ||
me(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. getting form api the user data from jwt token |
||
}; | ||
|
||
/** | ||
* Logout Handler | ||
*/ | ||
logoutHandler = () => { | ||
const { logout } = this.props; | ||
logout(); | ||
}; | ||
|
||
render() { | ||
const { user } = this.props; | ||
|
||
return ( | ||
<React.Fragment> | ||
{!user.isFetching && ( | ||
<ConnectedRouter history={history}> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. used connected router for redux |
||
<div> | ||
<nav className="navbar navbar-expand-lg navbar-light bg-light"> | ||
<div className="navbar-brand">APP</div> | ||
<button | ||
className="navbar-toggler" | ||
type="button" | ||
data-toggle="collapse" | ||
data-target="#navbarNav" | ||
aria-controls="navbarNav" | ||
aria-expanded="false" | ||
aria-label="Toggle navigation" | ||
> | ||
<span className="navbar-toggler-icon" /> | ||
</button> | ||
<div className="collapse navbar-collapse" id="navbarNav"> | ||
{user.isAuth ? ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. menu options for auth user and no auth user |
||
<ul className="navbar-nav"> | ||
<li> | ||
<div className="nav-item nav-link">{user.data.name}</div> | ||
</li> | ||
<li className="nav-item"> | ||
<div className="nav-item"> | ||
<button | ||
className="btn btn-primary" | ||
type="button" | ||
onClick={this.logoutHandler} | ||
> | ||
Logout | ||
</button> | ||
</div> | ||
</li> | ||
</ul> | ||
) : ( | ||
<ul className="navbar-nav"> | ||
<li className="nav-item"> | ||
<div className="nav-item nav-link"> | ||
<Link to="/login">Login</Link> | ||
</div> | ||
</li> | ||
<li className="nav-item"> | ||
<div className="nav-item nav-link"> | ||
<Link to="/signup">Signup</Link> | ||
</div> | ||
</li> | ||
</ul> | ||
)} | ||
</div> | ||
</nav> | ||
{routes} | ||
</div> | ||
</ConnectedRouter> | ||
)} | ||
</React.Fragment> | ||
); | ||
} | ||
} | ||
|
||
const mapStateToProps = state => ({ | ||
user: state.auth, | ||
}); | ||
|
||
const mapDispatchToProps = { | ||
logout, | ||
me, | ||
}; | ||
|
||
export default connect( | ||
mapStateToProps, | ||
mapDispatchToProps | ||
)(App); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import React from 'react'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. worked with private routes for auth user and specific role in order to prevent not allowed redirection |
||
import { Route, Redirect } from 'react-router-dom'; | ||
import { connect } from 'react-redux'; | ||
import PropTypes from 'prop-types'; | ||
|
||
export class PrivateRoute extends React.PureComponent { | ||
static propTypes = { | ||
user: PropTypes.shape({}).isRequired, | ||
component: PropTypes.func.isRequired, | ||
allowedRoles: PropTypes.arrayOf(PropTypes.string).isRequired, | ||
}; | ||
|
||
render() { | ||
const { user, component: Component, allowedRoles, ...rest } = this.props; | ||
return ( | ||
<Route | ||
{...rest} | ||
render={props => | ||
user.isAuth && allowedRoles.includes(user.data.role) ? ( | ||
<Component {...props} /> | ||
) : ( | ||
<Redirect to={{ pathname: '/login', state: { from: props.location } }} /> | ||
) | ||
} | ||
/> | ||
); | ||
} | ||
} | ||
|
||
const mapStateToProps = state => ({ | ||
user: state.auth, | ||
}); | ||
|
||
const mapDispatchToProps = {}; | ||
|
||
export default connect( | ||
mapStateToProps, | ||
mapDispatchToProps | ||
)(PrivateRoute); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import React from 'react'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. public routes in order to prevent auth user redirect to login and signup page |
||
import { Route, Redirect } from 'react-router-dom'; | ||
import { connect } from 'react-redux'; | ||
import PropTypes from 'prop-types'; | ||
import { userRole, adminRole } from '../../constants/variables'; | ||
|
||
export class PublicRoute extends React.PureComponent { | ||
static propTypes = { | ||
user: PropTypes.shape({}).isRequired, | ||
component: PropTypes.func.isRequired, | ||
}; | ||
|
||
render() { | ||
const { user, component: Component } = this.props; | ||
|
||
return ( | ||
<Route | ||
render={props => { | ||
let toPath; | ||
|
||
if (user.isAuth && user.data.role === adminRole) { | ||
toPath = 'users'; | ||
} | ||
if (user.isAuth && user.data.role === userRole) { | ||
toPath = 'home'; | ||
} | ||
|
||
if (toPath) { | ||
return ( | ||
<Route> | ||
<Redirect to={toPath} /> | ||
</Route> | ||
); | ||
} | ||
return <Component {...props} />; | ||
}} | ||
/> | ||
); | ||
} | ||
} | ||
|
||
const mapStateToProps = state => ({ | ||
user: state.auth, | ||
}); | ||
|
||
const mapDispatchToProps = {}; | ||
|
||
export default connect( | ||
mapStateToProps, | ||
mapDispatchToProps | ||
)(PublicRoute); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { connect } from 'react-redux'; | ||
import { signup } from '../../actions/auth.actions'; | ||
import Input from '../../components/input/Input'; | ||
import ErrorFormMessage from '../errorFormMessage/ErrorFormMessage'; | ||
|
||
export class Signup extends React.PureComponent { | ||
state = { | ||
role: 'admin', | ||
}; | ||
|
||
static propTypes = { | ||
user: PropTypes.shape({}).isRequired, | ||
signup: PropTypes.func.isRequired, | ||
}; | ||
|
||
/** | ||
* Handler change form | ||
*/ | ||
handleChange = event => { | ||
const { name, value } = event.target; | ||
|
||
this.setState({ [name]: value }); | ||
}; | ||
|
||
/** | ||
* Submit handler | ||
*/ | ||
handleSubmit = e => { | ||
e.preventDefault(); | ||
|
||
const { | ||
name: { value: name }, | ||
email: { value: email }, | ||
password: { value: password }, | ||
role: { value: role }, | ||
} = e.target; | ||
const { signup } = this.props; | ||
|
||
signup(name, email, password, role); | ||
}; | ||
|
||
render() { | ||
const { role } = this.state; | ||
const { user } = this.props; | ||
|
||
return ( | ||
<div className="container"> | ||
<h1 className="text-center m-2">SignUp</h1> | ||
<form name="form" onSubmit={this.handleSubmit}> | ||
<div className="form-group"> | ||
<select value={role} name="role" className="form-control" onChange={this.handleChange}> | ||
<option value="admin">Admin</option> | ||
<option value="user">User</option> | ||
</select> | ||
</div> | ||
<Input label="Name" type="text" placeholder="Name" name="name" /> | ||
<Input label="Email" type="email" placeholder="Email" name="email" /> | ||
<Input label="Password" type="password" placeholder="Password" name="password" /> | ||
<div className="form-group text-center"> | ||
<button type="submit" className="btn btn-primary"> | ||
Submit | ||
</button> | ||
</div> | ||
{user.errors && <ErrorFormMessage errors={user.errors} />} | ||
</form> | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
const mapStateToProps = state => ({ | ||
user: state.auth, | ||
}); | ||
|
||
const mapDispatchToProps = { | ||
signup, | ||
}; | ||
|
||
export default connect( | ||
mapStateToProps, | ||
mapDispatchToProps | ||
)(Signup); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { connect } from 'react-redux'; | ||
import { NavLink } from 'react-router-dom'; | ||
import { matchPath } from 'react-router'; | ||
import Input from '../../components/input/Input'; | ||
import Select from '../../components/select/Select'; | ||
import ErrorFormMessage from '../errorFormMessage/ErrorFormMessage'; | ||
import { getUser, updateUser } from '../../actions/users.actions'; | ||
|
||
export class EditUser extends React.PureComponent { | ||
static propTypes = { | ||
user: PropTypes.shape({}).isRequired, | ||
getUser: PropTypes.func.isRequired, | ||
updateUser: PropTypes.func.isRequired, | ||
history: PropTypes.shape({}).isRequired, | ||
}; | ||
|
||
componentDidMount() { | ||
this.getUserHandler(); | ||
} | ||
|
||
/** | ||
* Submit handler | ||
*/ | ||
handleSubmit = e => { | ||
e.preventDefault(); | ||
|
||
const { updateUser, user } = this.props; | ||
const { | ||
name: { value: name }, | ||
email: { value: email }, | ||
password: { value: password }, | ||
role: { value: role }, | ||
} = e.target; | ||
const match = this.getUrlParams(); | ||
const checkRoleChange = user.data.id === Number(match.params.id) && user.data.role !== role; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. check if the current user change it role, in order to redirect to proper action |
||
|
||
updateUser(match.params.id, name, email, password, role, checkRoleChange); | ||
}; | ||
|
||
/** | ||
* Get Url Params | ||
* | ||
* @return {Object} | ||
*/ | ||
getUrlParams = () => { | ||
const { history } = this.props; | ||
const match = matchPath(history.location.pathname, { | ||
path: '/users/:id', | ||
}); | ||
|
||
return match; | ||
}; | ||
|
||
/** | ||
* Handler change form | ||
*/ | ||
handleChange = event => { | ||
const { name, value } = event.target; | ||
|
||
this.setState({ [name]: value }); | ||
}; | ||
|
||
/** | ||
* Get User Handler | ||
*/ | ||
getUserHandler = () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. get user information to edit from url query params |
||
const { getUser } = this.props; | ||
const match = this.getUrlParams(); | ||
|
||
getUser(match.params.id); | ||
}; | ||
|
||
/** | ||
* Handler change form | ||
*/ | ||
handleChange = event => { | ||
const { name, value } = event.target; | ||
|
||
this.setState({ [name]: value }); | ||
}; | ||
|
||
render() { | ||
const { user } = this.props; | ||
|
||
return ( | ||
<div className="container"> | ||
<div> | ||
<h1 className="text-center m-2">EditUser</h1> | ||
<NavLink className="btn btn-primary text-center m-2" to="/users/"> | ||
Back to Users | ||
</NavLink> | ||
</div> | ||
{user.data && | ||
!user.isFetching && ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. waiting for redux and api return data |
||
<form name="form" onSubmit={this.handleSubmit}> | ||
<Select label="Role" name="role" value={user.data.role} /> | ||
<Input | ||
label="Name" | ||
type="text" | ||
placeholder="Name" | ||
name="name" | ||
value={user.data.name} | ||
/> | ||
<Input | ||
label="Email" | ||
type="email" | ||
placeholder="Email" | ||
name="email" | ||
value={user.data.email} | ||
/> | ||
<Input label="Password" type="password" placeholder="Password" name="password" /> | ||
<div className="form-group text-center"> | ||
<button type="submit" className="btn btn-primary"> | ||
Submit | ||
</button> | ||
</div> | ||
</form> | ||
)} | ||
{user.errors && <ErrorFormMessage errors={user.errors} />} | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
const mapStateToProps = state => ({ | ||
user: state.users, | ||
}); | ||
|
||
const mapDispatchToProps = { | ||
getUser, | ||
updateUser, | ||
}; | ||
|
||
export default connect( | ||
mapStateToProps, | ||
mapDispatchToProps | ||
)(EditUser); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
.pagination { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. some styling for pagination |
||
display: inline-block; | ||
padding-left: 0; | ||
margin: 22px 0; | ||
border-radius: 4px; | ||
} | ||
|
||
.pagination>li { | ||
display: inline; | ||
} | ||
|
||
.pagination>li>a, | ||
.pagination>li>span { | ||
position: relative; | ||
float: left; | ||
padding: 6px 12px; | ||
line-height: 1.42857143; | ||
text-decoration: none; | ||
color: #2c689c; | ||
background-color: #fff; | ||
border: 1px solid #ddd; | ||
margin-left: -1px; | ||
} | ||
|
||
.pagination>li:first-child>a, | ||
.pagination>li:first-child>span { | ||
margin-left: 0; | ||
border-bottom-left-radius: 4px; | ||
border-top-left-radius: 4px; | ||
} | ||
|
||
.pagination>li:last-child>a, | ||
.pagination>li:last-child>span { | ||
border-bottom-right-radius: 4px; | ||
border-top-right-radius: 4px; | ||
} | ||
|
||
.pagination>li>a:hover, | ||
.pagination>li>span:hover, | ||
.pagination>li>a:focus, | ||
.pagination>li>span:focus { | ||
z-index: 2; | ||
color: #1b4060; | ||
background-color: #eeeeee; | ||
border-color: #ddd; | ||
} | ||
|
||
.pagination>.active>a, | ||
.pagination>.active>span, | ||
.pagination>.active>a:hover, | ||
.pagination>.active>span:hover, | ||
.pagination>.active>a:focus, | ||
.pagination>.active>span:focus { | ||
z-index: 3; | ||
color: #fff; | ||
background-color: #2c689c; | ||
border-color: #2c689c; | ||
cursor: default; | ||
} | ||
|
||
.pagination>.disabled>span, | ||
.pagination>.disabled>span:hover, | ||
.pagination>.disabled>span:focus, | ||
.pagination>.disabled>a, | ||
.pagination>.disabled>a:hover, | ||
.pagination>.disabled>a:focus { | ||
color: #777777; | ||
background-color: #fff; | ||
border-color: #ddd; | ||
cursor: not-allowed; | ||
} | ||
|
||
.pagination-lg>li>a, | ||
.pagination-lg>li>span { | ||
padding: 10px 16px; | ||
font-size: 20px; | ||
line-height: 1.3333333; | ||
} | ||
|
||
.pagination-lg>li:first-child>a, | ||
.pagination-lg>li:first-child>span { | ||
border-bottom-left-radius: 6px; | ||
border-top-left-radius: 6px; | ||
} | ||
|
||
.pagination-lg>li:last-child>a, | ||
.pagination-lg>li:last-child>span { | ||
border-bottom-right-radius: 6px; | ||
border-top-right-radius: 6px; | ||
} | ||
|
||
.pagination-sm>li>a, | ||
.pagination-sm>li>span { | ||
padding: 5px 10px; | ||
font-size: 14px; | ||
line-height: 1.5; | ||
} | ||
|
||
.pagination-sm>li:first-child>a, | ||
.pagination-sm>li:first-child>span { | ||
border-bottom-left-radius: 3px; | ||
border-top-left-radius: 3px; | ||
} | ||
|
||
.pagination-sm>li:last-child>a, | ||
.pagination-sm>li:last-child>span { | ||
border-bottom-right-radius: 3px; | ||
border-top-right-radius: 3px; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { connect } from 'react-redux'; | ||
import { NavLink } from 'react-router-dom'; | ||
import ReactPaginate from 'react-paginate'; | ||
import { getUsers, deleteUser, filterUser, getUsersCount } from '../../actions/users.actions'; | ||
import './Users.css'; | ||
import { limitUsers } from '../../constants/variables'; | ||
|
||
export class Users extends React.PureComponent { | ||
static propTypes = { | ||
users: PropTypes.shape([]).isRequired, | ||
user: PropTypes.shape({}).isRequired, | ||
getUsers: PropTypes.func.isRequired, | ||
deleteUser: PropTypes.func.isRequired, | ||
filterUser: PropTypes.func.isRequired, | ||
getUsersCount: PropTypes.func.isRequired, | ||
}; | ||
|
||
state = { | ||
filtered: false, | ||
}; | ||
|
||
componentWillMount() { | ||
const { getUsers, getUsersCount } = this.props; | ||
|
||
getUsers(); | ||
getUsersCount(); | ||
} | ||
|
||
/** | ||
* Delete user | ||
* @params {Number} id | ||
*/ | ||
deleteUserHandler = id => { | ||
const { user, deleteUser } = this.props; | ||
|
||
deleteUser(id, user.data.id === id); | ||
}; | ||
|
||
/** | ||
* Filter user list action | ||
*/ | ||
filterUserList = event => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Filter user and handle with pagination |
||
const { filterUser } = this.props; | ||
|
||
if (event.target.value === '') { | ||
this.setState({ | ||
filtered: false, | ||
}); | ||
} else { | ||
filterUser(event.target.value); | ||
this.setState({ | ||
filtered: true, | ||
}); | ||
} | ||
}; | ||
|
||
/** | ||
* Handler click pagination | ||
*/ | ||
handlePageClick = data => { | ||
const { getUsers } = this.props; | ||
|
||
getUsers(data.selected + 1); | ||
window.scrollTo(0, 0); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. every time go to next page in pagination scroll to up |
||
}; | ||
|
||
/** | ||
* Render paginate component | ||
*/ | ||
renderPaginate = () => { | ||
const { users } = this.props; | ||
const { filtered } = this.state; | ||
const pageCount = filtered ? users.countFiltered : users.count; | ||
|
||
if (users.count <= limitUsers) { | ||
return ( | ||
<div className="m-2"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. pagination external component |
||
<ReactPaginate | ||
previousLabel="prev" | ||
nextLabel="next" | ||
pageCount={pageCount} | ||
marginPagesDisplayed={2} | ||
pageRangeDisplayed={5} | ||
onPageChange={this.handlePageClick} | ||
containerClassName="pagination" | ||
subContainerClassName="pages pagination" | ||
activeClassName="active" | ||
/> | ||
</div> | ||
); | ||
} | ||
return null; | ||
}; | ||
|
||
render() { | ||
const { users } = this.props; | ||
|
||
return ( | ||
<div className="m-2 text-center"> | ||
<div className="m-2 text-center"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. user filter |
||
<input type="text" placeholder="Search" onChange={this.filterUserList} /> | ||
<hr /> | ||
</div> | ||
<div> | ||
{users.filtered && ( | ||
<div> | ||
<ul className="list-unstyled"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. user list |
||
{users.filtered.map(item => ( | ||
<div key={item.id}> | ||
<li> | ||
<p> | ||
<b>Name: </b> | ||
{item.name} | ||
</p> | ||
<p> | ||
<b>Email: </b> | ||
{item.email} | ||
</p> | ||
<p> | ||
<b>Role: </b> | ||
{item.role.toUpperCase()} | ||
</p> | ||
</li> | ||
<div> | ||
<button | ||
className="btn btn-primary m-2" | ||
type="button" | ||
onClick={e => { | ||
this.deleteUserHandler(item.id); | ||
}} | ||
> | ||
Delete User | ||
</button> | ||
<NavLink className="btn btn-primary" to={`/users/${item.id}`}> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. redirect to edit user with query param id |
||
Edit | ||
</NavLink> | ||
</div> | ||
<hr /> | ||
</div> | ||
))} | ||
</ul> | ||
</div> | ||
)} | ||
{this.renderPaginate()} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. separate render pagination for better understanding |
||
</div> | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
const mapStateToProps = state => ({ | ||
users: state.users, | ||
user: state.auth, | ||
}); | ||
|
||
const mapDispatchToProps = { | ||
getUsers, | ||
deleteUser, | ||
filterUser, | ||
getUsersCount, | ||
}; | ||
|
||
export default connect( | ||
mapStateToProps, | ||
mapDispatchToProps | ||
)(Users); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { createBrowserHistory } from 'history'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. history in order to work with connect router redux redirect methods |
||
|
||
export default createBrowserHistory(); |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,17 @@ | ||
import React from 'react'; | ||
import ReactDOM from 'react-dom'; | ||
import './index.css'; | ||
import App from './App'; | ||
import 'bootstrap/dist/css/bootstrap.min.css'; | ||
import { render } from 'react-dom'; | ||
import { Provider } from 'react-redux'; | ||
import registerServiceWorker from './registerServiceWorker'; | ||
import App from './containers/main/App'; | ||
import configureStore from './store/configureStore'; | ||
|
||
ReactDOM.render(<App />, document.getElementById('root')); | ||
const store = configureStore(); | ||
|
||
render( | ||
<Provider store={store}> | ||
<App /> | ||
</Provider>, | ||
document.getElementById('root') | ||
); | ||
registerServiceWorker(); |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
import { emailPattern, passwordPattern, namePattern, CALL_API } from '../constants/variables'; | ||
|
||
/** | ||
* Handle communication with web service | ||
* | ||
* @param {Object} callAPI | ||
* @param {Object} next | ||
* @param {Object} actionWith | ||
* @param {Object} store | ||
*/ | ||
export const api = async (callAPI, next, actionWith, store) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. used async - await feature, more readably |
||
const [requestType, successType, failureType, nextType] = callAPI.types; | ||
|
||
try { | ||
const res = await fetch(callAPI.endpoint, { | ||
method: callAPI.method, | ||
headers: getHeaders(), | ||
body: JSON.stringify(callAPI.payload), | ||
}); | ||
const data = await res.json(); | ||
if (res.ok) { | ||
next( | ||
actionWith({ | ||
data, | ||
type: successType, | ||
}) | ||
); | ||
if (nextType) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in order to dispatch next action defined in action file |
||
if (nextType.type === 'login') { | ||
store.dispatch(nextType.action(data.email, data.password)); | ||
} else if (nextType.type === 'redirect') { | ||
store.dispatch(nextType.action); | ||
} else { | ||
store.dispatch(nextType.action()); | ||
} | ||
} | ||
} else { | ||
next( | ||
actionWith({ | ||
type: failureType, | ||
errors: [data.message || 'Error with API, please try again'], | ||
}) | ||
); | ||
} | ||
} catch (e) { | ||
next( | ||
actionWith({ | ||
type: failureType, | ||
errors: [e.message || 'Error with API, please try again'], | ||
}) | ||
); | ||
} | ||
}; | ||
/** | ||
* Get headers | ||
* @return {Object} headers | ||
*/ | ||
const getHeaders = () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. get headers method in order to apply auth jwt token for auth routes |
||
const token = localStorage.getItem('token'); | ||
const headers = { | ||
'Content-Type': 'application/json', | ||
}; | ||
|
||
if (token) { | ||
const tokenParsed = JSON.parse(token); | ||
headers.Authorization = `Bearer ${tokenParsed}`; | ||
} | ||
|
||
return headers; | ||
}; | ||
|
||
// Default middleware | ||
export default store => next => action => { | ||
const callAPI = action[CALL_API]; | ||
|
||
if (callAPI) { | ||
const { types } = callAPI; | ||
const [requestType, successType, failureType, nextType] = types; | ||
const actionWith = data => { | ||
const finalAction = Object.assign({}, action, data); | ||
delete finalAction[CALL_API]; | ||
return finalAction; | ||
}; | ||
|
||
next(actionWith({ type: requestType })); | ||
|
||
if (callAPI.validate) { | ||
if (!validate(callAPI.payload)) { | ||
return next(actionWith({ type: failureType, errors: callAPI.payload.errors })); | ||
} | ||
} | ||
|
||
if (callAPI.payload && callAPI.payload.errors) { | ||
delete callAPI.payload.errors; | ||
} | ||
|
||
api(callAPI, next, actionWith, store); | ||
} else { | ||
return next(action); | ||
} | ||
}; | ||
|
||
/** | ||
* Validate form data | ||
* | ||
* @param {Object} payload | ||
* @return {Boolean} | ||
*/ | ||
const validate = payload => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. validation form by field, using regex and adding a complete array of error to work with errormessage component |
||
let res; | ||
let regex; | ||
payload.errors = []; | ||
|
||
for (const key in payload) { | ||
if (Object.prototype.hasOwnProperty.call(payload, key)) { | ||
const element = payload[key]; | ||
switch (key) { | ||
case 'password': | ||
regex = RegExp(passwordPattern); | ||
res = regex.test(element); | ||
if (!res) { | ||
payload.errors.push( | ||
'Minimum eight characters, at least one letter, one number and one special' | ||
); | ||
} | ||
break; | ||
case 'email': | ||
regex = RegExp(emailPattern); | ||
res = regex.test(element); | ||
if (!res) { | ||
payload.errors.push('Must be a valid Email'); | ||
} | ||
break; | ||
case 'name': | ||
regex = RegExp(namePattern); | ||
res = regex.test(element); | ||
if (!res) { | ||
payload.errors.push('Must be a valid Name'); | ||
} | ||
break; | ||
|
||
default: | ||
break; | ||
} | ||
} | ||
} | ||
return payload.errors.length === 0; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import { | ||
SIGNUP_REQUEST, | ||
SIGNUP_SUCCESS, | ||
SIGNUP_FAILURE, | ||
LOGIN_REQUEST, | ||
LOGIN_SUCCESS, | ||
LOGIN_FAILURE, | ||
LOGOUT_SUCCESS, | ||
ME_REQUEST, | ||
ME_SUCCESS, | ||
ME_FAILURE, | ||
CLEAN_ERRORS, | ||
} from '../constants/actionTypes'; | ||
|
||
const initialState = { | ||
isFetching: false, | ||
isAuth: false, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. by default the user is not auth |
||
}; | ||
|
||
const auth = (state = initialState, action) => { | ||
switch (action.type) { | ||
case SIGNUP_REQUEST: | ||
return { | ||
...state, | ||
isFetching: true, | ||
}; | ||
case SIGNUP_SUCCESS: | ||
return { | ||
...state, | ||
isFetching: true, | ||
}; | ||
case SIGNUP_FAILURE: | ||
return { | ||
...state, | ||
errors: action.errors, | ||
isFetching: false, | ||
}; | ||
case LOGIN_REQUEST: | ||
return { | ||
...state, | ||
isFetching: true, | ||
}; | ||
case LOGIN_SUCCESS: | ||
localStorage.setItem('token', JSON.stringify(action.data.access_token)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. login set a new jwt token |
||
return { | ||
...state, | ||
data: action.data, | ||
isAuth: true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. isauth works to control private routes and menus |
||
isFetching: false, | ||
}; | ||
case LOGIN_FAILURE: | ||
return { | ||
...state, | ||
isAuth: false, | ||
errors: action.errors, | ||
isFetching: false, | ||
}; | ||
case LOGOUT_SUCCESS: | ||
localStorage.removeItem('token'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logout remove the token and return an empty user |
||
return {}; | ||
case ME_REQUEST: | ||
return { | ||
...state, | ||
isFetching: true, | ||
}; | ||
case ME_SUCCESS: | ||
return { | ||
...state, | ||
data: action.user, | ||
isAuth: true, | ||
isFetching: false, | ||
}; | ||
case ME_FAILURE: | ||
return { | ||
...state, | ||
isAuth: false, | ||
isFetching: false, | ||
}; | ||
case CLEAN_ERRORS: | ||
return { | ||
...state, | ||
errors: null, | ||
}; | ||
default: | ||
return state; | ||
} | ||
}; | ||
|
||
export default auth; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { combineReducers } from 'redux'; | ||
|
||
import auth from './auth.reducer'; | ||
import users from './users.reducer'; | ||
|
||
const rootReducer = combineReducers({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Used separated reducer and mix them |
||
auth, | ||
users, | ||
}); | ||
|
||
export default rootReducer; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import { | ||
GET_USERS_REQUEST, | ||
GET_USERS_SUCCESS, | ||
GET_USERS_FAILURE, | ||
GET_USER_REQUEST, | ||
GET_USER_SUCCESS, | ||
GET_USER_FAILURE, | ||
UPDATE_USER_REQUEST, | ||
UPDATE_USER_SUCCESS, | ||
UPDATE_USER_FAILURE, | ||
FILTER_USER_SUCCESS, | ||
GET_USERS_COUNT_REQUEST, | ||
GET_USERS_COUNT_SUCCESS, | ||
GET_USERS_COUNT_FAILURE, | ||
} from '../constants/actionTypes'; | ||
import { limitUsers } from '../constants/variables'; | ||
|
||
const initialState = { | ||
isFetching: false, | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. created initial state |
||
|
||
const users = (state = initialState, action) => { | ||
let filtered; | ||
switch (action.type) { | ||
case GET_USERS_REQUEST: | ||
return { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. every action return the state, action and isfetching in order to handle render method data |
||
...state, | ||
isFetching: true, | ||
}; | ||
case GET_USERS_SUCCESS: | ||
return { | ||
...state, | ||
data: action.data, | ||
filtered: action.data, | ||
isFetching: false, | ||
}; | ||
case GET_USERS_FAILURE: | ||
return { | ||
...state, | ||
errors: action.errors, | ||
isFetching: false, | ||
}; | ||
case GET_USER_REQUEST: | ||
return { | ||
...state, | ||
isFetching: true, | ||
}; | ||
case GET_USER_SUCCESS: | ||
return { | ||
...state, | ||
data: action.data, | ||
isFetching: false, | ||
}; | ||
case GET_USER_FAILURE: | ||
return { | ||
...state, | ||
errors: action.errors, | ||
isFetching: false, | ||
}; | ||
case UPDATE_USER_REQUEST: | ||
return { | ||
...state, | ||
isFetching: true, | ||
}; | ||
case UPDATE_USER_SUCCESS: | ||
return { | ||
...state, | ||
data: action.data, | ||
isFetching: false, | ||
errors: null, | ||
}; | ||
case UPDATE_USER_FAILURE: | ||
return { | ||
...state, | ||
errors: action.errors, | ||
isFetching: false, | ||
}; | ||
case FILTER_USER_SUCCESS: | ||
filtered = state.data.filter(val => | ||
val.name.toLowerCase().includes(action.name.toLowerCase()) | ||
); | ||
|
||
return { | ||
...state, | ||
filtered, | ||
countFiltered: Math.ceil(filtered.length / limitUsers), | ||
isFetching: false, | ||
}; | ||
case GET_USERS_COUNT_REQUEST: | ||
return { | ||
...state, | ||
isFetching: true, | ||
}; | ||
case GET_USERS_COUNT_SUCCESS: | ||
return { | ||
...state, | ||
count: Math.ceil(action.data.length / limitUsers), | ||
isFetching: false, | ||
}; | ||
case GET_USERS_COUNT_FAILURE: | ||
return { | ||
...state, | ||
isFetching: false, | ||
}; | ||
default: | ||
return state; | ||
} | ||
}; | ||
|
||
export default users; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import React from 'react'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Separated routes file |
||
import { Switch, Redirect } from 'react-router'; | ||
import Login from '../containers/login/Login'; | ||
import Signup from '../containers/signup/Signup'; | ||
import Users from '../containers/users/Users'; | ||
import EditUser from '../containers/users/EditUser'; | ||
import Home from '../components/home/Home'; | ||
import PrivateRoute from '../containers/privateRoute/PrivateRoute'; | ||
import PublicRoute from '../containers/publicRoute/PublicRoute'; | ||
import { adminRoles, userRoles } from '../constants/variables'; | ||
|
||
const routes = ( | ||
<div className="page-container"> | ||
<Switch> | ||
<PublicRoute exact path="/login" component={Login} /> | ||
<PublicRoute path="/signup" component={Signup} /> | ||
<PrivateRoute path="/users/:id" component={EditUser} allowedRoles={adminRoles} /> | ||
<PrivateRoute path="/users" component={Users} allowedRoles={adminRoles} /> | ||
<PrivateRoute path="/home" component={Home} allowedRoles={userRoles} /> | ||
<Redirect to="/login" /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. any other path goes to login page |
||
</Switch> | ||
</div> | ||
); | ||
|
||
export default routes; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { createStore, applyMiddleware } from 'redux'; | ||
import thunk from 'redux-thunk'; | ||
import { composeWithDevTools } from 'redux-devtools-extension'; | ||
import logger from 'redux-logger'; | ||
import { createBrowserHistory } from 'history'; | ||
import { connectRouter, routerMiddleware } from 'connected-react-router'; | ||
import rootReducer from '../reducers'; | ||
import api from '../middleware/api'; | ||
import history from '../history'; | ||
|
||
const configureStore = preloadedState => { | ||
const store = createStore( | ||
connectRouter(history)(rootReducer), | ||
preloadedState, | ||
composeWithDevTools(applyMiddleware(routerMiddleware(history), thunk, api, logger)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for dev env, used logger |
||
); | ||
|
||
if (module.hot) { | ||
// Enable Webpack hot module replacement for reducers | ||
module.hot.accept('../reducers', () => { | ||
store.replaceReducer(rootReducer); | ||
}); | ||
} | ||
|
||
return store; | ||
}; | ||
|
||
export default configureStore; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
if (process.env.NODE_ENV === 'production') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. used 2 env in order to change different options between them |
||
module.exports = require('./configureStore.prod'); | ||
} else { | ||
module.exports = require('./configureStore.dev'); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { createStore, applyMiddleware, compose } from 'redux'; | ||
import thunk from 'redux-thunk'; | ||
import { connectRouter, routerMiddleware } from 'connected-react-router'; | ||
import history from '../history'; | ||
import rootReducer from '../reducers'; | ||
import api from '../middleware/api'; | ||
|
||
const configureStore = preloadedState => { | ||
const store = createStore( | ||
connectRouter(history)(rootReducer), | ||
preloadedState, | ||
compose(applyMiddleware(routerMiddleware(history), thunk, api)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logger is not used to in prod env |
||
); | ||
|
||
return store; | ||
}; | ||
|
||
export default configureStore; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding editorconfig with personal configuration in order to get better coding in editor.