mirror of
https://github.com/usatiuk/ustk-todolist.git
synced 2025-10-28 23:57:49 +01:00
login, signup, logout,
design
This commit is contained in:
@@ -14,7 +14,8 @@
|
||||
{
|
||||
"allowTernary": true
|
||||
}
|
||||
]
|
||||
],
|
||||
"max-len": "off"
|
||||
},
|
||||
"env": {
|
||||
"browser": true
|
||||
|
||||
90
package-lock.json
generated
90
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
37
src/App.css
37
src/App.css
@@ -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;
|
||||
}
|
||||
|
||||
17
src/App.js
17
src/App.js
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
141
src/actions/lists.js
Normal 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());
|
||||
};
|
||||
}
|
||||
@@ -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
102
src/actions/user.js
Normal 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
5
src/actions/util.js
Normal 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
32
src/components/App.js
Normal 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,
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
24
src/components/Todos.js
Normal 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>
|
||||
);
|
||||
}
|
||||
80
src/components/user/LoginForm.js
Normal file
80
src/components/user/LoginForm.js
Normal 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)));
|
||||
91
src/components/user/SignupForm.js
Normal file
91
src/components/user/SignupForm.js
Normal 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)));
|
||||
19
src/containers/AppContainer.js
Normal file
19
src/containers/AppContainer.js
Normal 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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
37
src/containers/TodoListContainer.js
Normal file
37
src/containers/TodoListContainer.js
Normal 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;
|
||||
@@ -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));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { VisibilityFilters } from '../actions';
|
||||
import { VisibilityFilters } from '../actions/todos';
|
||||
|
||||
export default function getVisibleTodos(todos, filter) {
|
||||
switch (filter) {
|
||||
|
||||
@@ -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'),
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
55
src/reducers/user.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { VisibilityFilters, SET_VISIBILITY_FILTER } from '../actions';
|
||||
import { VisibilityFilters, SET_VISIBILITY_FILTER } from '../actions/todos';
|
||||
|
||||
const { SHOW_ALL } = VisibilityFilters;
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user