login, signup, logout,

design
This commit is contained in:
2018-06-01 18:17:44 +03:00
parent af11ed28e4
commit 2508a79d29
31 changed files with 815 additions and 283 deletions

View File

@@ -14,7 +14,8 @@
{
"allowTernary": true
}
]
],
"max-len": "off"
},
"env": {
"browser": true

90
package-lock.json generated
View File

@@ -2072,13 +2072,13 @@
"resolved": "http://registry.npmjs.org/compression/-/compression-1.7.2.tgz",
"integrity": "sha1-qv+81qr4VLROuygDU9WtFlH1mmk=",
"requires": {
"accepts": "1.3.5",
"accepts": "~1.3.4",
"bytes": "3.0.0",
"compressible": "2.0.13",
"compressible": "~2.0.13",
"debug": "2.6.9",
"on-headers": "1.0.1",
"on-headers": "~1.0.1",
"safe-buffer": "5.1.1",
"vary": "1.1.2"
"vary": "~1.1.2"
},
"dependencies": {
"safe-buffer": {
@@ -2971,6 +2971,11 @@
"next-tick": "1"
}
},
"es6-error": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="
},
"es6-iterator": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
@@ -4772,6 +4777,18 @@
"resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
"integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0="
},
"history": {
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/history/-/history-4.7.2.tgz",
"integrity": "sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA==",
"requires": {
"invariant": "^2.2.1",
"loose-envify": "^1.2.0",
"resolve-pathname": "^2.2.0",
"value-equal": "^0.4.0",
"warning": "^3.0.0"
}
},
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@@ -9050,6 +9067,38 @@
"prop-types": "^15.6.0"
}
},
"react-router": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-4.2.0.tgz",
"integrity": "sha512-DY6pjwRhdARE4TDw7XjxjZsbx9lKmIcyZoZ+SDO7SBJ1KUeWNxT22Kara2AC7u6/c2SYEHlEDLnzBCcNhLE8Vg==",
"requires": {
"history": "^4.7.2",
"hoist-non-react-statics": "^2.3.0",
"invariant": "^2.2.2",
"loose-envify": "^1.3.1",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.5.4",
"warning": "^3.0.0"
}
},
"react-router-dom": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.2.2.tgz",
"integrity": "sha512-cHMFC1ZoLDfEaMFoKTjN7fry/oczMgRt5BKfMAkTu5zEuJvUiPp1J8d0eXSVTnBh6pxlbdqDhozunOOLtmKfPA==",
"requires": {
"history": "^4.7.2",
"invariant": "^2.2.2",
"loose-envify": "^1.3.1",
"prop-types": "^15.5.4",
"react-router": "^4.2.0",
"warning": "^3.0.0"
}
},
"react-router-redux": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/react-router-redux/-/react-router-redux-4.0.8.tgz",
"integrity": "sha1-InQDWWtRUeGCN32rg1tdRfD4BU4="
},
"react-scripts": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-1.1.4.tgz",
@@ -9367,6 +9416,21 @@
"symbol-observable": "^1.2.0"
}
},
"redux-form": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/redux-form/-/redux-form-7.3.0.tgz",
"integrity": "sha512-WcZRsRsVG25l8Cih3bEeeoZFxSIvoHqTpBRe5Ifl1ob7xvEpYLXyYYHAFER1DpTfMZPgTPHZ4UkR4ILFP3hzkw==",
"requires": {
"deep-equal": "^1.0.1",
"es6-error": "^4.1.1",
"hoist-non-react-statics": "^2.5.0",
"invariant": "^2.2.3",
"is-promise": "^2.1.0",
"lodash": "^4.17.5",
"lodash-es": "^4.17.5",
"prop-types": "^15.6.1"
}
},
"redux-thunk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
@@ -9604,6 +9668,11 @@
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz",
"integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY="
},
"resolve-pathname": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz",
"integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg=="
},
"resolve-url": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
@@ -10994,6 +11063,11 @@
"spdx-expression-parse": "^3.0.0"
}
},
"value-equal": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz",
"integrity": "sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw=="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -11030,6 +11104,14 @@
"makeerror": "1.0.x"
}
},
"warning": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz",
"integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=",
"requires": {
"loose-envify": "^1.0.0"
}
},
"watch": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/watch/-/watch-0.10.0.tgz",

View File

@@ -11,9 +11,12 @@
"react": "^16.4.0",
"react-dom": "^16.4.0",
"react-redux": "^5.0.7",
"react-router-dom": "^4.2.2",
"react-router-redux": "^4.0.8",
"react-scripts": "1.1.4",
"react-spring": "^5.3.7",
"redux": "^4.0.0",
"redux-form": "^7.3.0",
"redux-thunk": "^2.3.0"
},
"scripts": {

View File

@@ -5,7 +5,7 @@ body {
user-select: none;
}
#header {
#lists-header {
display: flex;
justify-content: space-between;
}
@@ -63,16 +63,14 @@ body {
margin: 0 auto;
padding: 0;
max-width: 25rem;
border: 1px solid black;
border: 1px solid #dddddd;
border-radius: 5px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
#inputs {
border-top: 1px solid #888888;
border-bottom: 1px solid #888888;
transition: 0.4s ease-in-out;
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
display: flex;
height: 2.5rem;
}
@@ -105,11 +103,6 @@ body {
border: none;
}
#inputs.no-border {
transition: 0.4s ease-in-out;
border-bottom: none;
}
li:first-child .delete,
li:first-child .edit,
li:first-child .save,
@@ -218,3 +211,25 @@ li {
font-weight: 700;
color: black;
}
#user-header {
display: flex;
height: 2rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
justify-content: flex-end;
align-content: center;
}
.logout {
height: 100%;
flex-grow: 0;
flex-shrink: 1;
margin: 0.2rem;
padding: 0.2rem;
color: #555555;
border: none;
background: none;
transition: 0.3s ease-in-out;
font-size: 1rem;
font-weight: 500;
}

View File

@@ -1,17 +0,0 @@
import * as React from 'react';
import 'normalize.css';
import './App.css';
import InputContainer from './containers/InputContainer';
import TodosContainer from './containers/TodosContainer';
import Header from './components/Header';
export default function App() {
return (
<div id="container">
<Header />
<InputContainer />
<TodosContainer />
</div>
);
}

View File

@@ -1,19 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import App from './App';
import todoApp from './reducers';
it('renders without crashing', () => {
const store = createStore(todoApp, applyMiddleware(thunk));
const div = document.createElement('div');
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
div,
);
ReactDOM.unmountComponentAtNode(div);
});

141
src/actions/lists.js Normal file
View File

@@ -0,0 +1,141 @@
import { API_ROOT, getToken } from './util';
export const ADD_LIST = 'ADD_LIST';
export const REMOVE_LIST = 'REMOVE_LIST';
export const EDIT_LIST_NAME = 'EDIT_LIST_NAME';
export const RECIEVE_LISTS = 'RECIEVE_LISTS';
export const REQUEST_LISTS = 'REQUEST_LISTS';
export const INVALIDATE_LISTS = 'INVALIDATE_LISTS';
export const VALIDATE_LISTS = 'VALIDATE_LISTS';
export const CHANGE_LIST = 'CHANGE_LIST';
function requestLists() {
return { type: REQUEST_LISTS };
}
function recieveLists(lists) {
return { type: RECIEVE_LISTS, lists };
}
function invalidateLists() {
return { type: INVALIDATE_LISTS };
}
function validateLists() {
return { type: VALIDATE_LISTS };
}
export function changeList(list) {
return { type: CHANGE_LIST, list };
}
function addListToState(list) {
return { type: ADD_LIST, list };
}
export function addList(name) {
return async (dispatch) => {
dispatch(invalidateLists());
const response = await fetch(`${API_ROOT}/lists`, {
body: JSON.stringify({ name }),
headers: {
Authorization: `Bearer ${getToken()}`,
'content-type': 'application/json',
},
method: 'POST',
});
const json = await response.json();
const list = json.data;
dispatch(addListToState(list));
dispatch(changeList(list.id));
dispatch(validateLists());
};
}
function removeListFromState(id) {
return { type: REMOVE_LIST, id };
}
export function removeList(id) {
return async (dispatch, getState) => {
dispatch(invalidateLists());
const response = await fetch(`${API_ROOT}/lists/${id}`, {
headers: {
Authorization: `Bearer ${getToken()}`,
'content-type': 'application/json',
},
method: 'DELETE',
});
const json = await response.json();
if (json.success) {
dispatch(removeListFromState(id));
const state = getState();
const list = state.lists.lists[Object.keys(state.lists.lists)[0]]
? state.lists.lists[Object.keys(state.lists.lists)[0]].id
: '';
dispatch(changeList(list));
}
dispatch(validateLists());
};
}
function editListNameInState(id, name) {
return { type: EDIT_LIST_NAME, id, name };
}
export function editList(id, name) {
return async (dispatch) => {
dispatch(invalidateLists());
const response = await fetch(`${API_ROOT}/lists/${id}`, {
body: JSON.stringify({ name }),
headers: {
Authorization: `Bearer ${getToken()}`,
'content-type': 'application/json',
},
method: 'PATCH',
});
const json = await response.json();
if (json.success) {
dispatch(editListNameInState(id, name));
}
dispatch(validateLists());
};
}
export function fetchLists() {
return async (dispatch) => {
dispatch(requestLists());
const response = await fetch(`${API_ROOT}/lists`, {
headers: {
Authorization: `Bearer ${getToken()}`,
},
});
const json = await response.json();
const lists = json.data;
const listsObj = lists.reduce((obj, list) => {
const newObj = { ...obj };
newObj[list.id] = list;
return newObj;
}, {});
dispatch(recieveLists(listsObj));
if (lists.length !== 0) {
dispatch(changeList(listsObj[Object.keys(listsObj)[0]].id));
}
localStorage.setItem('lists', JSON.stringify(listsObj));
};
}
export function loadLists() {
return async (dispatch) => {
dispatch(requestLists());
try {
const listsJson = localStorage.getTodo('lists');
const listsObj = JSON.parse(listsJson);
dispatch(recieveLists(listsObj));
dispatch(changeList(listsObj[Object.keys(listsObj)[0]].id));
} catch (e) {
localStorage.removeItem('lists');
}
dispatch(fetchLists());
};
}

View File

@@ -1,4 +1,4 @@
const API_ROOT = 'http://localhost:4000';
import { API_ROOT, getToken } from './util';
export const ADD_TODO = 'ADD_TODO';
export const REMOVE_TODO = 'REMOVE_TODO';
@@ -50,6 +50,7 @@ export function addTodo(text) {
const response = await fetch(`${API_ROOT}/lists/${list}/todos`, {
body: JSON.stringify({ text }),
headers: {
Authorization: `Bearer ${getToken()}`,
'content-type': 'application/json',
},
method: 'POST',
@@ -71,6 +72,7 @@ export function removeTodo(id) {
dispatch(invalidateTodos());
const response = await fetch(`${API_ROOT}/todos/${id}`, {
headers: {
Authorization: `Bearer ${getToken()}`,
'content-type': 'application/json',
},
method: 'DELETE',
@@ -93,6 +95,7 @@ export function toggleTodo(id) {
const response = await fetch(`${API_ROOT}/todos/${id}`, {
body: JSON.stringify({ completed }),
headers: {
Authorization: `Bearer ${getToken()}`,
'content-type': 'application/json',
},
method: 'PATCH',
@@ -115,6 +118,7 @@ export function editTodo(id, text) {
const response = await fetch(`${API_ROOT}/todos/${id}`, {
body: JSON.stringify({ text }),
headers: {
Authorization: `Bearer ${getToken()}`,
'content-type': 'application/json',
},
method: 'PATCH',
@@ -131,135 +135,13 @@ export function editTodo(id, text) {
export function fetchTodos(list) {
return async (dispatch) => {
dispatch(requestTodos(list));
const response = await fetch(`${API_ROOT}/lists/${list.id}/todos`);
const response = await fetch(`${API_ROOT}/lists/${list.id}/todos`, {
headers: {
Authorization: `Bearer ${getToken()}`,
},
});
const json = await response.json();
const todos = json.data;
dispatch(recieveTodos(list, todos));
};
}
export const ADD_LIST = 'ADD_LIST';
export const REMOVE_LIST = 'REMOVE_LIST';
export const EDIT_LIST_NAME = 'EDIT_LIST_NAME';
export const RECIEVE_LISTS = 'RECIEVE_LISTS';
export const REQUEST_LISTS = 'REQUEST_LISTS';
export const INVALIDATE_LISTS = 'INVALIDATE_LISTS';
export const VALIDATE_LISTS = 'VALIDATE_LISTS';
export const CHANGE_LIST = 'CHANGE_LIST';
function requestLists() {
return { type: REQUEST_LISTS };
}
function recieveLists(lists) {
return { type: RECIEVE_LISTS, lists };
}
function invalidateLists() {
return { type: INVALIDATE_LISTS };
}
function validateLists() {
return { type: VALIDATE_LISTS };
}
export function changeList(list) {
return { type: CHANGE_LIST, list };
}
function addListToState(list) {
return { type: ADD_LIST, list };
}
export function addList(name) {
return async (dispatch) => {
dispatch(invalidateLists());
const response = await fetch(`${API_ROOT}/lists`, {
body: JSON.stringify({ name }),
headers: {
'content-type': 'application/json',
},
method: 'POST',
});
const json = await response.json();
const list = json.data;
dispatch(addListToState(list));
dispatch(changeList(list.id));
dispatch(validateLists());
};
}
function removeListFromState(id) {
return { type: REMOVE_LIST, id };
}
export function removeList(id) {
return async (dispatch, getState) => {
dispatch(invalidateLists());
const response = await fetch(`${API_ROOT}/lists/${id}`, {
headers: {
'content-type': 'application/json',
},
method: 'DELETE',
});
const json = await response.json();
if (json.success) {
dispatch(removeListFromState(id));
const state = getState();
const list = state.lists.lists[Object.keys(state.lists.lists)[0]]
? state.lists.lists[Object.keys(state.lists.lists)[0]].id
: '';
dispatch(changeList(list));
}
dispatch(validateLists());
};
}
function editListNameInState(id, name) {
return { type: EDIT_LIST_NAME, id, name };
}
export function editList(id, name) {
return async (dispatch) => {
dispatch(invalidateLists());
const response = await fetch(`${API_ROOT}/lists/${id}`, {
body: JSON.stringify({ name }),
headers: {
'content-type': 'application/json',
},
method: 'PATCH',
});
const json = await response.json();
if (json.success) {
dispatch(editListNameInState(id, name));
}
dispatch(validateLists());
};
}
export function fetchLists() {
return async (dispatch) => {
dispatch(requestLists());
try {
const listsJson = localStorage.getTodo('lists');
const listsObj = JSON.parse(listsJson);
dispatch(recieveLists(listsObj));
dispatch(changeList(listsObj[Object.keys(listsObj)[0]].id));
} catch (e) {
localStorage.setItem('lists', JSON.stringify({}));
}
const response = await fetch(`${API_ROOT}/lists`);
const json = await response.json();
const lists = json.data;
const listsObj = lists.reduce((obj, list) => {
const newObj = { ...obj };
newObj[list.id] = list;
return newObj;
}, {});
dispatch(recieveLists(listsObj));
if (lists.length !== 0) {
dispatch(changeList(listsObj[Object.keys(listsObj)[0]].id));
}
localStorage.setItem('lists', JSON.stringify(listsObj));
};
}

102
src/actions/user.js Normal file
View File

@@ -0,0 +1,102 @@
import { API_ROOT, getToken } from './util';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_FAIL = 'LOGIN_FAIL';
export const SIGNUP_SUCCESS = 'SIGNUP_SUCCESS';
export const SIGNUP_FAIL = 'SIGNUP_FAIL';
export const LOGOUT = 'LOGOUT';
export const START_LOGIN = 'INVALIDATE_USER';
export const REQUEST_USER = 'REQUEST_USER';
export const VALIDATE_USER = 'VALIDATE_USER';
function startLogin() {
return { type: START_LOGIN };
}
function loginSuccess(user) {
return { type: LOGIN_SUCCESS, user };
}
function loginFail(error) {
return { type: LOGIN_FAIL, error };
}
function validateUser() {
return { type: VALIDATE_USER };
}
export function loadUser() {
return async (dispatch) => {
if (getToken()) {
const response = await fetch(`${API_ROOT}/users/user`, {
headers: {
Authorization: `Bearer ${getToken()}`,
'content-type': 'application/json',
},
method: 'GET',
});
const json = await response.json();
if (json.success) {
localStorage.setItem('jwt', json.data.jwt);
dispatch(loginSuccess(json.data));
} else {
dispatch(loginFail(json.error));
}
} else {
dispatch(validateUser());
}
};
}
export function login(user) {
return async (dispatch) => {
dispatch(startLogin());
const response = await fetch(`${API_ROOT}/users/login`, {
body: JSON.stringify(user),
headers: {
'content-type': 'application/json',
},
method: 'POST',
});
const json = await response.json();
if (json.success) {
localStorage.setItem('jwt', json.data.jwt);
dispatch(loginSuccess(json.data));
} else {
dispatch(loginFail(json.error));
}
};
}
function signupSuccess(user) {
return { type: SIGNUP_SUCCESS, user };
}
function signupFail(errors) {
return { type: SIGNUP_FAIL, errors };
}
export function signup(user) {
return async (dispatch) => {
dispatch(startLogin());
const response = await fetch(`${API_ROOT}/users`, {
body: JSON.stringify(user),
headers: {
'content-type': 'application/json',
},
method: 'POST',
});
const json = await response.json();
if (json.success) {
localStorage.setItem('jwt', json.data.jwt);
dispatch(signupSuccess(json.data));
} else {
dispatch(signupFail(json.error));
}
};
}
export function logout() {
localStorage.removeItem('jwt');
return { type: LOGOUT };
}

5
src/actions/util.js Normal file
View File

@@ -0,0 +1,5 @@
export const API_ROOT = 'http://localhost:4000';
export function getToken() {
return localStorage.getItem('jwt');
}

32
src/components/App.js Normal file
View File

@@ -0,0 +1,32 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import 'normalize.css';
import '../App.css';
import TodosContainer from '../containers/TodosContainer';
import LoginForm from '../components/user/LoginForm';
import SignupForm from '../components/user/SignupForm';
export default class App extends React.Component {
componentWillMount() {
this.props.loadUser();
}
render() {
return (
<Router>
<div id="container">
<Route exact path="/" component={TodosContainer} />
<Route path="/login" component={LoginForm} />
<Route path="/signup" component={SignupForm} />
</div>
</Router>
);
}
}
App.propTypes = {
loadUser: PropTypes.func.isRequired,
};

View File

@@ -1,16 +1,24 @@
import React from 'react';
import FilterLink from '../containers/FilterLink';
import { VisibilityFilters } from '../actions';
import LogoutLink from '../containers/LogoutLink';
import { VisibilityFilters } from '../actions/todos';
import ListsContainer from '../containers/ListsContainer';
export default function Header() {
return (
<div id="header">
<ListsContainer />
<div id="filters">
<FilterLink filter={VisibilityFilters.SHOW_ALL}>all</FilterLink>
<FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>active</FilterLink>
<FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>completed</FilterLink>
<div id="user-header">
<LogoutLink>logout</LogoutLink>
</div>
<div id="lists-header">
<ListsContainer />
<div id="filters">
<FilterLink filter={VisibilityFilters.SHOW_ALL}>all</FilterLink>
<FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>active</FilterLink>
<FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>
completed
</FilterLink>
</div>
</div>
</div>
);

View File

@@ -4,19 +4,13 @@ import PropTypes from 'prop-types';
function Input(props) {
let input;
const classes = [];
if (!props.inputBottomBorder) {
classes.push('no-border');
}
function submit() {
props.onClick(input.value);
input.value = '';
}
return (
<div id="inputs" className={classes.join(' ')}>
<div id="inputs">
<input
ref={(node) => {
input = node;
@@ -32,7 +26,6 @@ function Input(props) {
}
Input.propTypes = {
inputBottomBorder: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired,
};

24
src/components/Todos.js Normal file
View File

@@ -0,0 +1,24 @@
import React from 'react';
import { Redirect } from 'react-router-dom';
import InputContainer from '../containers/InputContainer';
import TodoListContainer from '../containers/TodoListContainer';
import Header from './Header';
export default function Todos({ user, loadLists, history }) {
if (user.dirty) {
return <div>loading</div>;
}
if (user.user) {
loadLists();
} else {
history.push('/login');
}
return (
<div id="todos">
<Header />
<InputContainer />
<TodoListContainer />
</div>
);
}

View File

@@ -0,0 +1,80 @@
import React from 'react';
import { Field, reduxForm } from 'redux-form';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { login } from '../../actions/user';
function validate(values) {
const errors = {};
if (values.username === '') {
errors.username = 'should have username';
}
return errors;
}
const InputField = ({
input, label, meta: { touched, error }, type,
}) => (
<div>
<label htmlFor={input.name}>
{label} <input {...input} type={type} />
</label>
{touched && error && <span className="error">{error}</span>}
</div>
);
function LoginForm({
handleSubmit, login, user, history,
}) {
let errors;
if (user.errors) {
if (user.errors.name === 'AuthenticationError') {
errors = <div>Wrong username or password</div>;
}
}
if (user.user) {
history.push('/');
}
return (
<div id="login--form">
{errors}
<form onSubmit={handleSubmit(login)}>
<Field
label="username"
name="username"
component={InputField}
type="text"
/>
<Field
label="password"
name="password"
component={InputField}
type="password"
/>
<button type="submit">Submit</button>
</form>
</div>
);
}
function mapStateToProps(state) {
return {
user: state.user,
};
}
function mapDispatchToProps(dispatch) {
return {
login: ({ username, password }) => dispatch(login({ username, password })),
};
}
export default reduxForm({
form: 'loginForm',
initialValues: {
username: '',
password: '',
},
validate,
})(withRouter(connect(mapStateToProps, mapDispatchToProps)(LoginForm)));

View File

@@ -0,0 +1,91 @@
import React from 'react';
import { Field, reduxForm } from 'redux-form';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { signup } from '../../actions/user';
function validate(values) {
const errors = {};
if (values.username === '') {
errors.username = 'should have username';
}
if (values.password !== values.passwordRepeat) {
errors.passwordRepeat = 'Invalid';
}
return errors;
}
const InputField = ({
input, label, meta: { touched, error }, type,
}) => (
<div>
<label htmlFor={input.name}>
{label} <input {...input} type={type} />
</label>
{touched && error && <span className="error">{error}</span>}
</div>
);
function SignupForm({
handleSubmit, signup, user, history,
}) {
let errors;
if (user.errors) {
if (user.errors.name === 'AuthenticationError') {
errors = <div>Wrong username or password</div>;
}
}
if (user.user) {
history.push('/');
}
return (
<div id="signup--form">
{errors}
<form onSubmit={handleSubmit(signup)}>
<Field
label="username"
name="username"
component={InputField}
type="text"
/>
<Field
label="password"
name="password"
component={InputField}
type="password"
/>
<Field
label="repeat pasword"
name="passwordRepeat"
component={InputField}
type="password"
/>
<button type="submit">Submit</button>
</form>
</div>
);
}
function mapStateToProps(state) {
return {
user: state.user,
};
}
function mapDispatchToProps(dispatch) {
return {
signup: ({ username, password }) =>
dispatch(signup({ username, password })),
};
}
export default reduxForm({
form: 'signupForm',
initialValues: {
username: '',
password: '',
passwordRepeat: '',
},
validate,
})(withRouter(connect(mapStateToProps, mapDispatchToProps)(SignupForm)));

View File

@@ -0,0 +1,19 @@
import { connect } from 'react-redux';
import App from '../components/App';
import { loadUser } from '../actions/user';
function mapStateToProps(state) {
return {
user: state.user,
};
}
function mapDispatchToProps(dispatch) {
return {
loadUser: () => dispatch(loadUser()),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(App);

View File

@@ -1,7 +1,31 @@
import { connect } from 'react-redux';
import React from 'react';
import PropTypes from 'prop-types';
import { setVisibilityFilter } from '../actions/todos';
import Link from '../components/Link';
import { setVisibilityFilter } from '../actions';
function Link({ active, onClick, children }) {
const classes = ['filter'];
if (active) {
classes.push('filter--active');
}
return (
<button
className={classes.join(' ')}
onClick={(e) => {
e.preventDefault();
onClick();
}}
>
{children}
</button>
);
}
Link.propTypes = {
active: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
};
function mapStateToProps(state, ownProps) {
return {
@@ -15,6 +39,4 @@ function mapDispatchToProps(dispatch, ownProps) {
};
}
const FilterLink = connect(mapStateToProps, mapDispatchToProps)(Link);
export default FilterLink;
export default connect(mapStateToProps, mapDispatchToProps)(Link);

View File

@@ -1,22 +1,7 @@
import { connect } from 'react-redux';
import Input from '../components/Input';
import { addTodo } from '../actions';
import getVisibleTodos from './getVisibleTodos';
function mapStateToProps(state) {
const { list } = state.lists;
try {
const listTodos = state.lists.lists[list].todos;
return {
inputBottomBorder: getVisibleTodos(listTodos, state.visibilityFilter).length !== 0,
};
} catch (e) {
return {
inputBottomBorder: false,
};
}
}
import { addTodo } from '../actions/todos';
function mapDispatchToProps(dispatch) {
return {
@@ -24,4 +9,4 @@ function mapDispatchToProps(dispatch) {
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Input);
export default connect(null, mapDispatchToProps)(Input);

View File

@@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import Lists from '../components/Lists';
import { changeList, removeList, addList, editList } from '../actions';
import { changeList, removeList, addList, editList } from '../actions/lists';
function mapStateToProps(state) {
return {

View File

@@ -1,11 +1,11 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
export default function Link({ active, onClick, children }) {
const classes = ['filter'];
if (active) {
classes.push('filter--active');
}
import { logout } from '../actions/user';
function Link({ onClick, children }) {
const classes = ['logout'];
return (
<button
className={classes.join(' ')}
@@ -20,7 +20,14 @@ export default function Link({ active, onClick, children }) {
}
Link.propTypes = {
active: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
};
function mapDispatchToProps(dispatch) {
return {
onClick: () => dispatch(logout()),
};
}
export default connect(null, mapDispatchToProps)(Link);

View File

@@ -0,0 +1,37 @@
import { connect } from 'react-redux';
import TodoList from '../components/TodoList';
import { toggleTodo, removeTodo, editTodo } from '../actions/todos';
import getVisibleTodos from './getVisibleTodos';
function mapStateToProps(state) {
const { list } = state.lists;
try {
const listObj = state.lists.lists[list];
const listTodos = state.lists.lists[list].todos;
return {
list,
todos: getVisibleTodos(listTodos, state.visibilityFilter),
dirty: listObj.dirty,
};
} catch (e) {
return {
list: '',
todos: [],
dirty: true,
};
}
}
function mapDispatchToProps(dispatch) {
return {
toggleTodo: id => dispatch(toggleTodo(id)),
removeTodo: id => dispatch(removeTodo(id)),
editTodo: (id, text) => dispatch(editTodo(id, text)),
};
}
const TodosContainer = connect(mapStateToProps, mapDispatchToProps)(TodoList);
export default TodosContainer;

View File

@@ -1,37 +1,20 @@
import { connect } from 'react-redux';
import TodoList from '../components/TodoList';
import { toggleTodo, removeTodo, editTodo } from '../actions';
import { withRouter } from 'react-router-dom';
import getVisibleTodos from './getVisibleTodos';
import Todos from '../components/Todos';
import { loadLists } from '../actions/lists';
function mapStateToProps(state) {
const { list } = state.lists;
try {
const listObj = state.lists.lists[list];
const listTodos = state.lists.lists[list].todos;
return {
list,
todos: getVisibleTodos(listTodos, state.visibilityFilter),
dirty: listObj.dirty,
};
} catch (e) {
return {
list: '',
todos: [],
dirty: true,
};
}
return {
user: state.user,
};
}
function mapDispatchToProps(dispatch) {
return {
toggleTodo: id => dispatch(toggleTodo(id)),
removeTodo: id => dispatch(removeTodo(id)),
editTodo: (id, text) => dispatch(editTodo(id, text)),
loadLists: () => dispatch(loadLists()),
};
}
const TodosContainer = connect(mapStateToProps, mapDispatchToProps)(TodoList);
export default TodosContainer;
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Todos));

View File

@@ -1,4 +1,4 @@
import { VisibilityFilters } from '../actions';
import { VisibilityFilters } from '../actions/todos';
export default function getVisibleTodos(todos, filter) {
switch (filter) {

View File

@@ -3,17 +3,15 @@ import ReactDOM from 'react-dom';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import App from './App';
import AppContainer from './containers/AppContainer';
import registerServiceWorker from './registerServiceWorker';
import todoApp from './reducers';
import { fetchLists } from './actions';
const store = createStore(todoApp, applyMiddleware(thunk));
store.dispatch(fetchLists());
ReactDOM.render(
<Provider store={store}>
<App />
<AppContainer />
</Provider>,
document.getElementById('root'),
);

View File

@@ -1,11 +1,15 @@
import { combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form';
import lists from './lists';
import visibilityFilter from './visibilityFilter';
import user from './user';
const todoApp = combineReducers({
lists,
visibilityFilter,
form: formReducer,
user,
});
export default todoApp;

View File

@@ -7,9 +7,12 @@ import {
INVALIDATE_TODOS,
VALIDATE_TODOS,
EDIT_TODO,
} from '../actions';
} from '../actions/todos';
export default function todos(state = { dirty: true, fetching: false, todos: [] }, action) {
export default function todos(
state = { dirty: true, fetching: false, todos: [] },
action,
) {
switch (action.type) {
case RECIEVE_TODOS:
return {
@@ -51,7 +54,10 @@ export default function todos(state = { dirty: true, fetching: false, todos: []
case TOGGLE_TODO: {
return {
...state,
todos: state.todos.map(todo => (todo.id === action.id ? { ...todo, completed: !todo.completed } : todo)),
todos: state.todos.map(todo =>
(todo.id === action.id
? { ...todo, completed: !todo.completed }
: todo)),
};
}
default:

View File

@@ -3,20 +3,21 @@ import {
INVALIDATE_LISTS,
VALIDATE_LISTS,
REQUEST_LISTS,
RECIEVE_TODOS,
RECIEVE_LISTS,
ADD_LIST,
REMOVE_LIST,
EDIT_LIST_NAME,
} from '../actions/lists';
import {
ADD_TODO,
INVALIDATE_TODOS,
VALIDATE_TODOS,
REQUEST_TODOS,
RECIEVE_TODOS,
REMOVE_TODO,
TOGGLE_TODO,
RECIEVE_LISTS,
ADD_LIST,
REMOVE_LIST,
EDIT_TODO,
EDIT_LIST_NAME,
} from '../actions';
} from '../actions/todos';
import list from './list';
export default function lists(

55
src/reducers/user.js Normal file
View File

@@ -0,0 +1,55 @@
import {
LOGIN_SUCCESS,
LOGIN_FAIL,
START_LOGIN,
LOGOUT,
SIGNUP_FAIL,
SIGNUP_SUCCESS,
VALIDATE_USER,
} from '../actions/user';
export default function user(
state = {
dirty: true,
fetching: false,
user: null,
errors: null,
},
action,
) {
switch (action.type) {
case VALIDATE_USER:
return {
...state,
dirty: false,
};
case START_LOGIN:
return {
...state,
fetching: true,
};
case SIGNUP_SUCCESS:
case LOGIN_SUCCESS:
return {
...state,
user: action.user,
dirty: false,
fetching: false,
};
case SIGNUP_FAIL:
case LOGIN_FAIL:
return {
...state,
errors: action.error,
dirty: false,
fetching: false,
};
case LOGOUT:
return {
...state,
user: null,
};
default:
return state;
}
}

View File

@@ -1,4 +1,4 @@
import { VisibilityFilters, SET_VISIBILITY_FILTER } from '../actions';
import { VisibilityFilters, SET_VISIBILITY_FILTER } from '../actions/todos';
const { SHOW_ALL } = VisibilityFilters;

View File

@@ -8,15 +8,11 @@
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
const isLocalhost = Boolean(window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/));
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
@@ -39,10 +35,8 @@ export default function register() {
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://goo.gl/SC7cgQ'
);
console.log('This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://goo.gl/SC7cgQ');
});
} else {
// Is not local host. Just register service worker
@@ -55,7 +49,7 @@ export default function register() {
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
.then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
@@ -76,7 +70,7 @@ function registerValidSW(swUrl) {
};
};
})
.catch(error => {
.catch((error) => {
console.error('Error during service worker registration:', error);
});
}
@@ -84,14 +78,14 @@ function registerValidSW(swUrl) {
function checkValidServiceWorker(swUrl) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
.then((response) => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload();
});
@@ -102,15 +96,13 @@ function checkValidServiceWorker(swUrl) {
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
console.log('No internet connection found. App is running in offline mode.');
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister();
});
}