update, lint, prettify everything

put backend sources into src
setup circleci
don't use react-loadable because it seems unnecessary
This commit is contained in:
2021-03-13 19:58:06 +03:00
parent f19a55790c
commit afd1f98254
98 changed files with 13582 additions and 10832 deletions

View File

@@ -1,25 +1,22 @@
{
"extends": [
"eslint:recommended",
"plugin:jest/recommended",
"plugin:react/recommended"
],
"rules": {
"react/jsx-filename-extension": [
1,
{
"extensions": [".js", ".jsx"]
}
"root": true,
"extends": [
"eslint:recommended",
"plugin:jest/recommended",
"plugin:react/recommended"
],
"linebreak-style": "off",
"react/forbid-prop-types": "off",
"node/no-unsupported-features/es-syntax": ["off"],
"node/no-unsupported-features/es-builtins": ["off"],
"node/no-unsupported-features/node-builtins": ["off"],
"react/display-name": ["warn"],
"no-console": "warn"
},
"env": {
"browser": true
}
"rules": {
"react/display-name": "warn"
},
"parserOptions": {
"ecmaVersion": 2018,
"ecmaFeatures": {
"jsx": true
},
"sourceType": "module"
},
"env": {
"browser": true,
"node": true
}
}

11283
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,28 +3,29 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/core": "^4.11.3",
"@material-ui/icons": "^4.11.2",
"@redux-offline/redux-offline": "^2.6.0",
"http-proxy-middleware": "^1.0.6",
"prop-types": "^15.7.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-loadable": "^5.5.0",
"react-redux": "^7.2.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-router-redux": "^4.0.8",
"react-scripts": "3.4.3",
"react-spring": "^5.0.0",
"react-scripts": "4.0.3",
"react-spring": "^8.0.27",
"redux": "^4.0.5",
"redux-form": "^8.3.6",
"redux-form": "^8.3.7",
"redux-thunk": "^2.3.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
"eject": "react-scripts eject",
"lint": "eslint ./src/** --ext .js,.jsx,.ts,.tsx",
"lint-fix": "eslint ./src/** --ext .js,.jsx,.ts,.tsx --fix"
},
"browserslist": [
">0.2%",

View File

@@ -1,42 +1,42 @@
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';
export const START_CREATE_LIST = 'START_CREATE_LIST';
export const START_EDIT_LIST = 'START_EDIT_LIST';
export const STOP_CREATE_LIST = 'STOP_CREATE_LIST';
export const STOP_EDIT_LIST = 'STOP_EDIT_LIST';
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";
export const START_CREATE_LIST = "START_CREATE_LIST";
export const START_EDIT_LIST = "START_EDIT_LIST";
export const STOP_CREATE_LIST = "STOP_CREATE_LIST";
export const STOP_EDIT_LIST = "STOP_EDIT_LIST";
export const ADD_TODO = 'ADD_TODO';
export const REMOVE_TODO = 'REMOVE_TODO';
export const TOGGLE_TODO = 'TOGGLE_TODO';
export const EDIT_TODO = 'EDIT_TODO';
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER';
export const RECIEVE_TODOS = 'RECIEVE_TODOS';
export const REQUEST_TODOS = 'REQUEST_TODOS';
export const INVALIDATE_TODOS = 'INVALIDATE_TODOS';
export const VALIDATE_TODOS = 'VALIDATE_TODOS';
export const ADD_TODO = "ADD_TODO";
export const REMOVE_TODO = "REMOVE_TODO";
export const TOGGLE_TODO = "TOGGLE_TODO";
export const EDIT_TODO = "EDIT_TODO";
export const SET_VISIBILITY_FILTER = "SET_VISIBILITY_FILTER";
export const RECIEVE_TODOS = "RECIEVE_TODOS";
export const REQUEST_TODOS = "REQUEST_TODOS";
export const INVALIDATE_TODOS = "INVALIDATE_TODOS";
export const VALIDATE_TODOS = "VALIDATE_TODOS";
export const VisibilityFilters = {
SHOW_ALL: 'SHOW_ALL',
SHOW_COMPLETED: 'SHOW_COMPLETED',
SHOW_ACTIVE: 'SHOW_ACTIVE',
SHOW_ALL: "SHOW_ALL",
SHOW_COMPLETED: "SHOW_COMPLETED",
SHOW_ACTIVE: "SHOW_ACTIVE",
};
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';
export const RESET_USER = 'RESET_USER';
export const EDIT_START = 'EDIT_START';
export const EDIT_SUCCESS = 'EDIT_SUCCESS';
export const EDIT_FAIL = 'EDIT_FAIL';
export const RESET_EDIT = 'RESET_EDIT';
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";
export const RESET_USER = "RESET_USER";
export const EDIT_START = "EDIT_START";
export const EDIT_SUCCESS = "EDIT_SUCCESS";
export const EDIT_FAIL = "EDIT_FAIL";
export const RESET_EDIT = "RESET_EDIT";

View File

@@ -1,162 +1,162 @@
import {
REQUEST_LISTS,
RECIEVE_LISTS,
CHANGE_LIST,
START_CREATE_LIST,
START_EDIT_LIST,
STOP_CREATE_LIST,
STOP_EDIT_LIST,
ADD_LIST,
INVALIDATE_LISTS,
REMOVE_LIST,
EDIT_LIST_NAME,
RECIEVE_TODOS,
} from './defs';
REQUEST_LISTS,
RECIEVE_LISTS,
CHANGE_LIST,
START_CREATE_LIST,
START_EDIT_LIST,
STOP_CREATE_LIST,
STOP_EDIT_LIST,
ADD_LIST,
INVALIDATE_LISTS,
REMOVE_LIST,
EDIT_LIST_NAME,
RECIEVE_TODOS,
} from "./defs";
import { API_ROOT, getToken, mongoObjectId } from './util';
import { API_ROOT, getToken, mongoObjectId } from "./util";
function requestLists() {
return { type: REQUEST_LISTS };
return { type: REQUEST_LISTS };
}
function recieveLists(lists) {
return { type: RECIEVE_LISTS, lists };
return { type: RECIEVE_LISTS, lists };
}
export function changeList(list) {
return { type: CHANGE_LIST, list };
return { type: CHANGE_LIST, list };
}
export function startCreateList() {
return { type: START_CREATE_LIST };
return { type: START_CREATE_LIST };
}
export function startEditList() {
return { type: START_EDIT_LIST };
return { type: START_EDIT_LIST };
}
export function stopCreateList() {
return { type: STOP_CREATE_LIST };
return { type: STOP_CREATE_LIST };
}
export function stopEditList() {
return { type: STOP_EDIT_LIST };
return { type: STOP_EDIT_LIST };
}
export function addList(name) {
return async dispatch => {
const id = mongoObjectId();
dispatch({
type: ADD_LIST,
list: {
name,
id,
todos: [],
},
meta: {
offline: {
effect: {
url: `${API_ROOT}/lists`,
body: JSON.stringify({ name, id }),
headers: {
Authorization: `Bearer ${getToken()}`,
'content-type': 'application/json',
return async (dispatch) => {
const id = mongoObjectId();
dispatch({
type: ADD_LIST,
list: {
name,
id,
todos: [],
},
method: 'POST',
},
rollback: {
type: INVALIDATE_LISTS,
},
},
},
});
};
meta: {
offline: {
effect: {
url: `${API_ROOT}/lists`,
body: JSON.stringify({ name, id }),
headers: {
Authorization: `Bearer ${getToken()}`,
"content-type": "application/json",
},
method: "POST",
},
rollback: {
type: INVALIDATE_LISTS,
},
},
},
});
};
}
export function removeList() {
return async (dispatch, getState) => {
const state = getState();
const { list } = state.lists;
dispatch({
type: REMOVE_LIST,
list,
meta: {
offline: {
effect: {
url: `${API_ROOT}/lists/${list}`,
headers: {
Authorization: `Bearer ${getToken()}`,
'content-type': 'application/json',
return async (dispatch, getState) => {
const state = getState();
const { list } = state.lists;
dispatch({
type: REMOVE_LIST,
list,
meta: {
offline: {
effect: {
url: `${API_ROOT}/lists/${list}`,
headers: {
Authorization: `Bearer ${getToken()}`,
"content-type": "application/json",
},
method: "DELETE",
},
rollback: {
type: INVALIDATE_LISTS,
},
},
},
method: 'DELETE',
},
rollback: {
type: INVALIDATE_LISTS,
},
},
},
});
};
});
};
}
export function editList(name) {
return async (dispatch, getState) => {
const state = getState();
const { list } = state.lists;
dispatch({
type: EDIT_LIST_NAME,
list,
name,
meta: {
offline: {
effect: {
url: `${API_ROOT}/lists/${list}`,
body: JSON.stringify({ name }),
headers: {
Authorization: `Bearer ${getToken()}`,
'content-type': 'application/json',
return async (dispatch, getState) => {
const state = getState();
const { list } = state.lists;
dispatch({
type: EDIT_LIST_NAME,
list,
name,
meta: {
offline: {
effect: {
url: `${API_ROOT}/lists/${list}`,
body: JSON.stringify({ name }),
headers: {
Authorization: `Bearer ${getToken()}`,
"content-type": "application/json",
},
method: "PATCH",
},
rollback: {
type: INVALIDATE_LISTS,
},
},
},
method: 'PATCH',
},
rollback: {
type: INVALIDATE_LISTS,
},
},
},
});
};
});
};
}
function normalizeTodos(lists) {
return lists.reduce((todos, list) => {
const listTodosObj = list.todos.reduce(
(listTodos, todo) => ({
...listTodos,
[todo.id]: { ...todo },
}),
{},
);
return { ...todos, ...listTodosObj };
}, {});
return lists.reduce((todos, list) => {
const listTodosObj = list.todos.reduce(
(listTodos, todo) => ({
...listTodos,
[todo.id]: { ...todo },
}),
{},
);
return { ...todos, ...listTodosObj };
}, {});
}
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, curList) => {
const newObj = { ...obj };
newObj[curList.id] = {
dirty: true,
fetching: false,
editing: false,
...curList,
todos: curList.todos.map(todo => todo.id),
};
return newObj;
}, {});
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, curList) => {
const newObj = { ...obj };
newObj[curList.id] = {
dirty: true,
fetching: false,
editing: false,
...curList,
todos: curList.todos.map((todo) => todo.id),
};
return newObj;
}, {});
dispatch({ type: RECIEVE_TODOS, todos: normalizeTodos(lists) });
dispatch(recieveLists(listsObj));
};
dispatch({ type: RECIEVE_TODOS, todos: normalizeTodos(lists) });
dispatch(recieveLists(listsObj));
};
}

View File

@@ -1,137 +1,137 @@
import {
REQUEST_TODOS,
RECIEVE_TODOS,
ADD_TODO,
REMOVE_TODO,
TOGGLE_TODO,
EDIT_TODO,
INVALIDATE_LISTS,
} from './defs';
REQUEST_TODOS,
RECIEVE_TODOS,
ADD_TODO,
REMOVE_TODO,
TOGGLE_TODO,
EDIT_TODO,
INVALIDATE_LISTS,
} from "./defs";
import { API_ROOT, getToken, mongoObjectId } from './util';
import { API_ROOT, getToken, mongoObjectId } from "./util";
export function fetchTodos() {
return async dispatch => {
dispatch({ type: REQUEST_TODOS });
const response = await fetch(`${API_ROOT}/todos`, {
headers: {
Authorization: `Bearer ${getToken()}`,
},
});
const json = await response.json();
const todos = json.data;
dispatch({ type: RECIEVE_TODOS, todos });
};
return async (dispatch) => {
dispatch({ type: REQUEST_TODOS });
const response = await fetch(`${API_ROOT}/todos`, {
headers: {
Authorization: `Bearer ${getToken()}`,
},
});
const json = await response.json();
const todos = json.data;
dispatch({ type: RECIEVE_TODOS, todos });
};
}
export function addTodo(text) {
return async (dispatch, getState) => {
const state = getState();
const { list } = state.lists;
const id = mongoObjectId();
if (list) {
dispatch({
type: ADD_TODO,
todo: {
text,
id,
completed: false,
},
meta: {
offline: {
effect: {
url: `${API_ROOT}/lists/${list}/todos`,
body: JSON.stringify({ text, id }),
headers: {
Authorization: `Bearer ${getToken()}`,
'content-type': 'application/json',
},
method: 'POST',
},
rollback: {
type: INVALIDATE_LISTS,
},
},
},
});
}
};
return async (dispatch, getState) => {
const state = getState();
const { list } = state.lists;
const id = mongoObjectId();
if (list) {
dispatch({
type: ADD_TODO,
todo: {
text,
id,
completed: false,
},
meta: {
offline: {
effect: {
url: `${API_ROOT}/lists/${list}/todos`,
body: JSON.stringify({ text, id }),
headers: {
Authorization: `Bearer ${getToken()}`,
"content-type": "application/json",
},
method: "POST",
},
rollback: {
type: INVALIDATE_LISTS,
},
},
},
});
}
};
}
export function removeTodo(id) {
return async dispatch => {
dispatch({
type: REMOVE_TODO,
id,
meta: {
offline: {
effect: {
url: `${API_ROOT}/todos/${id}`,
headers: {
Authorization: `Bearer ${getToken()}`,
'content-type': 'application/json',
return async (dispatch) => {
dispatch({
type: REMOVE_TODO,
id,
meta: {
offline: {
effect: {
url: `${API_ROOT}/todos/${id}`,
headers: {
Authorization: `Bearer ${getToken()}`,
"content-type": "application/json",
},
method: "DELETE",
},
rollback: {
type: INVALIDATE_LISTS,
},
},
},
method: 'DELETE',
},
rollback: {
type: INVALIDATE_LISTS,
},
},
},
});
};
});
};
}
export function toggleTodo(id) {
return async (dispatch, getState) => {
const state = getState();
const todoObj = state.todos.todos[id];
const completed = !todoObj.completed;
dispatch({
type: TOGGLE_TODO,
id,
meta: {
offline: {
effect: {
url: `${API_ROOT}/todos/${id}`,
body: JSON.stringify({ completed }),
headers: {
Authorization: `Bearer ${getToken()}`,
'content-type': 'application/json',
return async (dispatch, getState) => {
const state = getState();
const todoObj = state.todos.todos[id];
const completed = !todoObj.completed;
dispatch({
type: TOGGLE_TODO,
id,
meta: {
offline: {
effect: {
url: `${API_ROOT}/todos/${id}`,
body: JSON.stringify({ completed }),
headers: {
Authorization: `Bearer ${getToken()}`,
"content-type": "application/json",
},
method: "PATCH",
},
rollback: {
type: INVALIDATE_LISTS,
},
},
},
method: 'PATCH',
},
rollback: {
type: INVALIDATE_LISTS,
},
},
},
});
};
});
};
}
export function editTodo(id, text) {
return async dispatch => {
dispatch({
type: EDIT_TODO,
id,
text,
meta: {
offline: {
effect: {
url: `${API_ROOT}/todos/${id}`,
body: JSON.stringify({ text }),
headers: {
Authorization: `Bearer ${getToken()}`,
'content-type': 'application/json',
return async (dispatch) => {
dispatch({
type: EDIT_TODO,
id,
text,
meta: {
offline: {
effect: {
url: `${API_ROOT}/todos/${id}`,
body: JSON.stringify({ text }),
headers: {
Authorization: `Bearer ${getToken()}`,
"content-type": "application/json",
},
method: "PATCH",
},
rollback: {
type: INVALIDATE_LISTS,
},
},
},
method: 'PATCH',
},
rollback: {
type: INVALIDATE_LISTS,
},
},
},
});
};
});
};
}

View File

@@ -1,188 +1,186 @@
import {
START_LOGIN,
LOGIN_SUCCESS,
LOGIN_FAIL,
VALIDATE_USER,
SIGNUP_SUCCESS,
SIGNUP_FAIL,
RESET_USER,
LOGOUT,
EDIT_START,
EDIT_SUCCESS,
EDIT_FAIL,
RESET_EDIT,
} from './defs';
START_LOGIN,
LOGIN_SUCCESS,
LOGIN_FAIL,
VALIDATE_USER,
SIGNUP_SUCCESS,
SIGNUP_FAIL,
RESET_USER,
LOGOUT,
EDIT_START,
EDIT_SUCCESS,
EDIT_FAIL,
RESET_EDIT,
} from "./defs";
import { API_ROOT, getToken, setToken } from './util';
import { fetchLists } from './lists';
import { API_ROOT, getToken, setToken } from "./util";
import { fetchLists } from "./lists";
function startLogin() {
return { type: START_LOGIN };
return { type: START_LOGIN };
}
function loginSuccess(user) {
return { type: LOGIN_SUCCESS, user };
return { type: LOGIN_SUCCESS, user };
}
function loginFail(error) {
return { type: LOGIN_FAIL, error };
return { type: LOGIN_FAIL, error };
}
function validateUser() {
return { type: VALIDATE_USER };
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) {
dispatch(loginSuccess(json.data));
dispatch(fetchLists());
} else {
dispatch(loginFail(json.error));
}
} else {
dispatch(validateUser());
}
};
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) {
dispatch(loginSuccess(json.data));
dispatch(fetchLists());
} 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) {
setToken(json.data.jwt);
dispatch(loginSuccess(json.data));
dispatch(fetchLists());
} else {
dispatch(loginFail(json.error));
}
};
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) {
setToken(json.data.jwt);
dispatch(loginSuccess(json.data));
dispatch(fetchLists());
} else {
dispatch(loginFail(json.error));
}
};
}
export function loginJWT(jwt) {
return async dispatch => {
dispatch(startLogin());
const response = await fetch(`${API_ROOT}/users/user`, {
headers: {
'content-type': 'application/json',
Authorization: `Bearer ${jwt}`,
},
method: 'GET',
});
const json = await response.json();
if (json.success) {
setToken(jwt);
dispatch(loginSuccess(json.data));
dispatch(fetchLists());
} else {
dispatch(loginFail(json.error));
}
};
return async (dispatch) => {
dispatch(startLogin());
const response = await fetch(`${API_ROOT}/users/user`, {
headers: {
"content-type": "application/json",
Authorization: `Bearer ${jwt}`,
},
method: "GET",
});
const json = await response.json();
if (json.success) {
setToken(jwt);
dispatch(loginSuccess(json.data));
dispatch(fetchLists());
} else {
dispatch(loginFail(json.error));
}
};
}
function signupSuccess(user) {
return { type: SIGNUP_SUCCESS, user };
return { type: SIGNUP_SUCCESS, user };
}
function signupFail(error) {
return { type: SIGNUP_FAIL, error };
return { type: SIGNUP_FAIL, error };
}
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) {
setToken(json.data.jwt);
dispatch(signupSuccess(json.data));
dispatch(fetchLists());
} else {
dispatch(signupFail(json.error));
}
};
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) {
setToken(json.data.jwt);
dispatch(signupSuccess(json.data));
dispatch(fetchLists());
} else {
dispatch(signupFail(json.error));
}
};
}
function startEdit(user) {
return { type: EDIT_START, user };
return { type: EDIT_START, user };
}
function editSuccess(user) {
return { type: EDIT_SUCCESS, user };
return { type: EDIT_SUCCESS, user };
}
function editFail(error) {
return { type: EDIT_FAIL, error };
return { type: EDIT_FAIL, error };
}
export function edit(user) {
return async dispatch => {
dispatch(startEdit());
const response = await fetch(`${API_ROOT}/users/user`, {
body: JSON.stringify(user),
headers: {
Authorization: `Bearer ${getToken()}`,
'content-type': 'application/json',
},
method: 'PATCH',
});
const json = await response.json();
if (json.success) {
dispatch(editSuccess(json.data));
} else {
dispatch(editFail(json.error));
}
};
return async (dispatch) => {
dispatch(startEdit());
const response = await fetch(`${API_ROOT}/users/user`, {
body: JSON.stringify(user),
headers: {
Authorization: `Bearer ${getToken()}`,
"content-type": "application/json",
},
method: "PATCH",
});
const json = await response.json();
if (json.success) {
dispatch(editSuccess(json.data));
} else {
dispatch(editFail(json.error));
}
};
}
export function deleteUser() {
return async dispatch => {
await fetch(`${API_ROOT}/users/user`, {
headers: {
Authorization: `Bearer ${getToken()}`,
'content-type': 'application/json',
},
method: 'DELETE',
});
dispatch(reset());
};
return async (dispatch) => {
await fetch(`${API_ROOT}/users/user`, {
headers: {
Authorization: `Bearer ${getToken()}`,
"content-type": "application/json",
},
method: "DELETE",
});
dispatch(reset());
};
}
export function resetEdit() {
return { type: RESET_EDIT };
return { type: RESET_EDIT };
}
export function reset() {
return { type: RESET_USER };
return { type: RESET_USER };
}
export function logout() {
return async dispatch => {
dispatch({ type: LOGOUT });
};
return async (dispatch) => {
dispatch({ type: LOGOUT });
};
}

View File

@@ -1,23 +1,23 @@
export const API_ROOT = '/__';
export const API_ROOT = "/__";
let token = null;
export function setToken(_token) {
token = _token;
token = _token;
}
export function getToken() {
return token;
return token;
}
export function mongoObjectId() {
// eslint-disable-next-line
const timestamp = ((new Date().getTime() / 1000) | 0).toString(16);
return (
timestamp +
'xxxxxxxxxxxxxxxx'
// eslint-disable-next-line
.replace(/[x]/g, () => ((Math.random() * 16) | 0).toString(16))
.toLowerCase()
);
// eslint-disable-next-line
const timestamp = ((new Date().getTime() / 1000) | 0).toString(16);
return (
timestamp +
"xxxxxxxxxxxxxxxx"
// eslint-disable-next-line
.replace(/[x]/g, () => ((Math.random() * 16) | 0).toString(16))
.toLowerCase()
);
}

View File

@@ -1,5 +1,5 @@
import { SET_VISIBILITY_FILTER } from './defs';
import { SET_VISIBILITY_FILTER } from "./defs";
export default function setVisibilityFilter(filter) {
return { type: SET_VISIBILITY_FILTER, filter };
return { type: SET_VISIBILITY_FILTER, filter };
}

View File

@@ -1,246 +1,246 @@
#lists-header {
display: flex;
justify-content: space-between;
background-color: #fbfbfb;
display: flex;
justify-content: space-between;
background-color: #fbfbfb;
}
#lists {
display: flex;
flex-direction: row;
flex-grow: 1;
width: 80%;
margin-right: 1rem;
height: 100px;
display: flex;
flex-direction: row;
flex-grow: 1;
width: 80%;
margin-right: 1rem;
height: 100px;
}
.loading {
align-self: center;
margin-left: 2rem;
align-self: center;
margin-left: 2rem;
}
#listactions {
display: flex;
flex-direction: column;
justify-content: center;
margin: 0.2rem 0.1rem;
margin-left: 0.5rem;
display: flex;
flex-direction: column;
justify-content: center;
margin: 0.2rem 0.1rem;
margin-left: 0.5rem;
}
#listactions button {
color: #555555;
background: none;
border: none;
margin: 0.1rem 0.3rem;
padding: 0.3rem 0.7em;
color: #555555;
background: none;
border: none;
margin: 0.1rem 0.3rem;
padding: 0.3rem 0.7em;
}
#listactions .backbutton {
margin: 1rem 0;
margin: 1rem 0;
}
#filters {
display: flex;
justify-content: center;
margin: 0;
align-self: center;
flex-shrink: 0;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
position: sticky;
z-index: 5;
display: flex;
justify-content: center;
margin: 0;
align-self: center;
flex-shrink: 0;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
position: sticky;
z-index: 5;
}
#filters button {
min-width: 3rem;
min-width: 3rem;
}
#inputs {
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.1);
display: flex;
height: 2.5rem;
position: relative;
z-index: 10;
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.1);
display: flex;
height: 2.5rem;
position: relative;
z-index: 10;
}
#input {
color: black;
background: white;
font-family: 'Roboto';
box-sizing: border-box;
font-size: 1rem;
flex-grow: 1;
margin: 0;
padding: 0;
padding-left: 1rem;
height: 100%;
line-height: 100%;
border: none;
color: black;
background: white;
font-family: "Roboto";
box-sizing: border-box;
font-size: 1rem;
flex-grow: 1;
margin: 0;
padding: 0;
padding-left: 1rem;
height: 100%;
line-height: 100%;
border: none;
}
#input::placeholder {
opacity: 0.35;
opacity: 0.35;
}
#input:focus::placeholder {
opacity: 0;
opacity: 0;
}
#add {
color: black;
flex-grow: 0;
margin: 0;
padding: 0;
height: 100%;
max-width: 2rem;
background-color: white;
border: none;
color: black;
flex-grow: 0;
margin: 0;
padding: 0;
height: 100%;
max-width: 2rem;
background-color: white;
border: none;
}
li:first-child .delete,
li:first-child .edit,
li:first-child .save,
li:first-child .todo {
border-top: none;
border-top: none;
}
.done {
color: rgba(0, 0, 0, 0.6);
text-decoration: line-through;
color: rgba(0, 0, 0, 0.6);
text-decoration: line-through;
}
li button {
color: black;
outline: none;
text-align: left;
flex-shrink: 0;
box-sizing: border-box;
padding: 0;
text-align: center;
border: none;
border-top: 1px solid #f0f0f0;
background: none;
width: 2rem;
font-size: 1rem;
transition: 0.17s ease-in-out;
overflow: hidden;
box-shadow: inset 3px 0 6px -3px rgba(0, 0, 0, 0.3);
color: black;
outline: none;
text-align: left;
flex-shrink: 0;
box-sizing: border-box;
padding: 0;
text-align: center;
border: none;
border-top: 1px solid #f0f0f0;
background: none;
width: 2rem;
font-size: 1rem;
transition: 0.17s ease-in-out;
overflow: hidden;
box-shadow: inset 3px 0 6px -3px rgba(0, 0, 0, 0.3);
}
/* make it usable on smartphones */
@media only screen and (max-width: 600px) {
#header {
position: fixed;
top: 0;
left: 0;
background-color: white;
z-index: 5;
width: 100%;
}
#inputs {
position: fixed;
top: 8rem;
left: 0;
background-color: white;
z-index: 10;
width: 100%;
height: 65px;
max-height: 65px !important;
}
#container {
margin-top: 12rem;
border-top-left-radius: 0;
border-top-right-radius: 0;
margin-bottom: 3rem;
}
#filters {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
max-height: 3rem !important;
opacity: 1 !important;
background-color: white;
}
#filters button {
height: 3rem !important;
padding: 0 1.5rem !important;
}
li button {
padding: 0 1.5rem !important;
}
#header {
position: fixed;
top: 0;
left: 0;
background-color: white;
z-index: 5;
width: 100%;
}
#inputs {
position: fixed;
top: 8rem;
left: 0;
background-color: white;
z-index: 10;
width: 100%;
height: 65px;
max-height: 65px !important;
}
#container {
margin-top: 12rem;
border-top-left-radius: 0;
border-top-right-radius: 0;
margin-bottom: 3rem;
}
#filters {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
max-height: 3rem !important;
opacity: 1 !important;
background-color: white;
}
#filters button {
height: 3rem !important;
padding: 0 1.5rem !important;
}
li button {
padding: 0 1.5rem !important;
}
}
li button.todo {
padding: 0.5rem;
box-shadow: none;
padding: 0.5rem;
box-shadow: none;
}
ul {
margin: 0;
padding: 0;
margin: 0;
padding: 0;
}
li {
height: 60px;
width: 100%;
font-weight: 500;
overflow: hidden;
font-size: 1rem;
display: flex;
height: 60px;
width: 100%;
font-weight: 500;
overflow: hidden;
font-size: 1rem;
display: flex;
}
.todo {
text-align: left;
box-sizing: border-box;
word-wrap: break-word;
width: 100%;
padding: 0.5rem;
padding-left: 1rem;
border-top: 1px solid #f0f0f0;
font-weight: 400;
flex-grow: 2;
flex-shrink: 1;
transition: 0.1s ease-in-out;
font-family: Roboto;
text-align: left;
box-sizing: border-box;
word-wrap: break-word;
width: 100%;
padding: 0.5rem;
padding-left: 1rem;
border-top: 1px solid #f0f0f0;
font-weight: 400;
flex-grow: 2;
flex-shrink: 1;
transition: 0.1s ease-in-out;
font-family: Roboto;
}
textarea.todo--input {
background: #fafafa;
color: black;
resize: none;
border: none;
margin: 0;
padding: 0;
box-sizing: border-box;
word-wrap: break-word;
width: 100%;
height: 100%;
max-height: 100%;
font-family: 'Roboto';
font-size: 1rem;
transition: 0.1s ease-in-out;
background: #fafafa;
color: black;
resize: none;
border: none;
margin: 0;
padding: 0;
box-sizing: border-box;
word-wrap: break-word;
width: 100%;
height: 100%;
max-height: 100%;
font-family: "Roboto";
font-size: 1rem;
transition: 0.1s ease-in-out;
}
.disabled {
width: 2rem;
background-color: #fafafa;
color: #555555;
box-shadow: none;
padding: 0.5rem 1rem;
border-top: 1px solid #f0f0f0;
width: 2rem;
background-color: #fafafa;
color: #555555;
box-shadow: none;
padding: 0.5rem 1rem;
border-top: 1px solid #f0f0f0;
}
.filter {
margin: 0.1rem;
color: #555555;
border: none;
background: none;
transition: 0.1s ease-in-out;
font-size: 1rem;
font-weight: 400;
font-family: Roboto;
margin: 0.1rem;
color: #555555;
border: none;
background: none;
transition: 0.1s ease-in-out;
font-size: 1rem;
font-weight: 400;
font-family: Roboto;
}
.filter--active {
font-weight: 500;
color: black;
font-weight: 500;
color: black;
}

View File

@@ -1,83 +1,48 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import CssBaseline from '@material-ui/core/CssBaseline';
import Loadable from 'react-loadable';
import * as React from "react";
import PropTypes from "prop-types";
import { BrowserRouter as Router, Route } from "react-router-dom";
import CssBaseline from "@material-ui/core/CssBaseline";
import Protected from './Protected';
import OnlyUnauth from './OnlyUnauth';
import './Container.css';
import './App.css';
import Protected from "./Protected";
import OnlyUnauth from "./OnlyUnauth";
import "./Container.css";
import "./App.css";
import TodosView from "./todolist/TodosView";
import LoginForm from "./user/LoginForm";
import SignupForm from "./user/SignupForm";
import EditForm from "./user/EditForm";
function Loading(props) {
if (props.error) {
return (
<div>
Error! <button onClick={props.retry}>Retry</button>
</div>
);
} else if (props.pastDelay) {
return <div>Loading...</div>;
} else {
return null;
}
}
const ProtectedTodosView = Protected(TodosView);
const LoadableTodosView = Protected(
Loadable({
loader: () => import('./todolist/TodosView'),
loading: () => Loading,
delay: 1000,
}),
);
const ProtectedLoginForm = OnlyUnauth(LoginForm);
const LoadableLoginForm = OnlyUnauth(
Loadable({
loader: () => import('./user/LoginForm'),
loading: () => Loading,
delay: 1000,
}),
);
const ProtectedSignupForm = OnlyUnauth(SignupForm);
const LoadableSignupForm = OnlyUnauth(
Loadable({
loader: () => import('./user/SignupForm'),
loading: () => Loading,
delay: 1000,
}),
);
const LoadableEditView = Protected(
Loadable({
loader: () => import('./user/EditForm'),
loading: () => Loading,
delay: 1000,
}),
);
const ProtectedEditView = Protected(EditForm);
export default class App extends React.PureComponent {
componentDidMount() {
const { loadUser } = this.props;
loadUser();
}
componentDidMount() {
const { loadUser } = this.props;
loadUser();
}
render() {
return (
<React.Fragment>
<CssBaseline />
<Router>
<div id="container">
<Route exact path="/" component={LoadableTodosView} />
<Route path="/login" component={LoadableLoginForm} />
<Route path="/signup" component={LoadableSignupForm} />
<Route path="/edit" component={LoadableEditView} />
</div>
</Router>
</React.Fragment>
);
}
render() {
return (
<React.Fragment>
<CssBaseline />
<Router>
<div id="container">
<Route exact path="/" component={ProtectedTodosView} />
<Route path="/login" component={ProtectedLoginForm} />
<Route path="/signup" component={ProtectedSignupForm} />
<Route path="/edit" component={ProtectedEditView} />
</div>
</Router>
</React.Fragment>
);
}
}
App.propTypes = {
loadUser: PropTypes.func.isRequired,
loadUser: PropTypes.func.isRequired,
};

View File

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

View File

@@ -1,60 +1,60 @@
@import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500|Material+Icons');
@import url("https://fonts.googleapis.com/css?family=Roboto:300,400,500|Material+Icons");
body {
background: white;
color: black;
font-family: 'Roboto';
user-select: none;
background: white;
color: black;
font-family: "Roboto";
user-select: none;
}
#root {
margin-top: 5rem;
margin-top: 5rem;
}
#container {
overflow: hidden;
margin: 0 auto;
padding: 0;
max-width: 25rem;
border: 1px solid #dddddd;
border-radius: 7px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
overflow: hidden;
margin: 0 auto;
padding: 0;
max-width: 25rem;
border: 1px solid #dddddd;
border-radius: 7px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
}
@media only screen and (max-width: 600px) {
#root {
margin: 0;
}
#container {
max-width: 100%;
}
#root {
margin: 0;
}
#container {
max-width: 100%;
}
}
#user-header {
display: flex;
height: 2rem;
position: relative;
z-index: 20;
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.1);
justify-content: flex-end;
align-content: center;
display: flex;
height: 2rem;
position: relative;
z-index: 20;
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.1);
justify-content: flex-end;
align-content: center;
}
#user-header button {
box-sizing: border-box;
margin-right: 0.2rem;
height: 100%;
flex-grow: 0;
flex-shrink: 1;
color: #555555;
border: none;
background: none;
transition: 0.1s ease-in-out;
font-size: 0.8rem;
font-weight: 300;
font-family: Roboto;
box-sizing: border-box;
margin-right: 0.2rem;
height: 100%;
flex-grow: 0;
flex-shrink: 1;
color: #555555;
border: none;
background: none;
transition: 0.1s ease-in-out;
font-size: 0.8rem;
font-weight: 300;
font-family: Roboto;
}
#user-header button:hover {
color: #555555;
color: #555555;
}

View File

@@ -1,12 +1,12 @@
import React from 'react';
import UserHeader from './user/UserHeader';
import Lists from './lists/Lists';
import React from "react";
import UserHeader from "./user/UserHeader";
import Lists from "./lists/Lists";
export default function Header() {
return (
<div id="header">
<UserHeader />
<Lists />
</div>
);
return (
<div id="header">
<UserHeader />
<Lists />
</div>
);
}

View File

@@ -1,25 +1,22 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import * as React from "react";
import PropTypes from "prop-types";
import { Redirect } from "react-router-dom";
import { connect } from "react-redux";
export default function OnlyUnauth(WrappedComponent) {
function Component({ loggedIn }) {
return loggedIn ? <Redirect to="/" /> : <WrappedComponent />;
}
function Component({ loggedIn }) {
return loggedIn ? <Redirect to="/" /> : <WrappedComponent />;
}
Component.propTypes = {
loggedIn: PropTypes.bool.isRequired,
};
function mapStateToProps(state) {
return {
loggedIn: state.user.user !== undefined && state.user.user !== null,
Component.propTypes = {
loggedIn: PropTypes.bool.isRequired,
};
}
return connect(
mapStateToProps,
null,
)(Component);
function mapStateToProps(state) {
return {
loggedIn: state.user.user !== undefined && state.user.user !== null,
};
}
return connect(mapStateToProps, null)(Component);
}

View File

@@ -1,25 +1,22 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import * as React from "react";
import PropTypes from "prop-types";
import { Redirect } from "react-router-dom";
import { connect } from "react-redux";
export default function Protected(WrappedComponent) {
function Component({ loggedIn }) {
return loggedIn ? <WrappedComponent /> : <Redirect to="/login" />;
}
function Component({ loggedIn }) {
return loggedIn ? <WrappedComponent /> : <Redirect to="/login" />;
}
Component.propTypes = {
loggedIn: PropTypes.bool.isRequired,
};
function mapStateToProps(state) {
return {
loggedIn: state.user.user !== undefined && state.user.user !== null,
Component.propTypes = {
loggedIn: PropTypes.bool.isRequired,
};
}
return connect(
mapStateToProps,
null,
)(Component);
function mapStateToProps(state) {
return {
loggedIn: state.user.user !== undefined && state.user.user !== null,
};
}
return connect(mapStateToProps, null)(Component);
}

View File

@@ -1,51 +1,48 @@
import { connect } from 'react-redux';
import React from 'react';
import PropTypes from 'prop-types';
import { ButtonBase } from '@material-ui/core';
import setVisibilityFilter from '../../actions/visibilityFilter';
import { connect } from "react-redux";
import React from "react";
import PropTypes from "prop-types";
import { ButtonBase } from "@material-ui/core";
import setVisibilityFilter from "../../actions/visibilityFilter";
function Link({ active, onClick, children }) {
const classes = ['filter'];
if (active) {
classes.push('filter--active');
}
return (
<ButtonBase
style={{
padding: '0 1rem',
color: active ? 'black' : '#444444',
height: '2rem',
}}
className={classes.join(' ')}
onClick={e => {
e.preventDefault();
onClick();
}}
>
{children}
</ButtonBase>
);
const classes = ["filter"];
if (active) {
classes.push("filter--active");
}
return (
<ButtonBase
style={{
padding: "0 1rem",
color: active ? "black" : "#444444",
height: "2rem",
}}
className={classes.join(" ")}
onClick={(e) => {
e.preventDefault();
onClick();
}}
>
{children}
</ButtonBase>
);
}
Link.propTypes = {
active: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
active: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
};
function mapStateToProps(state, ownProps) {
return {
active: ownProps.filter === state.visibilityFilter,
};
return {
active: ownProps.filter === state.visibilityFilter,
};
}
function mapDispatchToProps(dispatch, ownProps) {
return {
onClick: () => dispatch(setVisibilityFilter(ownProps.filter)),
};
return {
onClick: () => dispatch(setVisibilityFilter(ownProps.filter)),
};
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Link);
export default connect(mapStateToProps, mapDispatchToProps)(Link);

View File

@@ -1,17 +1,19 @@
import React from 'react';
import FilterLink from './FilterLink';
import { VisibilityFilters } from '../../actions/defs';
import React from "react";
import FilterLink from "./FilterLink";
import { VisibilityFilters } from "../../actions/defs";
function Filters(styles) {
return (
<div style={styles} id="filters">
<FilterLink filter={VisibilityFilters.SHOW_ALL}>all</FilterLink>
<FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>active</FilterLink>
<FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>
completed
</FilterLink>
</div>
);
return (
<div style={styles} id="filters">
<FilterLink filter={VisibilityFilters.SHOW_ALL}>all</FilterLink>
<FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>
active
</FilterLink>
<FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>
completed
</FilterLink>
</div>
);
}
export default Filters;

View File

@@ -1,117 +1,117 @@
import DeleteIcon from '@material-ui/icons/Delete';
import AddIcon from '@material-ui/icons/Add';
import EditIcon from '@material-ui/icons/Edit';
import BackButton from '@material-ui/icons/ArrowBack';
import { IconButton } from '@material-ui/core';
import React from 'react';
import PropTypes from 'prop-types';
import { Transition, config } from 'react-spring';
import DeleteIcon from "@material-ui/icons/Delete";
import AddIcon from "@material-ui/icons/Add";
import EditIcon from "@material-ui/icons/Edit";
import BackButton from "@material-ui/icons/ArrowBack";
import { IconButton } from "@material-ui/core";
import React from "react";
import PropTypes from "prop-types";
import { Transition, config } from "react-spring";
const button = {
width: 30,
height: 30,
padding: 0,
width: 30,
height: 30,
padding: 0,
};
const icon = {
fontSize: 24,
fontSize: 24,
};
export default function ListActions({
startCreateList,
removeList,
startEditList,
stopCreateList,
stopEditList,
creating,
editing,
list,
startCreateList,
removeList,
startEditList,
stopCreateList,
stopEditList,
creating,
editing,
list,
}) {
function back() {
if (editing) {
stopEditList();
function back() {
if (editing) {
stopEditList();
}
if (creating) {
stopCreateList();
}
}
if (creating) {
stopCreateList();
const actions = [];
if (!creating && !editing) {
actions.push((styles) => (
<IconButton
key="create"
style={{ ...button, ...styles }}
onClick={() => startCreateList()}
>
<AddIcon style={icon} />
</IconButton>
));
}
}
const actions = [];
if (!creating && !editing) {
actions.push(styles => (
<IconButton
key="create"
style={{ ...button, ...styles }}
onClick={() => startCreateList()}
>
<AddIcon style={icon} />
</IconButton>
));
}
if (list && !creating && !editing) {
actions.push(styles => (
<IconButton
key="remove"
style={{ ...button, ...styles }}
onClick={() => removeList()}
>
<DeleteIcon style={icon} />
</IconButton>
));
}
if (list && !creating && !editing) {
actions.push(styles => (
<IconButton
key="edit"
style={{ ...button, ...styles }}
onClick={() => startEditList()}
>
<EditIcon style={icon} />
</IconButton>
));
}
if (creating || editing) {
actions.push(styles => (
<IconButton
key="back"
style={{ ...button, ...styles }}
className="backbutton"
onClick={() => back()}
>
<BackButton style={icon} />
</IconButton>
));
}
return (
<div id="listactions">
<Transition
config={{
...config.stiff,
overshootClamping: true,
restSpeedThreshold: 0.5,
restDisplacementThreshold: 0.5,
}}
keys={actions.map(action => action({}).key)}
from={{ opacity: 0, height: 0, margin: 0, padding: 0 }}
enter={{ opacity: 1, height: 30, margin: 0, padding: 0 }}
leave={{ opacity: 0, height: 0, margin: 0, padding: 0 }}
>
{actions}
</Transition>
</div>
);
if (list && !creating && !editing) {
actions.push((styles) => (
<IconButton
key="remove"
style={{ ...button, ...styles }}
onClick={() => removeList()}
>
<DeleteIcon style={icon} />
</IconButton>
));
}
if (list && !creating && !editing) {
actions.push((styles) => (
<IconButton
key="edit"
style={{ ...button, ...styles }}
onClick={() => startEditList()}
>
<EditIcon style={icon} />
</IconButton>
));
}
if (creating || editing) {
actions.push((styles) => (
<IconButton
key="back"
style={{ ...button, ...styles }}
className="backbutton"
onClick={() => back()}
>
<BackButton style={icon} />
</IconButton>
));
}
return (
<div id="listactions">
<Transition
config={{
...config.stiff,
overshootClamping: true,
restSpeedThreshold: 0.5,
restDisplacementThreshold: 0.5,
}}
keys={actions.map((action) => action({}).key)}
from={{ opacity: 0, height: 0, margin: 0, padding: 0 }}
enter={{ opacity: 1, height: 30, margin: 0, padding: 0 }}
leave={{ opacity: 0, height: 0, margin: 0, padding: 0 }}
>
{actions}
</Transition>
</div>
);
}
ListActions.defaultProps = {
list: '',
list: "",
};
ListActions.propTypes = {
startCreateList: PropTypes.func.isRequired,
removeList: PropTypes.func.isRequired,
startEditList: PropTypes.func.isRequired,
creating: PropTypes.bool.isRequired,
editing: PropTypes.bool.isRequired,
list: PropTypes.string,
stopCreateList: PropTypes.func.isRequired,
stopEditList: PropTypes.func.isRequired,
startCreateList: PropTypes.func.isRequired,
removeList: PropTypes.func.isRequired,
startEditList: PropTypes.func.isRequired,
creating: PropTypes.bool.isRequired,
editing: PropTypes.bool.isRequired,
list: PropTypes.string,
stopCreateList: PropTypes.func.isRequired,
stopEditList: PropTypes.func.isRequired,
};

View File

@@ -1,31 +1,28 @@
import { connect } from 'react-redux';
import ListActions from './ListActions';
import { connect } from "react-redux";
import ListActions from "./ListActions";
import {
startCreateList,
startEditList,
removeList,
stopCreateList,
stopEditList,
} from '../../actions/lists';
startCreateList,
startEditList,
removeList,
stopCreateList,
stopEditList,
} from "../../actions/lists";
function mapStateToProps(state) {
return {
list: state.lists.list,
creating: state.lists.creating,
editing: state.lists.editing,
};
return {
list: state.lists.list,
creating: state.lists.creating,
editing: state.lists.editing,
};
}
function mapDispatchToProps(dispatch) {
return {
startCreateList: () => dispatch(startCreateList()),
startEditList: () => dispatch(startEditList()),
stopCreateList: () => dispatch(stopCreateList()),
stopEditList: () => dispatch(stopEditList()),
removeList: () => dispatch(removeList()),
};
return {
startCreateList: () => dispatch(startCreateList()),
startEditList: () => dispatch(startEditList()),
stopCreateList: () => dispatch(stopCreateList()),
stopEditList: () => dispatch(stopEditList()),
removeList: () => dispatch(removeList()),
};
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(ListActions);
export default connect(mapStateToProps, mapDispatchToProps)(ListActions);

View File

@@ -1,43 +1,43 @@
import React from 'react';
import PropTypes from 'prop-types';
import { IconButton } from '@material-ui/core';
import React from "react";
import PropTypes from "prop-types";
import { IconButton } from "@material-ui/core";
const button = {
width: 36,
height: 36,
width: 36,
height: 36,
};
export default function ListInput({ onClick, children, defaultValue }) {
let input;
return (
<div id="listselector" className="list--input">
<input
ref={node => {
input = node;
}}
defaultValue={defaultValue}
style={{ height: 40 }}
id="input"
type="text"
onKeyPress={e => {
if (e.key === 'Enter') {
onClick(input.value);
}
}}
/>
<IconButton
style={button}
type="submit"
onClick={() => input.value.trim() && onClick(input.value)}
>
{children}
</IconButton>
</div>
);
let input;
return (
<div id="listselector" className="list--input">
<input
ref={(node) => {
input = node;
}}
defaultValue={defaultValue}
style={{ height: 40 }}
id="input"
type="text"
onKeyPress={(e) => {
if (e.key === "Enter") {
onClick(input.value);
}
}}
/>
<IconButton
style={button}
type="submit"
onClick={() => input.value.trim() && onClick(input.value)}
>
{children}
</IconButton>
</div>
);
}
ListInput.propTypes = {
onClick: PropTypes.func.isRequired,
children: PropTypes.element.isRequired,
defaultValue: PropTypes.string,
onClick: PropTypes.func.isRequired,
children: PropTypes.element.isRequired,
defaultValue: PropTypes.string,
};

View File

@@ -1,34 +1,33 @@
import { connect } from 'react-redux';
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from "react-redux";
import React from "react";
import PropTypes from "prop-types";
import ListActionsContainer from './ListActionsContainer';
import SelectorContainer from './SelectorContainer';
import ListActionsContainer from "./ListActionsContainer";
import SelectorContainer from "./SelectorContainer";
function Lists({ userLoaded, listsLoaded }) {
return (
<div id="lists-header">
{userLoaded &&
listsLoaded && (
<div id="lists">
<ListActionsContainer />
<SelectorContainer />
</div>
)}
</div>
);
return (
<div id="lists-header">
{userLoaded && listsLoaded && (
<div id="lists">
<ListActionsContainer />
<SelectorContainer />
</div>
)}
</div>
);
}
Lists.propTypes = {
userLoaded: PropTypes.bool.isRequired,
listsLoaded: PropTypes.bool.isRequired,
userLoaded: PropTypes.bool.isRequired,
listsLoaded: PropTypes.bool.isRequired,
};
function mapStateToProps(state) {
return {
userLoaded: state.user.loaded,
listsLoaded: state.lists.loaded,
};
return {
userLoaded: state.user.loaded,
listsLoaded: state.lists.loaded,
};
}
export default connect(mapStateToProps)(Lists);

View File

@@ -1,53 +1,53 @@
#listselector {
display: flex;
margin-left: 1rem;
overflow: hidden;
align-self: center;
background-color: #fbfbfb;
flex-grow: 1;
margin-top: 1rem;
margin-bottom: 1rem;
display: flex;
margin-left: 1rem;
overflow: hidden;
align-self: center;
background-color: #fbfbfb;
flex-grow: 1;
margin-top: 1rem;
margin-bottom: 1rem;
}
#listselector input {
padding: 0;
color: black;
font-size: 1.5rem;
font-weight: 400;
outline: none;
border: none;
background-color: #fbfbfb;
border-bottom: 1px solid #888888;
width: 80%;
padding: 0;
color: black;
font-size: 1.5rem;
font-weight: 400;
outline: none;
border: none;
background-color: #fbfbfb;
border-bottom: 1px solid #888888;
width: 80%;
}
#listselector button {
align-self: center;
width: 20%;
font-size: 0.9rem;
color: #1b881b;
background: none;
border: none;
margin: 0.1rem 0.3rem;
padding: 0.3rem 0.7em;
align-self: center;
width: 20%;
font-size: 0.9rem;
color: #1b881b;
background: none;
border: none;
margin: 0.1rem 0.3rem;
padding: 0.3rem 0.7em;
}
#listselector select {
max-width: 100%;
color: black;
font-size: 1.5rem;
font-weight: 400;
outline: none;
border: none;
background-color: #fbfbfb;
max-width: 100%;
color: black;
font-size: 1.5rem;
font-weight: 400;
outline: none;
border: none;
background-color: #fbfbfb;
}
#listselector select option {
max-width: 100%;
color: black;
font-size: 1.5rem;
font-weight: 400;
outline: none;
border: none;
background-color: #fbfbfb;
max-width: 100%;
color: black;
font-size: 1.5rem;
font-weight: 400;
outline: none;
border: none;
background-color: #fbfbfb;
}

View File

@@ -1,69 +1,69 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Select, MenuItem } from '@material-ui/core';
import AddIcon from '@material-ui/icons/Add';
import CheckIcon from '@material-ui/icons/Check';
import React from "react";
import PropTypes from "prop-types";
import { Select, MenuItem } from "@material-ui/core";
import AddIcon from "@material-ui/icons/Add";
import CheckIcon from "@material-ui/icons/Check";
import ListInput from './ListInput';
import './Selector.css';
import ListInput from "./ListInput";
import "./Selector.css";
const icon = {
fontSize: 24,
fontSize: 24,
};
export default function Selector({
lists,
list,
onChange,
editing,
creating,
addList,
editList,
lists,
list,
onChange,
editing,
creating,
addList,
editList,
}) {
if (creating) {
return (
<ListInput onClick={addList}>
<AddIcon style={icon} />
</ListInput>
);
}
if (editing) {
return (
<ListInput onClick={editList} defaultValue={lists.lists[list].name}>
<CheckIcon style={icon} />
</ListInput>
);
}
if (list) {
return (
<div id="listselector">
<Select
style={{ fontSize: '1.5rem', width: '100%', height: 40 }}
value={list}
onChange={e => onChange(e.target.value)}
>
{Object.values(lists.lists).map(elem => (
<MenuItem key={elem.id} value={elem.id}>
{elem.name}
</MenuItem>
))}
</Select>
</div>
);
}
return null;
if (creating) {
return (
<ListInput onClick={addList}>
<AddIcon style={icon} />
</ListInput>
);
}
if (editing) {
return (
<ListInput onClick={editList} defaultValue={lists.lists[list].name}>
<CheckIcon style={icon} />
</ListInput>
);
}
if (list) {
return (
<div id="listselector">
<Select
style={{ fontSize: "1.5rem", width: "100%", height: 40 }}
value={list}
onChange={(e) => onChange(e.target.value)}
>
{Object.values(lists.lists).map((elem) => (
<MenuItem key={elem.id} value={elem.id}>
{elem.name}
</MenuItem>
))}
</Select>
</div>
);
}
return null;
}
Selector.defaultProps = {
list: '',
list: "",
};
Selector.propTypes = {
list: PropTypes.string,
editing: PropTypes.bool.isRequired,
creating: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
editList: PropTypes.func.isRequired,
addList: PropTypes.func.isRequired,
lists: PropTypes.object.isRequired,
list: PropTypes.string,
editing: PropTypes.bool.isRequired,
creating: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
editList: PropTypes.func.isRequired,
addList: PropTypes.func.isRequired,
lists: PropTypes.object.isRequired,
};

View File

@@ -1,25 +1,22 @@
import { connect } from 'react-redux';
import Selector from './Selector';
import { changeList, addList, editList } from '../../actions/lists';
import { connect } from "react-redux";
import Selector from "./Selector";
import { changeList, addList, editList } from "../../actions/lists";
function mapStateToProps(state) {
return {
lists: state.lists,
list: state.lists.list,
editing: state.lists.editing,
creating: state.lists.creating,
};
return {
lists: state.lists,
list: state.lists.list,
editing: state.lists.editing,
creating: state.lists.creating,
};
}
function mapDispatchToProps(dispatch) {
return {
onChange: list => dispatch(changeList(list)),
addList: name => dispatch(addList(name)),
editList: name => dispatch(editList(name)),
};
return {
onChange: (list) => dispatch(changeList(list)),
addList: (name) => dispatch(addList(name)),
editList: (name) => dispatch(editList(name)),
};
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Selector);
export default connect(mapStateToProps, mapDispatchToProps)(Selector);

View File

@@ -1,162 +1,168 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { animated } from 'react-spring';
import { ButtonBase } from '@material-ui/core';
import DeleteIcon from '@material-ui/icons/Delete';
import EditIcon from '@material-ui/icons/Edit';
import CheckIcon from '@material-ui/icons/Check';
import * as React from "react";
import PropTypes from "prop-types";
import { animated } from "react-spring";
import { ButtonBase } from "@material-ui/core";
import DeleteIcon from "@material-ui/icons/Delete";
import EditIcon from "@material-ui/icons/Edit";
import CheckIcon from "@material-ui/icons/Check";
const icon = {
fontSize: 24,
padding: 0,
fontSize: 24,
padding: 0,
};
const disabledAction = {
backgroundColor: '#fafafa',
color: '#dddddd',
backgroundColor: "#fafafa",
color: "#dddddd",
};
class Todo extends React.Component {
constructor(props) {
super(props);
this.state = {
hover: false,
};
this.onMouseOver = this.onMouseOver.bind(this);
this.onMouseOut = this.onMouseOut.bind(this);
this.startEdit = this.startEdit.bind(this);
this.stopEdit = this.stopEdit.bind(this);
}
onMouseOver() {
const { state } = this;
this.setState({
...state,
hover: true,
});
}
onMouseOut() {
const { state } = this;
this.setState({
...state,
hover: false,
});
}
startEdit() {
const { state } = this;
this.setState({
...state,
editing: true,
});
}
stopEdit(value) {
const { editTodo } = this.props;
editTodo(value);
const { state } = this;
this.setState({
...state,
editing: false,
hover: false,
});
}
render() {
const deleteClasses = ['delete'];
const editClasses = ['edit'];
const { hover, editing } = this.state;
const { todo, removeTodo, toggleTodo, style } = this.props;
if (!hover) {
deleteClasses.push('disabled');
editClasses.push('disabled');
constructor(props) {
super(props);
this.state = {
hover: false,
};
this.onMouseOver = this.onMouseOver.bind(this);
this.onMouseOut = this.onMouseOut.bind(this);
this.startEdit = this.startEdit.bind(this);
this.stopEdit = this.stopEdit.bind(this);
}
let input;
onMouseOver() {
const { state } = this;
this.setState({
...state,
hover: true,
});
}
const text = editing ? (
<div className="todo">
<textarea
className="todo--input"
defaultValue={todo.text}
ref={node => {
input = node;
}}
/>
</div>
) : (
<ButtonBase
style={{
justifyContent: 'left',
paddingLeft: '1rem',
textDecoration: todo.completed ? 'line-through' : 'none',
color: todo.completed ? '#888888' : 'black',
}}
className="todo"
onClick={() => {
toggleTodo();
}}
>
{todo.text}
</ButtonBase>
);
const ButtonBases = editing
? [
<ButtonBase
key="save"
style={{ backgroundColor: 'lightgreen' }}
className="save"
onClick={() => this.stopEdit(input.value)}
>
<CheckIcon style={icon} />
</ButtonBase>,
]
: [
<ButtonBase
key="remove"
style={hover ? { backgroundColor: 'pink' } : disabledAction}
className={deleteClasses.join(' ')}
onClick={removeTodo}
>
<DeleteIcon style={icon} />
</ButtonBase>,
<ButtonBase
key="edit"
style={hover ? { backgroundColor: 'lightcyan' } : disabledAction}
className={editClasses.join(' ')}
onClick={this.startEdit}
>
<EditIcon style={icon} />
</ButtonBase>,
];
return (
<animated.li
style={{
...style,
borderTop: '1px solid #f0f0f0',
}}
onMouseOver={this.onMouseOver}
onFocus={this.onMouseOver}
onMouseOut={this.onMouseOut}
onBlur={this.onMouseOut}
>
{text}
{ButtonBases}
</animated.li>
);
}
onMouseOut() {
const { state } = this;
this.setState({
...state,
hover: false,
});
}
startEdit() {
const { state } = this;
this.setState({
...state,
editing: true,
});
}
stopEdit(value) {
const { editTodo } = this.props;
editTodo(value);
const { state } = this;
this.setState({
...state,
editing: false,
hover: false,
});
}
render() {
const deleteClasses = ["delete"];
const editClasses = ["edit"];
const { hover, editing } = this.state;
const { todo, removeTodo, toggleTodo, style } = this.props;
if (!hover) {
deleteClasses.push("disabled");
editClasses.push("disabled");
}
let input;
const text = editing ? (
<div className="todo">
<textarea
className="todo--input"
defaultValue={todo.text}
ref={(node) => {
input = node;
}}
/>
</div>
) : (
<ButtonBase
style={{
justifyContent: "left",
paddingLeft: "1rem",
textDecoration: todo.completed ? "line-through" : "none",
color: todo.completed ? "#888888" : "black",
}}
className="todo"
onClick={() => {
toggleTodo();
}}
>
{todo.text}
</ButtonBase>
);
const ButtonBases = editing
? [
<ButtonBase
key="save"
style={{ backgroundColor: "lightgreen" }}
className="save"
onClick={() => this.stopEdit(input.value)}
>
<CheckIcon style={icon} />
</ButtonBase>,
]
: [
<ButtonBase
key="remove"
style={
hover ? { backgroundColor: "pink" } : disabledAction
}
className={deleteClasses.join(" ")}
onClick={removeTodo}
>
<DeleteIcon style={icon} />
</ButtonBase>,
<ButtonBase
key="edit"
style={
hover
? { backgroundColor: "lightcyan" }
: disabledAction
}
className={editClasses.join(" ")}
onClick={this.startEdit}
>
<EditIcon style={icon} />
</ButtonBase>,
];
return (
<animated.li
style={{
...style,
borderTop: "1px solid #f0f0f0",
}}
onMouseOver={this.onMouseOver}
onFocus={this.onMouseOver}
onMouseOut={this.onMouseOut}
onBlur={this.onMouseOut}
>
{text}
{ButtonBases}
</animated.li>
);
}
}
Todo.propTypes = {
todo: PropTypes.shape({
id: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired,
}).isRequired,
removeTodo: PropTypes.func.isRequired,
toggleTodo: PropTypes.func.isRequired,
editTodo: PropTypes.func.isRequired,
style: PropTypes.shape({ height: PropTypes.object.isRequired }).isRequired,
todo: PropTypes.shape({
id: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired,
}).isRequired,
removeTodo: PropTypes.func.isRequired,
toggleTodo: PropTypes.func.isRequired,
editTodo: PropTypes.func.isRequired,
style: PropTypes.shape({ height: PropTypes.object.isRequired }).isRequired,
};
export default Todo;

View File

@@ -1,71 +1,71 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { Transition, config } from 'react-spring';
import * as React from "react";
import PropTypes from "prop-types";
import { Transition, config } from "react-spring";
import Todo from './Todo';
import Todo from "./Todo";
export default function TodosContainer({
todos,
toggleTodo,
removeTodo,
editTodo,
todos,
toggleTodo,
removeTodo,
editTodo,
}) {
return (
<ul id="list">
<Transition
native
config={{
...config.default,
overshootClamping: true,
restSpeedThreshold: 1,
restDisplacementThreshold: 1,
}}
items={todos}
keys={todo => todo.id}
from={{
height: 0,
borderColor: '#f0f0f0',
opacity: 0.7,
}}
enter={{
height: 60,
borderColor: '#f0f0f0',
opacity: 1,
}}
leave={{
height: 0,
borderColor: '#ffffff',
borderWidth: 0,
opacity: 0.3,
padding: 0,
margin: 0,
pointerEvents: 'none',
}}
>
{todos.map(todo => styles => (
<Todo
key={todo.id}
todo={todo}
style={styles}
toggleTodo={() => toggleTodo(todo.id)}
removeTodo={() => removeTodo(todo.id)}
editTodo={text => editTodo(todo.id, text)}
/>
))}
</Transition>
</ul>
);
return (
<ul id="list">
<Transition
native
config={{
...config.default,
overshootClamping: true,
restSpeedThreshold: 1,
restDisplacementThreshold: 1,
}}
items={todos}
keys={(todo) => todo.id}
from={{
height: 0,
borderColor: "#f0f0f0",
opacity: 0.7,
}}
enter={{
height: 60,
borderColor: "#f0f0f0",
opacity: 1,
}}
leave={{
height: 0,
borderColor: "#ffffff",
borderWidth: 0,
opacity: 0.3,
padding: 0,
margin: 0,
pointerEvents: "none",
}}
>
{todos.map((todo) => (styles) => (
<Todo
key={todo.id}
todo={todo}
style={styles}
toggleTodo={() => toggleTodo(todo.id)}
removeTodo={() => removeTodo(todo.id)}
editTodo={(text) => editTodo(todo.id, text)}
/>
))}
</Transition>
</ul>
);
}
TodosContainer.propTypes = {
todos: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired,
}),
).isRequired,
removeTodo: PropTypes.func.isRequired,
toggleTodo: PropTypes.func.isRequired,
editTodo: PropTypes.func.isRequired,
todos: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired,
}),
).isRequired,
removeTodo: PropTypes.func.isRequired,
toggleTodo: PropTypes.func.isRequired,
editTodo: PropTypes.func.isRequired,
};

View File

@@ -1,34 +1,31 @@
import { connect } from 'react-redux';
import TodoList from './TodoList';
import { toggleTodo, removeTodo, editTodo } from '../../actions/todos';
import { connect } from "react-redux";
import TodoList from "./TodoList";
import { toggleTodo, removeTodo, editTodo } from "../../actions/todos";
import getVisibleTodos from './getVisibleTodos';
import getVisibleTodos from "./getVisibleTodos";
function mapStateToProps(state) {
return {
todos: state.lists.list
? getVisibleTodos(
state.lists.lists[state.lists.list].todos.map(
id => state.todos.todos[id],
),
state.visibilityFilter,
)
: [],
dirty: state.todos.dirty,
};
return {
todos: state.lists.list
? getVisibleTodos(
state.lists.lists[state.lists.list].todos.map(
(id) => state.todos.todos[id],
),
state.visibilityFilter,
)
: [],
dirty: state.todos.dirty,
};
}
function mapDispatchToProps(dispatch) {
return {
toggleTodo: id => dispatch(toggleTodo(id)),
removeTodo: id => dispatch(removeTodo(id)),
editTodo: (id, text) => dispatch(editTodo(id, text)),
};
return {
toggleTodo: (id) => dispatch(toggleTodo(id)),
removeTodo: (id) => dispatch(removeTodo(id)),
editTodo: (id, text) => dispatch(editTodo(id, text)),
};
}
const TodosContainer = connect(
mapStateToProps,
mapDispatchToProps,
)(TodoList);
const TodosContainer = connect(mapStateToProps, mapDispatchToProps)(TodoList);
export default TodosContainer;

View File

@@ -1,52 +1,52 @@
import { connect } from 'react-redux';
import { connect } from "react-redux";
import React from 'react';
import PropTypes from 'prop-types';
import { Transition } from 'react-spring';
import React from "react";
import PropTypes from "prop-types";
import { Transition } from "react-spring";
import withRouter from 'react-router-dom/withRouter';
import Input from '../todos/Input';
import TodoListContainer from './TodoListContainer';
import Header from '../Header';
import Filters from '../filters/Filters';
import withRouter from "react-router-dom/withRouter";
import Input from "../todos/Input";
import TodoListContainer from "./TodoListContainer";
import Header from "../Header";
import Filters from "../filters/Filters";
class Todos extends React.PureComponent {
render() {
const { list } = this.props;
return (
<div id="todos">
<Header />
<Transition
from={{ opacity: 0, maxHeight: 0 }}
enter={{ opacity: 1, maxHeight: 38 }}
leave={{ opacity: 0, maxHeight: 0 }}
>
{list && (styles => <Input styles={styles} />)}
</Transition>
<TodoListContainer />
<Transition
from={{ opacity: 0, maxHeight: 0 }}
enter={{ opacity: 1, maxHeight: 32 }}
leave={{ opacity: 0, maxHeight: 0 }}
>
{list && Filters}
</Transition>
</div>
);
}
render() {
const { list } = this.props;
return (
<div id="todos">
<Header />
<Transition
from={{ opacity: 0, maxHeight: 0 }}
enter={{ opacity: 1, maxHeight: 38 }}
leave={{ opacity: 0, maxHeight: 0 }}
>
{list && ((styles) => <Input styles={styles} />)}
</Transition>
<TodoListContainer />
<Transition
from={{ opacity: 0, maxHeight: 0 }}
enter={{ opacity: 1, maxHeight: 32 }}
leave={{ opacity: 0, maxHeight: 0 }}
>
{list && Filters}
</Transition>
</div>
);
}
}
Todos.propTypes = {
list: PropTypes.bool.isRequired,
user: PropTypes.any.isRequired,
history: PropTypes.any.isRequired,
list: PropTypes.bool.isRequired,
user: PropTypes.any.isRequired,
history: PropTypes.any.isRequired,
};
function mapStateToProps(state) {
return {
list: Boolean(state.lists.list),
user: state.user,
};
return {
list: Boolean(state.lists.list),
user: state.user,
};
}
export default withRouter(connect(mapStateToProps)(Todos));

View File

@@ -1,14 +1,18 @@
import { VisibilityFilters } from '../../actions/defs';
import { VisibilityFilters } from "../../actions/defs";
export default function getVisibleTodos(todos, filter) {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return todos.filter(todo => todo);
case VisibilityFilters.SHOW_ACTIVE:
return todos.filter(todo => todo).filter(todo => !todo.completed);
case VisibilityFilters.SHOW_COMPLETED:
return todos.filter(todo => todo).filter(todo => todo.completed);
default:
return todos.filter(todo => todo);
}
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return todos.filter((todo) => todo);
case VisibilityFilters.SHOW_ACTIVE:
return todos
.filter((todo) => todo)
.filter((todo) => !todo.completed);
case VisibilityFilters.SHOW_COMPLETED:
return todos
.filter((todo) => todo)
.filter((todo) => todo.completed);
default:
return todos.filter((todo) => todo);
}
}

View File

@@ -1,60 +1,61 @@
import { connect } from 'react-redux';
import { connect } from "react-redux";
import * as React from 'react';
import PropTypes from 'prop-types';
import { Button } from '@material-ui/core';
import AddIcon from '@material-ui/icons/Add';
import { addTodo } from '../../actions/todos';
import * as React from "react";
import PropTypes from "prop-types";
import { Button } from "@material-ui/core";
import AddIcon from "@material-ui/icons/Add";
import { addTodo } from "../../actions/todos";
function Input({ onClick, styles }) {
let input;
let input;
function submit() {
if (input.value.trim() !== '') {
onClick(input.value);
function submit() {
if (input.value.trim() !== "") {
onClick(input.value);
}
input.value = "";
}
input.value = '';
}
return (
<div style={styles} id="inputs">
<input
aria-label="todo text"
ref={node => {
input = node;
}}
id="input"
type="text"
placeholder="Add something!"
onKeyPress={e => {
if (e.key === 'Enter') {
submit();
}
}}
/>
<Button style={{ borderRadius: 0 }} id="add" onClick={() => submit()}>
<AddIcon />
</Button>
</div>
);
return (
<div style={styles} id="inputs">
<input
aria-label="todo text"
ref={(node) => {
input = node;
}}
id="input"
type="text"
placeholder="Add something!"
onKeyPress={(e) => {
if (e.key === "Enter") {
submit();
}
}}
/>
<Button
style={{ borderRadius: 0 }}
id="add"
onClick={() => submit()}
>
<AddIcon />
</Button>
</div>
);
}
Input.propTypes = {
styles: PropTypes.any.isRequired,
onClick: PropTypes.func.isRequired,
styles: PropTypes.any.isRequired,
onClick: PropTypes.func.isRequired,
};
function mapStateToProps(state, ownProps) {
return { ...ownProps };
return { ...ownProps };
}
function mapDispatchToProps(dispatch) {
return {
onClick: text => dispatch(addTodo(text)),
};
return {
onClick: (text) => dispatch(addTodo(text)),
};
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Input);
export default connect(mapStateToProps, mapDispatchToProps)(Input);

View File

@@ -1,128 +1,123 @@
import React from 'react';
import { Field, reduxForm } from 'redux-form';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import { ButtonBase, Button } from '@material-ui/core';
import React from "react";
import { Field, reduxForm } from "redux-form";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import PropTypes from "prop-types";
import { ButtonBase, Button } from "@material-ui/core";
import InputField from './InputField';
import UserErrors from './UserErrors';
import InputField from "./InputField";
import UserErrors from "./UserErrors";
import './Form.css';
import "./Form.css";
import { edit, resetEdit, deleteUser } from '../../actions/user';
import { edit, resetEdit, deleteUser } from "../../actions/user";
function validate(values) {
const errors = {};
if (values.password !== values.passwordRepeat) {
errors.passwordRepeat = 'Passwords should match';
}
return errors;
const errors = {};
if (values.password !== values.passwordRepeat) {
errors.passwordRepeat = "Passwords should match";
}
return errors;
}
function EditForm({
handleSubmit,
onSubmit,
deleteUser,
user,
history,
reset,
handleSubmit,
onSubmit,
deleteUser,
user,
history,
reset,
}) {
if (user.user && user.editSuccess) {
reset();
history.push('/');
}
return (
<React.Fragment>
<div id="user-header">
<ButtonBase
style={{
marginLeft: '0',
marginRight: 'auto',
padding: '0 0.5rem',
}}
onClick={() => {
history.push('/');
}}
>
todos
</ButtonBase>
</div>
<div id="form">
<form onSubmit={handleSubmit(onSubmit)}>
<UserErrors user={user} />
<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"
/>
<div id="buttons">
<Button onClick={() => deleteUser()}>Delete your account</Button>
<Button
id="submitbutton"
variant="raised"
color="primary"
type="submit"
>
Save
</Button>
</div>
</form>
</div>
</React.Fragment>
);
if (user.user && user.editSuccess) {
reset();
history.push("/");
}
return (
<React.Fragment>
<div id="user-header">
<ButtonBase
style={{
marginLeft: "0",
marginRight: "auto",
padding: "0 0.5rem",
}}
onClick={() => {
history.push("/");
}}
>
todos
</ButtonBase>
</div>
<div id="form">
<form onSubmit={handleSubmit(onSubmit)}>
<UserErrors user={user} />
<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"
/>
<div id="buttons">
<Button onClick={() => deleteUser()}>
Delete your account
</Button>
<Button
id="submitbutton"
variant="raised"
color="primary"
type="submit"
>
Save
</Button>
</div>
</form>
</div>
</React.Fragment>
);
}
EditForm.propTypes = {
handleSubmit: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
history: PropTypes.any.isRequired,
reset: PropTypes.func.isRequired,
deleteUser: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
history: PropTypes.any.isRequired,
reset: PropTypes.func.isRequired,
deleteUser: PropTypes.func.isRequired,
};
function mapStateToProps(state) {
return {
user: state.user,
};
return {
user: state.user,
};
}
function mapDispatchToProps(dispatch) {
return {
reset: () => dispatch(resetEdit()),
deleteUser: () => dispatch(deleteUser()),
onSubmit: ({ username, password }) =>
dispatch(edit({ username, password })),
};
return {
reset: () => dispatch(resetEdit()),
deleteUser: () => dispatch(deleteUser()),
onSubmit: ({ username, password }) =>
dispatch(edit({ username, password })),
};
}
export default reduxForm({
form: 'editForm',
initialValues: {
username: '',
password: '',
passwordRepeat: '',
},
validate,
})(
withRouter(
connect(
mapStateToProps,
mapDispatchToProps,
)(EditForm),
),
);
form: "editForm",
initialValues: {
username: "",
password: "",
passwordRepeat: "",
},
validate,
})(withRouter(connect(mapStateToProps, mapDispatchToProps)(EditForm)));

View File

@@ -1,37 +1,37 @@
#form {
margin: 2rem;
display: flex;
justify-content: center;
margin: 2rem;
display: flex;
justify-content: center;
}
form {
max-width: 80%;
display: flex;
flex-direction: column;
width: 20rem;
max-width: 80%;
display: flex;
flex-direction: column;
width: 20rem;
}
.error {
margin: 1rem;
color: red;
margin: 1rem;
color: red;
}
#googlebutton {
margin: auto;
margin-left: 0rem;
margin: auto;
margin-left: 0rem;
}
#submitbutton {
margin: auto;
margin-right: 0rem;
margin: auto;
margin-right: 0rem;
}
#buttons {
margin-top: 1rem;
display: flex;
justify-content: space-around;
margin-top: 1rem;
display: flex;
justify-content: space-around;
}
#buttons button {
margin: 0 0.5rem;
}
margin: 0 0.5rem;
}

View File

@@ -1,30 +1,30 @@
import React from 'react';
import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import { ButtonBase } from '@material-ui/core';
import React from "react";
import { withRouter } from "react-router-dom";
import PropTypes from "prop-types";
import { ButtonBase } from "@material-ui/core";
function Link({ history, to, text }) {
return (
<ButtonBase
style={{
marginLeft: '0',
marginRight: 'auto',
padding: '0 1rem',
}}
onClick={e => {
e.preventDefault();
history.push(to);
}}
>
{text}
</ButtonBase>
);
return (
<ButtonBase
style={{
marginLeft: "0",
marginRight: "auto",
padding: "0 1rem",
}}
onClick={(e) => {
e.preventDefault();
history.push(to);
}}
>
{text}
</ButtonBase>
);
}
Link.propTypes = {
history: PropTypes.any,
to: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
history: PropTypes.any,
to: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
};
export default withRouter(Link);

View File

@@ -1,35 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
import { TextField } from '@material-ui/core';
import React from "react";
import PropTypes from "prop-types";
import { TextField } from "@material-ui/core";
export default function InputField({
required,
input,
label,
meta: { touched, error },
type,
required,
input,
label,
meta: { touched, error },
type,
}) {
return (
<React.Fragment>
<TextField
label={label}
required={required}
{...input}
type={type}
style={{ marginBottom: '1rem' }}
/>
{touched && error && <span className="error">{error}</span>}
</React.Fragment>
);
return (
<React.Fragment>
<TextField
label={label}
required={required}
{...input}
type={type}
style={{ marginBottom: "1rem" }}
/>
{touched && error && <span className="error">{error}</span>}
</React.Fragment>
);
}
InputField.propTypes = {
required: PropTypes.bool.isRequired,
input: PropTypes.any.isRequired,
label: PropTypes.string.isRequired,
meta: PropTypes.shape({
touched: PropTypes.bool,
error: PropTypes.string,
}).isRequired,
type: PropTypes.string.isRequired,
required: PropTypes.bool.isRequired,
input: PropTypes.any.isRequired,
label: PropTypes.string.isRequired,
meta: PropTypes.shape({
touched: PropTypes.bool,
error: PropTypes.string,
}).isRequired,
type: PropTypes.string.isRequired,
};

View File

@@ -1,125 +1,118 @@
import React from 'react';
import { Field, reduxForm } from 'redux-form';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { ButtonBase, Button } from '@material-ui/core';
import { withRouter } from 'react-router';
import React from "react";
import { Field, reduxForm } from "redux-form";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import { ButtonBase, Button } from "@material-ui/core";
import { withRouter } from "react-router";
import InputField from './InputField';
import UserErrors from './UserErrors';
import InputField from "./InputField";
import UserErrors from "./UserErrors";
import './Form.css';
import "./Form.css";
import { login, reset, loginJWT } from '../../actions/user';
import { login, reset, loginJWT } from "../../actions/user";
class LoginForm extends React.PureComponent {
componentDidMount() {
const { setJWT } = this.props;
const params = new URLSearchParams(new URL(window.location).search);
if (params.has('jwt')) {
const jwt = params.get('jwt');
setJWT(jwt);
componentDidMount() {
const { setJWT } = this.props;
const params = new URLSearchParams(new URL(window.location).search);
if (params.has("jwt")) {
const jwt = params.get("jwt");
setJWT(jwt);
}
}
}
render() {
const { resetUser, history, handleSubmit, user, onLogin } = this.props;
return (
<React.Fragment>
<div id="user-header">
<ButtonBase
style={{
marginRight: '1rem',
padding: '0 0.5rem',
borderRadius: '7px',
}}
onClick={() => {
resetUser();
history.push('/signup');
}}
>
signup
</ButtonBase>
</div>
<div id="form">
<form onSubmit={handleSubmit(onLogin)}>
<UserErrors user={user} />
<Field
label="username"
name="username"
required
component={InputField}
type="text"
/>
<Field
label="password"
name="password"
required
component={InputField}
type="password"
/>
render() {
const { resetUser, history, handleSubmit, user, onLogin } = this.props;
return (
<React.Fragment>
<div id="user-header">
<ButtonBase
style={{
marginRight: "1rem",
padding: "0 0.5rem",
borderRadius: "7px",
}}
onClick={() => {
resetUser();
history.push("/signup");
}}
>
signup
</ButtonBase>
</div>
<div id="form">
<form onSubmit={handleSubmit(onLogin)}>
<UserErrors user={user} />
<Field
label="username"
name="username"
required
component={InputField}
type="text"
/>
<Field
label="password"
name="password"
required
component={InputField}
type="password"
/>
<div id="buttons">
<Button
id="googlebutton"
variant="raised"
onClick={() => {
window.location = '/__/users/login/google/';
}}
>
Google
</Button>
<Button
id="submitbutton"
variant="raised"
color="primary"
type="submit"
>
Login
</Button>
</div>
</form>
</div>
</React.Fragment>
);
}
<div id="buttons">
<Button
id="googlebutton"
variant="raised"
onClick={() => {
window.location = "/__/users/login/google/";
}}
>
Google
</Button>
<Button
id="submitbutton"
variant="raised"
color="primary"
type="submit"
>
Login
</Button>
</div>
</form>
</div>
</React.Fragment>
);
}
}
LoginForm.propTypes = {
handleSubmit: PropTypes.func.isRequired,
onLogin: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
history: PropTypes.any.isRequired,
resetUser: PropTypes.func.isRequired,
setJWT: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired,
onLogin: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
history: PropTypes.any.isRequired,
resetUser: PropTypes.func.isRequired,
setJWT: PropTypes.func.isRequired,
};
function mapStateToProps(state) {
return {
user: state.user,
};
return {
user: state.user,
};
}
function mapDispatchToProps(dispatch) {
return {
resetUser: () => dispatch(reset()),
onLogin: ({ username, password }) =>
dispatch(login({ username, password })),
setJWT: jwt => dispatch(loginJWT(jwt)),
};
return {
resetUser: () => dispatch(reset()),
onLogin: ({ username, password }) =>
dispatch(login({ username, password })),
setJWT: (jwt) => dispatch(loginJWT(jwt)),
};
}
export default reduxForm({
form: 'loginForm',
initialValues: {
username: '',
password: '',
},
})(
withRouter(
connect(
mapStateToProps,
mapDispatchToProps,
)(LoginForm)
),
);
form: "loginForm",
initialValues: {
username: "",
password: "",
},
})(withRouter(connect(mapStateToProps, mapDispatchToProps)(LoginForm)));

View File

@@ -1,40 +1,37 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { ButtonBase } from '@material-ui/core';
import React from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import { ButtonBase } from "@material-ui/core";
import { logout } from '../../actions/user';
import { logout } from "../../actions/user";
function Link({ onClick, children }) {
return (
<ButtonBase
style={{
marginLeft: 'auto',
marginRight: 0,
padding: '0 1rem',
}}
onClick={e => {
e.preventDefault();
onClick();
}}
>
{children}
</ButtonBase>
);
return (
<ButtonBase
style={{
marginLeft: "auto",
marginRight: 0,
padding: "0 1rem",
}}
onClick={(e) => {
e.preventDefault();
onClick();
}}
>
{children}
</ButtonBase>
);
}
Link.propTypes = {
onClick: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
onClick: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
};
function mapDispatchToProps(dispatch) {
return {
onClick: () => dispatch(logout()),
};
return {
onClick: () => dispatch(logout()),
};
}
export default connect(
null,
mapDispatchToProps,
)(Link);
export default connect(null, mapDispatchToProps)(Link);

View File

@@ -1,118 +1,111 @@
import React from 'react';
import { Field, reduxForm } from 'redux-form';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { ButtonBase, Button } from '@material-ui/core';
import { withRouter } from 'react-router';
import React from "react";
import { Field, reduxForm } from "redux-form";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import { ButtonBase, Button } from "@material-ui/core";
import { withRouter } from "react-router";
import InputField from './InputField';
import UserErrors from './UserErrors';
import InputField from "./InputField";
import UserErrors from "./UserErrors";
import './Form.css';
import "./Form.css";
import { signup, reset } from '../../actions/user';
import { signup, reset } from "../../actions/user";
function validate(values) {
const errors = {};
if (values.password !== values.passwordRepeat) {
errors.passwordRepeat = 'Passwords should match';
}
return errors;
const errors = {};
if (values.password !== values.passwordRepeat) {
errors.passwordRepeat = "Passwords should match";
}
return errors;
}
function SignupForm({ handleSubmit, onSignup, user, history, resetUser }) {
return (
<React.Fragment>
<div id="user-header">
<ButtonBase
style={{
marginRight: '1rem',
padding: '0 0.5rem',
borderRadius: '7px',
}}
onClick={() => {
resetUser();
history.push('/login');
}}
>
login
</ButtonBase>
</div>
<div id="form">
<form onSubmit={handleSubmit(onSignup)}>
<UserErrors user={user} />
<Field
label="username"
name="username"
required
component={InputField}
type="text"
/>
<Field
label="password"
name="password"
required
component={InputField}
type="password"
/>
<Field
label="repeat pasword"
name="passwordRepeat"
required
component={InputField}
type="password"
/>
<div id="buttons">
<Button
id="submitbutton"
variant="raised"
color="primary"
type="submit"
>
Signup
</Button>
</div>
</form>
</div>
</React.Fragment>
);
return (
<React.Fragment>
<div id="user-header">
<ButtonBase
style={{
marginRight: "1rem",
padding: "0 0.5rem",
borderRadius: "7px",
}}
onClick={() => {
resetUser();
history.push("/login");
}}
>
login
</ButtonBase>
</div>
<div id="form">
<form onSubmit={handleSubmit(onSignup)}>
<UserErrors user={user} />
<Field
label="username"
name="username"
required
component={InputField}
type="text"
/>
<Field
label="password"
name="password"
required
component={InputField}
type="password"
/>
<Field
label="repeat pasword"
name="passwordRepeat"
required
component={InputField}
type="password"
/>
<div id="buttons">
<Button
id="submitbutton"
variant="raised"
color="primary"
type="submit"
>
Signup
</Button>
</div>
</form>
</div>
</React.Fragment>
);
}
SignupForm.propTypes = {
handleSubmit: PropTypes.func.isRequired,
onSignup: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
history: PropTypes.any.isRequired,
resetUser: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired,
onSignup: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
history: PropTypes.any.isRequired,
resetUser: PropTypes.func.isRequired,
};
function mapStateToProps(state) {
return {
user: state.user,
};
return {
user: state.user,
};
}
function mapDispatchToProps(dispatch) {
return {
resetUser: () => dispatch(reset()),
onSignup: ({ username, password }) =>
dispatch(signup({ username, password })),
};
return {
resetUser: () => dispatch(reset()),
onSignup: ({ username, password }) =>
dispatch(signup({ username, password })),
};
}
export default reduxForm({
form: 'signupForm',
initialValues: {
username: '',
password: '',
passwordRepeat: '',
},
validate,
})(
withRouter(
connect(
mapStateToProps,
mapDispatchToProps,
)(SignupForm)
),
);
form: "signupForm",
initialValues: {
username: "",
password: "",
passwordRepeat: "",
},
validate,
})(withRouter(connect(mapStateToProps, mapDispatchToProps)(SignupForm)));

View File

@@ -1,32 +1,32 @@
import React from 'react';
import React from "react";
function UserErrors({ user }) {
const errors = [];
if (user.errors) {
if (user.errors.name === 'AuthenticationError') {
errors.push(
<div key="wrongauth" className="error">
Wrong username or password
</div>,
);
const errors = [];
if (user.errors) {
if (user.errors.name === "AuthenticationError") {
errors.push(
<div key="wrongauth" className="error">
Wrong username or password
</div>,
);
}
if (user.errors.name === "ValidationError") {
if (user.errors.message.split(" ").includes("unique.")) {
errors.push(
<div key="exists" className="error">
User already exists
</div>,
);
} else {
errors.push(
<div key="invalid" className="error">
Validation error
</div>,
);
}
}
}
if (user.errors.name === 'ValidationError') {
if (user.errors.message.split(' ').includes('unique.')) {
errors.push(
<div key="exists" className="error">
User already exists
</div>,
);
} else {
errors.push(
<div key="invalid" className="error">
Validation error
</div>,
);
}
}
}
return errors || null;
return errors || null;
}
export default UserErrors;

View File

@@ -1,13 +1,13 @@
import React from 'react';
import React from "react";
import LogoutLink from './LogoutLink';
import HeaderLink from './HeaderLink';
import LogoutLink from "./LogoutLink";
import HeaderLink from "./HeaderLink";
export default function UserHeader() {
return (
<div id="user-header">
<HeaderLink to="/edit" text="account"/>
<LogoutLink>logout</LogoutLink>
</div>
);
return (
<div id="user-header">
<HeaderLink to="/edit" text="account" />
<LogoutLink>logout</LogoutLink>
</div>
);
}

View File

@@ -1,38 +1,38 @@
import React from 'react';
import ReactDOM from 'react-dom';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import { applyMiddleware, createStore, compose } from 'redux';
import { offline } from '@redux-offline/redux-offline';
import offlineConfig from '@redux-offline/redux-offline/lib/defaults';
import React from "react";
import ReactDOM from "react-dom";
import thunk from "redux-thunk";
import { Provider } from "react-redux";
import { applyMiddleware, createStore, compose } from "redux";
import { offline } from "@redux-offline/redux-offline";
import offlineConfig from "@redux-offline/redux-offline/lib/defaults";
import AppContainer from './components/AppContainer';
import registerServiceWorker from './registerServiceWorker';
import todoApp from './reducers';
import { setToken } from './actions/util';
import keepSynced from './middleware/keepSynced';
import AppContainer from "./components/AppContainer";
import registerServiceWorker from "./registerServiceWorker";
import todoApp from "./reducers";
import { setToken } from "./actions/util";
import keepSynced from "./middleware/keepSynced";
let store;
const persistCallback = () => {
const state = store.getState();
if (state.user.user) {
setToken(state.user.user.jwt);
}
ReactDOM.render(
<Provider store={store}>
<AppContainer />
</Provider>,
document.getElementById('root'),
);
const state = store.getState();
if (state.user.user) {
setToken(state.user.user.jwt);
}
ReactDOM.render(
<Provider store={store}>
<AppContainer />
</Provider>,
document.getElementById("root"),
);
};
store = createStore(
todoApp,
compose(
offline({ ...offlineConfig, persistCallback }),
applyMiddleware(thunk, keepSynced),
),
todoApp,
compose(
offline({ ...offlineConfig, persistCallback }),
applyMiddleware(thunk, keepSynced),
),
);
registerServiceWorker();

View File

@@ -1,20 +1,20 @@
import { REQUEST_LISTS, INVALIDATE_LISTS } from '../actions/defs';
import { fetchLists } from '../actions/lists';
import { REQUEST_LISTS, INVALIDATE_LISTS } from "../actions/defs";
import { fetchLists } from "../actions/lists";
export default store => next => action => {
next(action);
if (action.type !== REQUEST_LISTS && typeof action !== 'function') {
const state = store.getState();
if (state.user.user) {
const dirtyLists = state.lists.dirty || false;
const dirtyTodos = state.todos.dirty || false;
const fetchingLists = state.lists.fetching || false;
if (
((dirtyLists || dirtyTodos) && !fetchingLists) ||
action.type === INVALIDATE_LISTS
) {
store.dispatch(fetchLists());
}
export default (store) => (next) => (action) => {
next(action);
if (action.type !== REQUEST_LISTS && typeof action !== "function") {
const state = store.getState();
if (state.user.user) {
const dirtyLists = state.lists.dirty || false;
const dirtyTodos = state.todos.dirty || false;
const fetchingLists = state.lists.fetching || false;
if (
((dirtyLists || dirtyTodos) && !fetchingLists) ||
action.type === INVALIDATE_LISTS
) {
store.dispatch(fetchLists());
}
}
}
}
};

View File

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

View File

@@ -1,36 +1,23 @@
import {
CHANGE_LIST,
INVALIDATE_LISTS,
VALIDATE_LISTS,
REQUEST_LISTS,
RECIEVE_LISTS,
ADD_LIST,
REMOVE_LIST,
EDIT_LIST_NAME,
START_CREATE_LIST,
START_EDIT_LIST,
STOP_CREATE_LIST,
STOP_EDIT_LIST,
REMOVE_TODO,
ADD_TODO,
LOGOUT,
} from '../actions/defs';
CHANGE_LIST,
INVALIDATE_LISTS,
VALIDATE_LISTS,
REQUEST_LISTS,
RECIEVE_LISTS,
ADD_LIST,
REMOVE_LIST,
EDIT_LIST_NAME,
START_CREATE_LIST,
START_EDIT_LIST,
STOP_CREATE_LIST,
STOP_EDIT_LIST,
REMOVE_TODO,
ADD_TODO,
LOGOUT,
} from "../actions/defs";
export default function lists(
state = {
dirty: true,
fetching: false,
lists: null,
loaded: false,
creating: false,
list: null,
editing: false,
},
action,
) {
switch (action.type) {
case LOGOUT:
return {
state = {
dirty: true,
fetching: false,
lists: null,
@@ -38,123 +25,141 @@ export default function lists(
creating: false,
list: null,
editing: false,
};
case CHANGE_LIST:
return { ...state, list: action.list };
case RECIEVE_LISTS: {
const newLists = Object.values(action.lists);
let { list } = state;
if (newLists.length !== 0) {
if (!newLists.some(curList => curList.id === list)) {
list = newLists[0].id || null;
},
action,
) {
switch (action.type) {
case LOGOUT:
return {
dirty: true,
fetching: false,
lists: null,
loaded: false,
creating: false,
list: null,
editing: false,
};
case CHANGE_LIST:
return { ...state, list: action.list };
case RECIEVE_LISTS: {
const newLists = Object.values(action.lists);
let { list } = state;
if (newLists.length !== 0) {
if (!newLists.some((curList) => curList.id === list)) {
list = newLists[0].id || null;
}
} else {
list = null;
}
return {
...state,
dirty: false,
loaded: true,
fetching: false,
lists: action.lists,
list,
};
}
} else {
list = null;
}
return {
...state,
dirty: false,
loaded: true,
fetching: false,
lists: action.lists,
list,
};
case START_CREATE_LIST:
return {
...state,
creating: true,
};
case STOP_CREATE_LIST:
return {
...state,
creating: false,
};
case ADD_LIST:
return {
...state,
creating: false,
lists: { ...state.lists, [action.list.id]: action.list },
list: action.list.id,
};
case REMOVE_LIST: {
const newLists = { ...state.lists };
delete newLists[action.list];
const listsObjs = Object.values(newLists);
const list = listsObjs.length
? listsObjs[listsObjs.length - 1].id
: null;
return {
...state,
list,
lists: newLists,
};
}
case START_EDIT_LIST: {
return {
...state,
editing: true,
};
}
case STOP_EDIT_LIST: {
return {
...state,
editing: false,
};
}
case EDIT_LIST_NAME: {
return {
...state,
editing: false,
lists: {
...state.lists,
[action.list]: {
...state.lists[action.list],
name: action.name,
},
},
};
}
case REMOVE_TODO: {
return {
...state,
lists: {
...state.lists,
[state.list]: {
...state.lists[state.list],
todos: state.lists[state.list].todos.filter(
(todo) => todo !== action.id,
),
},
},
};
}
case ADD_TODO: {
return {
...state,
lists: {
...state.lists,
[state.list]: {
...state.lists[state.list],
todos: [
action.todo.id,
...state.lists[state.list].todos,
],
},
},
};
}
case INVALIDATE_LISTS:
return {
...state,
dirty: true,
};
case VALIDATE_LISTS:
return {
...state,
dirty: false,
};
case REQUEST_LISTS:
return {
...state,
fetching: true,
};
default:
return state;
}
case START_CREATE_LIST:
return {
...state,
creating: true,
};
case STOP_CREATE_LIST:
return {
...state,
creating: false,
};
case ADD_LIST:
return {
...state,
creating: false,
lists: { ...state.lists, [action.list.id]: action.list },
list: action.list.id,
};
case REMOVE_LIST: {
const newLists = { ...state.lists };
delete newLists[action.list];
const listsObjs = Object.values(newLists);
const list = listsObjs.length ? listsObjs[listsObjs.length - 1].id : null;
return {
...state,
list,
lists: newLists,
};
}
case START_EDIT_LIST: {
return {
...state,
editing: true,
};
}
case STOP_EDIT_LIST: {
return {
...state,
editing: false,
};
}
case EDIT_LIST_NAME: {
return {
...state,
editing: false,
lists: {
...state.lists,
[action.list]: {
...state.lists[action.list],
name: action.name,
},
},
};
}
case REMOVE_TODO: {
return {
...state,
lists: {
...state.lists,
[state.list]: {
...state.lists[state.list],
todos: state.lists[state.list].todos.filter(
todo => todo !== action.id,
),
},
},
};
}
case ADD_TODO: {
return {
...state,
lists: {
...state.lists,
[state.list]: {
...state.lists[state.list],
todos: [action.todo.id, ...state.lists[state.list].todos],
},
},
};
}
case INVALIDATE_LISTS:
return {
...state,
dirty: true,
};
case VALIDATE_LISTS:
return {
...state,
dirty: false,
};
case REQUEST_LISTS:
return {
...state,
fetching: true,
};
default:
return state;
}
}

View File

@@ -1,99 +1,102 @@
import {
ADD_TODO,
REMOVE_TODO,
TOGGLE_TODO,
RECIEVE_TODOS,
REQUEST_TODOS,
INVALIDATE_TODOS,
VALIDATE_TODOS,
EDIT_TODO,
REMOVE_LIST,
LOGOUT,
} from '../actions/defs';
ADD_TODO,
REMOVE_TODO,
TOGGLE_TODO,
RECIEVE_TODOS,
REQUEST_TODOS,
INVALIDATE_TODOS,
VALIDATE_TODOS,
EDIT_TODO,
REMOVE_LIST,
LOGOUT,
} from "../actions/defs";
export default function todos(
state = {
dirty: true,
fetching: false,
todos: null,
},
action,
) {
switch (action.type) {
case LOGOUT:
return {
state = {
dirty: true,
fetching: false,
todos: null,
};
case RECIEVE_TODOS:
return {
...state,
dirty: false,
fetching: false,
todos: action.todos,
};
case ADD_TODO:
return {
...state,
todos: { [action.todo.id]: action.todo, ...state.todos },
};
case INVALIDATE_TODOS:
return {
...state,
dirty: true,
};
case VALIDATE_TODOS:
return {
...state,
dirty: false,
};
case EDIT_TODO:
return {
...state,
todos: {
...state.todos,
[action.id]: { ...state.todos[action.id], text: action.text },
},
};
case REQUEST_TODOS:
return {
...state,
fetching: true,
};
case REMOVE_TODO: {
const newTodos = { ...state.todos };
delete newTodos[action.id];
return {
...state,
todos: newTodos,
};
}
case REMOVE_LIST: {
const newTodos = { ...state.todos };
Object.keys(newTodos).forEach(todoId => {
if (newTodos[todoId].list === action.list) {
delete newTodos[todoId];
},
action,
) {
switch (action.type) {
case LOGOUT:
return {
dirty: true,
fetching: false,
todos: null,
};
case RECIEVE_TODOS:
return {
...state,
dirty: false,
fetching: false,
todos: action.todos,
};
case ADD_TODO:
return {
...state,
todos: { [action.todo.id]: action.todo, ...state.todos },
};
case INVALIDATE_TODOS:
return {
...state,
dirty: true,
};
case VALIDATE_TODOS:
return {
...state,
dirty: false,
};
case EDIT_TODO:
return {
...state,
todos: {
...state.todos,
[action.id]: {
...state.todos[action.id],
text: action.text,
},
},
};
case REQUEST_TODOS:
return {
...state,
fetching: true,
};
case REMOVE_TODO: {
const newTodos = { ...state.todos };
delete newTodos[action.id];
return {
...state,
todos: newTodos,
};
}
});
return {
...state,
todos: newTodos,
};
case REMOVE_LIST: {
const newTodos = { ...state.todos };
Object.keys(newTodos).forEach((todoId) => {
if (newTodos[todoId].list === action.list) {
delete newTodos[todoId];
}
});
return {
...state,
todos: newTodos,
};
}
case TOGGLE_TODO: {
return {
...state,
todos: {
...state.todos,
[action.id]: {
...state.todos[action.id],
completed: !state.todos[action.id].completed,
},
},
};
}
default:
return state;
}
case TOGGLE_TODO: {
return {
...state,
todos: {
...state.todos,
[action.id]: {
...state.todos[action.id],
completed: !state.todos[action.id].completed,
},
},
};
}
default:
return state;
}
}

View File

@@ -1,90 +1,90 @@
import {
LOGIN_SUCCESS,
LOGIN_FAIL,
START_LOGIN,
LOGOUT,
SIGNUP_FAIL,
SIGNUP_SUCCESS,
VALIDATE_USER,
RESET_USER,
EDIT_SUCCESS,
EDIT_FAIL,
RESET_EDIT,
} from '../actions/defs';
LOGIN_SUCCESS,
LOGIN_FAIL,
START_LOGIN,
LOGOUT,
SIGNUP_FAIL,
SIGNUP_SUCCESS,
VALIDATE_USER,
RESET_USER,
EDIT_SUCCESS,
EDIT_FAIL,
RESET_EDIT,
} from "../actions/defs";
export default function user(
state = {
dirty: true,
fetching: false,
user: null,
loaded: false,
errors: null,
},
action,
state = {
dirty: true,
fetching: false,
user: null,
loaded: false,
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,
errors: null,
dirty: false,
loaded: true,
fetching: false,
};
case EDIT_SUCCESS:
return {
...state,
user: action.user,
editSuccess: true,
};
case SIGNUP_FAIL:
case LOGIN_FAIL:
return {
...state,
user: null,
errors: action.error,
dirty: false,
fetching: false,
loaded: false,
};
case EDIT_FAIL:
return {
...state,
errors: action.error,
editSuccess: false,
};
case RESET_EDIT:
return {
...state,
editSuccess: null,
};
case RESET_USER:
return {
...state,
fetching: false,
loaded: false,
user: null,
errors: null,
};
case LOGOUT:
return {
...state,
loaded: false,
user: null,
};
default:
return state;
}
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,
errors: null,
dirty: false,
loaded: true,
fetching: false,
};
case EDIT_SUCCESS:
return {
...state,
user: action.user,
editSuccess: true,
};
case SIGNUP_FAIL:
case LOGIN_FAIL:
return {
...state,
user: null,
errors: action.error,
dirty: false,
fetching: false,
loaded: false,
};
case EDIT_FAIL:
return {
...state,
errors: action.error,
editSuccess: false,
};
case RESET_EDIT:
return {
...state,
editSuccess: null,
};
case RESET_USER:
return {
...state,
fetching: false,
loaded: false,
user: null,
errors: null,
};
case LOGOUT:
return {
...state,
loaded: false,
user: null,
};
default:
return state;
}
}

View File

@@ -1,12 +1,12 @@
import { VisibilityFilters, SET_VISIBILITY_FILTER } from '../actions/defs';
import { VisibilityFilters, SET_VISIBILITY_FILTER } from "../actions/defs";
const { SHOW_ALL } = VisibilityFilters;
export default function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter;
default:
return state;
}
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter;
default:
return state;
}
}

View File

@@ -9,110 +9,113 @@
// This link also includes instructions on opting out of this behavior.
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 === "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}$/,
),
);
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
// eslint-disable-next-line no-param-reassign
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
navigator.serviceWorker
.register(swUrl)
.then((registration) => {
// eslint-disable-next-line no-param-reassign
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log(
"New content is available; please refresh.",
);
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log("Content is cached for offline use.");
}
}
};
};
})
.catch((error) => {
console.error("Error during service worker registration:", error);
});
}
function checkValidServiceWorker(swUrl) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.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 => {
registration.unregister().then(() => {
window.location.reload();
});
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.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) => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
"No internet connection found. App is running in offline mode.",
);
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.',
);
});
}
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
// 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',
);
// 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",
);
});
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl);
}
});
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl);
}
});
}
}
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister();
});
}
}

View File

@@ -1,5 +1,5 @@
const { createProxyMiddleware } = require('http-proxy-middleware');
const { createProxyMiddleware } = require("http-proxy-middleware");
module.exports = function(app) {
app.use(createProxyMiddleware('/__', { target: 'http://localhost:4000/' }));
module.exports = function (app) {
app.use(createProxyMiddleware("/__", { target: "http://localhost:4000/" }));
};