Структура проекта:
Файл public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link rel="stylesheet" href ="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<title>Список задач</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
Файл src/index.js
import React from 'react';
import {Provider} from 'react-redux';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App';
import * as serviceWorker from './serviceWorker';
import configureStore from './store';
const store = configureStore();
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'))
;
if (module.hot) {
module.hot.accept('./components/App', () => {
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
})
}
serviceWorker.unregister();
Файл src/actions.auth.js
import * as types from '../constants/auth';
import callApi from '../utils/call-api';
export function signup(email, password, repeatPassword){
return (dispatch) => {
dispatch({
type: types.SIGNUP_REQUEST
})
return callApi('/signup', undefined, {method: "POST"},{
email,
password,
repeatPassword
})
.then(json => {
if (!json.token) {
throw new Error('Джок токен!');
}
localStorage.setItem('token', json.token);
dispatch({
type: types.SIGNUP_SUCCESS,
payload: json
})
})
.catch(reason => dispatch({
type: types.SIGNUP_FAILURE,
payload: reason
}));
};
}
export function login(username, password){
return (dispatch) => {
dispatch({
type: types.LOGIN_REQUEST
})
return callApi('/login', undefined, {method: "POST"}, {
username,
password,
})
.then(json => {
if (!json.token) {
throw new Error('Джок токен!');
}
localStorage.setItem('token', json.token);
dispatch({
type: types.LOGIN_SUCCESS,
payload: json
})
})
.catch(reason => dispatch({
type: types.LOGIN_FAILURE,
payload: reason
}));
};
}
export function logout(){
return (dispatch) => {
dispatch({
type: types.LOGOUT_REQUEST
})
};
}
export function recieveAuth() {
return(dispatch, getState) => {
const {token} = getState().auth;
if (!token) {
dispatch({
type: types.RECIEVE_AUTH_FAILURE
})
}
return callApi('/userauth', token)
.then(json => {
dispatch({
type: types.RECIEVE_AUTH_SUCCESS,
payload: json
})
})
.catch(reason => dispatch({
type: types.RECIEVE_AUTH_FAILURE,
payload: reason
}));
}
}
Файл src/auth/index.js
export * from './auth';
export * from './todo';
export * from './services';
Файл src/auth/services.js
import * as types from '../constants';
import history from '../utils/history';
export function redirect(to) {
return (dispatch) => {
history.push(`${process.env.PUBLIC_URL}${to}`);
dispatch({
type: types.REDIRECT,
payload: {to}
})
}
}
Файл src/auth/todo.js
import * as types from '../constants/todo';
import callApi from '../utils/call-api';
export function fetchTodo() {
return (dispatch, getState) => {
const {token} = getState().auth;
dispatch({
type: types.FETCH_TODO_REQUEST
})
return callApi('/todo', token)
.then(data => dispatch({
type: types.FETCH_TODO_SUCCESS,
payload: data
}))
.catch(reason => dispatch({
type: types.FETCH_TODO_FAILURE,
payload: reason
}))
};
}
Файл src/components/App.js
import React from 'react';
import {Router, Route, Switch, Redirect} from 'react-router-dom';
import PrivateRoute from '../containers/PrivateRoute';
import TodoPage from '../containers/TodoPage';
import WelcomePage from '../containers/WelcomePage';
import history from '../utils/history';
const App = () =>(
<Router history={history}>
<Switch>
<Route exact path='/(welcome)?' component={WelcomePage} />
<PrivateRoute path='/todo' component={TodoPage} />
<Redirect to='/' />
</Switch>
</Router>
);
export default App;
Файл src/components/LoginForm.js
import React from 'react';
class LoginForm extends React.Component {
state = {
email: {
value: ''
},
password: {
value: ''
}
}
handleInputChange = (event) => {
event.persist();
const { name, value } = event.target;
this.setState((prevState)=>({
[name]: {
...prevState[name],
value
}
}))
}
handleSubmit = (event) => {
event.preventDefault();
const { email, password } = this.state;
this.props.onSubmit(email.value, password.value);
}
render() {
const { email, password} = this.state;
return(
<form onSubmit={this.handleSubmit}>
<div className="form-group">
<label>Е-майл</label>
<input type="email" value={email.value} onChange={this.handleInputChange} className="form-control" name="email" aria-describedby="emailHelp" placeholder="Введите е-майл" />
</div>
<div className="form-group">
<label>Пароль</label>
<input type="password" value={password.value} onChange={this.handleInputChange} className="form-control" name="password" placeholder="Введите пароль" />
</div>
<button type="submit" className="btn btn-primary">Войти</button>
</form>
)
}
}
export default LoginForm;
Файл src/components/SignupForm.js
import React from 'react';
class SignupForm extends React.Component {
state = {
email: {
value: ''
},
password: {
value: ''
},
repeatPassword: {
value: ''
}
}
handleInputChange = (event) => {
event.persist();
const { name, value } = event.target;
this.setState((prevState)=>({
[name]: {
...prevState[name],
value
}
}))
}
handleSubmit = (event) => {
event.preventDefault();
const { email, password, repeatPassword } = this.state;
this.props.onSubmit(email.value, password.value, repeatPassword.value);
}
render() {
const { email, password, repeatPassword} = this.state;
return(
<form onSubmit={this.handleSubmit}>
<div className="form-group">
<label>Е-майл</label>
<input type="email" value={email.value} onChange={this.handleInputChange} className="form-control" name="email" aria-describedby="emailHelp" placeholder="Введите е-майл" />
</div>
<div className="form-group">
<label>Пароль</label>
<input type="password" value={password.value} onChange={this.handleInputChange} className="form-control" name="password" placeholder="Введите пароль" />
</div>
<div className="form-group">
<label>Повторите пароль</label>
<input type="password" value={repeatPassword.value} onChange={this.handleInputChange} className="form-control" name="repeatPassword" placeholder="Введите пароль" />
</div>
<button type="submit" className="btn btn-primary">Зарегистрироваться</button>
</form>
)
}
}
export default SignupForm;
Файл src/components/TodoPage.js
import React from 'react';
class TodoPage extends React.Component {
componentDidMount(){
const { fetchTodo } = this.props;
Promise.all([
fetchTodo(),
])
}
render() {
const { todos } = this.props;
const elements = todos.map((item)=>{
const{_id, name} = item;
return(
<li key={_id} className="list-group-item">{name}</li>
)
})
return (
<div>
<h1>Список задач</h1>
<ul className="list-group todo-list">
{elements}
</ul>
</div>
)
}
}
export default TodoPage;
Файл src/components/WelcomePage.js
import React from 'react';
import {Redirect} from 'react-router-dom';
import LoginForm from './LoginForm';
import SignupForm from './SignupForm';
class WelcomePage extends React.Component {
state = {
vhod: true
}
onSubmitVhod = (event) => {
event.persist();
this.setState((prevState) => ({
vhod: true
}))
}
onSubmitRegister = (event) => {
event.persist();
this.setState((prevState) => ({
vhod: false
}))
}
render() {
const {signup, login, isAuthenticated} = this.props;
let classVhod = 'nav-link';
let classRegister = 'nav-link';
if (this.state.vhod) {
classVhod = classVhod + ' active';
} else {
classRegister = classRegister + ' active';
}
if (isAuthenticated){
return (
<Redirect to="/todo" />
)
}
return (
<div className="card text-center">
<div className="card-header">
<ul className="nav nav-tabs card-header-tabs">
<li className="nav-item" onClick={this.onSubmitVhod}>
<p className={classVhod}>Вход</p>
</li>
<li className="nav-item" onClick={this.onSubmitRegister}>
<p className={classRegister}>Регитрация</p>
</li>
</ul>
</div>
<div className="card-body">
{this.state.vhod ? (
<LoginForm onSubmit={login} />
) : (
<SignupForm onSubmit={signup} />
)
}
</div>
</div>
)
}
}
export default WelcomePage;
Файл src/constants/auth.js
export const SIGNUP_REQUEST = Symbol('auth/SIGNUP_REQUEST');
export const SIGNUP_SUCCESS = Symbol('auth/SIGNUP_SUCCESS');
export const SIGNUP_FAILURE = Symbol('auth/SIGNUP_FAILURE');
export const LOGIN_REQUEST = Symbol('auth/LOGIN_REQUEST');
export const LOGIN_SUCCESS = Symbol('auth/LOGIN_SUCCESS');
export const LOGIN_FAILURE = Symbol('auth/LOGIN_FAILURE');
export const LOGOUT_REQUEST = Symbol('auth/LOGOUT_REQUEST');
export const LOGOUT_SUCCESS = Symbol('auth/LOGOUT_SUCCESS');
export const LOGOUT_FAILURE = Symbol('auth/LOGOUT_FAILURE');
export const RECIEVE_AUTH_REQUEST = Symbol('auth/RECIEVE_AUTH_REQUEST');
export const RECIEVE_AUTH_SUCCESS = Symbol('auth/RECIEVE_AUTH_SUCCESS');
export const RECIEVE_AUTH_FAILURE = Symbol('auth/RECIEVE_AUTH_FAILURE');
Файл src/constants/index.js
export * from './auth';
export * from './services';
Файл src/constants/services.js
export const REDIRECT = Symbol('services/REDIRECT');
Файл src/constants/todo.js
export const FETCH_TODO_REQUEST = Symbol('todo/FETCH_TODO_REQUEST');
export const FETCH_TODO_SUCCESS = Symbol('todo/FETCH_TODO_SUCCESS');
export const FETCH_TODO_FAILURE = Symbol('todo/FETCH_TODO_FAILURE');
Файл src/containets/PrivateRoute.js
import React from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {Route, Redirect, withRouter} from 'react-router-dom';
import {recieveAuth} from '../actions';
class PrivateRoute extends React.Component {
componentDidMount() {
this.props.recieveAuth();
}
render() {
const { component: Component, isAuthenticated, ...rest } = this.props;
return (
<Route {...rest} render={props => (
isAuthenticated ? (
<Component {...props} />
) : (
<Redirect to={{
state: {from : props.location}
}} />
)
)} />
);
}
}
const mapStateToProps = state => ({
isAuthenticated: state.auth.isAuthenticated,
});
const mapDispatchToProps = dispatch => bindActionCreators({
recieveAuth
}, dispatch);
export default withRouter(connect(
mapStateToProps,
mapDispatchToProps
)(PrivateRoute));
Файл src/containers/TodoPage.js
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {fetchTodo} from '../actions/todo';
import * as fromTodos from '../reducers/todo';
import TodoPage from '../components/TodoPage';
const mapStateToProps = state => ({
todos: fromTodos.getByIds(state.todo, state.todo.todoIds)
});
const mapDispatchToProps = dispatch => bindActionCreators({
fetchTodo
}, dispatch);
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoPage);
Файл src/containers/WelcomePage.js
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import { signup, login } from '../actions';
import WelcomePage from '../components/WelcomePage';
const mapStateToProps = state => ({
isAuthenticated: state.auth.isAuthenticated,
});
const mapDispatchToProps = dispatch => bindActionCreators({
signup,
login,
}, dispatch);
export default connect(
mapStateToProps,
mapDispatchToProps
)(WelcomePage);
Файл src/reducers/auth.js
import * as types from '../constants';
const token = localStorage.getItem('token');
const initialState = {
isAuthenticated: !!token,
user: null,
token,
};
export default function auth(state = initialState, action) {
switch(action.type){
case types.SIGNUP_SUCCESS:
case types.LOGIN_SUCCESS:
return {
...state,
isAuthenticated: true,
user: action.payload.user,
token: action.payload.token
};
case types.RECIEVE_AUTH_SUCCESS:
return {
...state,
isAuthenticated: true,
user: action.payload.user,
}
case types.SIGNUP_FAILURE:
case types.LOGIN_FAILURE:
case types.RECIEVE_AUTH_FAILURE:
case types.LOGOUT_SUCCESS:
return {
...state,
isAuthenticated: false,
user: null,
token: '',
};
default:
return state;
}
}
Файл src/reducers/index.js
import {combineReducers} from 'redux';
import auth from './auth';
import todo from './todo';
export default combineReducers({
auth,
todo
});
Файл src/reducers/todo.js
import {combineReducers} from 'redux';
import * as types from '../constants/todo';
const initialState = {
todoIds: [],
byIds: {}
};
const todoIds = (state = initialState.todoIds, action) => {
switch (action.type) {
case types.FETCH_TODO_SUCCESS:
return action.payload.todo.map(getTodoId);
default:
return state;
}
}
const byIds = (state = initialState.byIds, action) => {
switch (action.type) {
case types.FETCH_TODO_SUCCESS:
return {
...state,
...action.payload.todo.reduce((ids, todo) => ({
...ids,
[todo._id] : todo
}), {}),
}
default:
return state;
}
}
export default combineReducers({
todoIds,
byIds
})
export const getTodoId = (todo) => todo._id;
export const getByIds = (state, ids) => ids.map(id => state.byIds[id]);
Файл src/store/index.js
import {createStore, applyMiddleware, compose} from 'redux';
import thunkMiddleware from 'redux-thunk';
import loggerMiddleware from 'redux-logger';
import rootReducer from '../reducers';
export default function configureStore() {
if (process.env.NODE_ENV === 'production') {
return createStore(
rootReducer,
applyMiddleware(
thunkMiddleware
)
)
} else {
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({serialize: true}) : compose;
const store = createStore(
rootReducer,
composeEnhancers(
applyMiddleware(
thunkMiddleware,
loggerMiddleware
)
)
)
if (module.hot) {
module.hot.accept('../reducers', ()=>{
store.replaceReducer(rootReducer)
})
}
return store;
}
}
Файл src/utils/call-api.js
import fetch from 'isomorphic-fetch';
export default function callApi(endpoint, token, options, payload) {
const authHeaders = token ? {
'Authorization' : `Bearer ${token}`
} : {};
return fetch(`http://localhost:5000${endpoint}`, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-type': 'application/json',
...authHeaders
},
body: JSON.stringify(payload),
...options
})
.then(response => response.json())
.then(json => {
if (json.success) {
return json;
}
throw new Error(json.message);
})
}
Файл src/utils/history.js
import {createBrowserHistory} from 'history';
export default createBrowserHistory();