add, remove lists

This commit is contained in:
2018-05-27 15:07:02 +03:00
parent 3edf267dad
commit 0a0e45e98d
19 changed files with 2735 additions and 3274 deletions

5449
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -105,7 +105,7 @@ li:first-child .item {
box-shadow: inset -3px 0 3px -3px rgba(0, 0, 0, 0.5);
}
.item {
.todo {
box-sizing: border-box;
word-wrap: break-word;
max-width: 100%;

View File

@@ -3,7 +3,7 @@ import 'normalize.css';
import './App.css';
import InputContainer from './containers/InputContainer';
import ItemsContainer from './containers/ItemsContainer';
import TodosContainer from './containers/TodosContainer';
import Header from './components/Header';
export default function App() {
@@ -11,7 +11,7 @@ export default function App() {
<div id="container">
<Header />
<InputContainer />
<ItemsContainer />
<TodosContainer />
</div>
);
}

View File

@@ -1,8 +1,9 @@
const API_ROOT = 'http://localhost:4000';
export const ADD_ITEM = 'ADD_ITEM';
export const REMOVE_ITEM = 'REMOVE_ITEM';
export const TOGGLE_ITEM = 'TOGGLE_ITEM';
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';
@@ -15,8 +16,8 @@ export const VisibilityFilters = {
SHOW_ACTIVE: 'SHOW_ACTIVE',
};
function toggleItemInList(list, id) {
return { type: TOGGLE_ITEM, list, id };
function toggleTodoInList(id) {
return { type: TOGGLE_TODO, id };
}
export function setVisibilityFilter(filter) {
@@ -29,42 +30,45 @@ function requestTodos(list) {
function recieveTodos(list, todos) {
return { type: RECIEVE_TODOS, list, todos };
}
function invalidateTodos(list) {
return { type: INVALIDATE_TODOS, list };
function invalidateTodos() {
return { type: INVALIDATE_TODOS };
}
function validateTodos(list) {
return { type: VALIDATE_TODOS, list };
function validateTodos() {
return { type: VALIDATE_TODOS };
}
function addTodoToList(list, todo) {
return { type: ADD_ITEM, list, todo };
function addTodoToList(todo) {
return { type: ADD_TODO, todo };
}
export function addItem(list, text) {
export function addTodo(text) {
return async (dispatch, getState) => {
dispatch(invalidateTodos(list));
const state = getState();
const response = await fetch(`${API_ROOT}/lists/${state.lists.lists[list].slug}/todos`, {
body: JSON.stringify({ text }),
headers: {
'content-type': 'application/json',
},
method: 'POST',
});
const json = await response.json();
const todo = json.data;
dispatch(addTodoToList(list, todo));
dispatch(validateTodos(list));
const { list } = state.lists;
if (list) {
dispatch(invalidateTodos());
const response = await fetch(`${API_ROOT}/lists/${list}/todos`, {
body: JSON.stringify({ text }),
headers: {
'content-type': 'application/json',
},
method: 'POST',
});
const json = await response.json();
const todo = json.data;
dispatch(addTodoToList(todo));
dispatch(validateTodos());
}
};
}
function removeTodoFromList(list, id) {
return { type: REMOVE_ITEM, list, id };
function removeTodoFromList(id) {
return { type: REMOVE_TODO, id };
}
export function removeItem(list, id) {
export function removeTodo(id) {
return async (dispatch) => {
dispatch(invalidateTodos(list));
dispatch(invalidateTodos());
const response = await fetch(`${API_ROOT}/todos/${id}`, {
headers: {
'content-type': 'application/json',
@@ -73,17 +77,17 @@ export function removeItem(list, id) {
});
const json = await response.json();
if (json.success) {
dispatch(removeTodoFromList(list, id));
dispatch(removeTodoFromList(id));
}
dispatch(validateTodos(list));
dispatch(validateTodos());
};
}
export function toggleItem(list, id) {
export function toggleTodo(id) {
return async (dispatch, getState) => {
dispatch(invalidateTodos(list));
dispatch(invalidateTodos());
const state = getState();
const listObj = state.lists.lists[list];
const listObj = state.lists.lists[state.lists.list];
const todoObj = listObj.todos.find(todo => todo.id === id);
const completed = !todoObj.completed;
const response = await fetch(`${API_ROOT}/todos/${id}`, {
@@ -95,16 +99,39 @@ export function toggleItem(list, id) {
});
const json = await response.json();
if (json.success) {
dispatch(toggleItemInList(list, id));
dispatch(toggleTodoInList(id));
}
dispatch(validateTodos(list));
dispatch(validateTodos());
};
}
function editTodoInList(id, todo) {
return { type: EDIT_TODO, id, todo };
}
export function editTodo(id, text) {
return async (dispatch) => {
dispatch(invalidateTodos());
const response = await fetch(`${API_ROOT}/todos/${id}`, {
body: JSON.stringify({ text }),
headers: {
'content-type': 'application/json',
},
method: 'PATCH',
});
const json = await response.json();
if (json.success) {
const todo = json.data;
dispatch(editTodoInList(id, todo));
}
dispatch(validateTodos());
};
}
export function fetchTodos(list) {
return async (dispatch) => {
dispatch(requestTodos(list));
const response = await fetch(`${API_ROOT}/todos`);
const response = await fetch(`${API_ROOT}/lists/${list.id}/todos`);
const json = await response.json();
const todos = json.data;
dispatch(recieveTodos(list, todos));
@@ -113,6 +140,7 @@ export function fetchTodos(list) {
export const ADD_LIST = 'ADD_LIST';
export const REMOVE_LIST = 'REMOVE_LIST';
export const EDIT_LIST = 'EDIT_LIST';
export const RECIEVE_LISTS = 'RECIEVE_LISTS';
export const REQUEST_LISTS = 'REQUEST_LISTS';
export const INVALIDATE_LISTS = 'INVALIDATE_LISTS';
@@ -125,6 +153,7 @@ function requestLists() {
function recieveLists(lists) {
return { type: RECIEVE_LISTS, lists };
}
function invalidateLists() {
return { type: INVALIDATE_LISTS };
}
@@ -135,11 +164,83 @@ export function changeList(list) {
return { type: CHANGE_LIST, list };
}
function addListToState(list) {
return { type: ADD_LIST, list };
}
export function addList(name) {
return async (dispatch) => {
dispatch(invalidateLists());
const response = await fetch(`${API_ROOT}/lists`, {
body: JSON.stringify({ name }),
headers: {
'content-type': 'application/json',
},
method: 'POST',
});
const json = await response.json();
const list = json.data;
dispatch(addListToState(list));
dispatch(changeList(list.id));
dispatch(validateLists());
};
}
function removeListFromState(id) {
return { type: REMOVE_LIST, id };
}
export function removeList(id) {
return async (dispatch, getState) => {
dispatch(invalidateLists());
const response = await fetch(`${API_ROOT}/lists/${id}`, {
headers: {
'content-type': 'application/json',
},
method: 'DELETE',
});
const json = await response.json();
if (json.success) {
dispatch(removeListFromState(id));
const state = getState();
const list = state.lists.lists[Object.keys(state.lists.lists)[0]]
? state.lists.lists[Object.keys(state.lists.lists)[0]].id
: '';
dispatch(changeList(list));
}
dispatch(validateLists());
};
}
function editListInState(id, list) {
return { type: EDIT_LIST, id, list };
}
export function editList(id, name) {
return async (dispatch) => {
dispatch(invalidateLists());
const response = await fetch(`${API_ROOT}/lists/${id}`, {
body: JSON.stringify({ name }),
headers: {
'content-type': 'application/json',
},
method: 'PATCH',
});
const json = await response.json();
if (json.success) {
const list = json.data;
dispatch(editListInState(id, list));
}
dispatch(validateLists());
};
}
export function fetchLists() {
return async (dispatch) => {
dispatch(requestLists());
try {
const listsJson = localStorage.getItem('lists');
const listsJson = localStorage.getTodo('lists');
const listsObj = JSON.parse(listsJson);
dispatch(recieveLists(listsObj));
dispatch(changeList(listsObj[Object.keys(listsObj)[0]].id));
@@ -155,8 +256,11 @@ export function fetchLists() {
newObj[list.id] = list;
return newObj;
}, {});
dispatch(recieveLists(listsObj));
dispatch(changeList(listsObj[Object.keys(listsObj)[0]].id));
if (lists.length !== 0) {
dispatch(changeList(listsObj[Object.keys(listsObj)[0]].id));
}
localStorage.setItem('lists', JSON.stringify(listsObj));
};
}

View File

@@ -1,13 +1,13 @@
import React from 'react';
import FilterLink from '../containers/FilterLink';
import ListSelector from '../containers/ListSelector';
import { VisibilityFilters } from '../actions';
import ListsContainer from '../containers/ListsContainer';
export default function Header() {
return (
<div className="header">
<div id="listselector">
<ListSelector />
<ListsContainer />
</div>
<div className="filters">
<FilterLink filter={VisibilityFilters.SHOW_ALL}>all</FilterLink>

View File

@@ -0,0 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
export default function ListActions({ addList, removeList, list }) {
return (
<div>
<button onClick={() => addList(prompt('List name?'))}>Add</button>
<button onClick={() => removeList(list)}>Remove</button>
</div>
);
}
ListActions.defaultProps = {
list: '',
};
ListActions.propTypes = {
addList: PropTypes.func.isRequired,
removeList: PropTypes.func.isRequired,
list: PropTypes.string,
};

30
src/components/Lists.js Normal file
View File

@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import Selector from './Selector';
import ListActions from './ListActions';
export default function Lists({
list, lists, onChange, addList, removeList,
}) {
const selectorProps = { list, lists, onChange };
const actionsProps = { addList, removeList, list };
return (
<div>
<Selector {...selectorProps} /> <ListActions {...actionsProps} />
</div>
);
}
Lists.defaultProps = {
list: '',
};
Lists.propTypes = {
lists: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
})).isRequired,
list: PropTypes.string,
onChange: PropTypes.func.isRequired,
addList: PropTypes.func.isRequired,
removeList: PropTypes.func.isRequired,
};

View File

@@ -7,20 +7,24 @@ export default function Selector(props) {
{list.name}
</option>
));
console.log(props.list);
return (
<div>
<select defaultValue={props.list} onChange={e => props.onChange(e.target.value)}>
<select value={props.list} onChange={e => props.onChange(e.target.value)}>
{lists}
</select>
</div>
);
}
Selector.defaultProps = {
list: '',
};
Selector.propTypes = {
lists: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
slug: PropTypes.string.isRequired,
})).isRequired,
list: PropTypes.string.isRequired,
list: PropTypes.string,
onChange: PropTypes.func.isRequired,
};

View File

@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as React from 'react';
import PropTypes from 'prop-types';
class Item extends React.Component {
class Todo extends React.Component {
constructor(props) {
super(props);
this.state = {
@@ -29,9 +29,9 @@ class Item extends React.Component {
deleteClasses.push('disabled');
}
const itemClasses = ['item'];
if (this.props.item.completed) {
itemClasses.push('done');
const todoClasses = ['todo'];
if (this.props.todo.completed) {
todoClasses.push('done');
}
return (
@@ -44,16 +44,16 @@ class Item extends React.Component {
<div className={deleteClasses.join(' ')} onClick={this.props.handleDelete}>
<FontAwesomeIcon icon={faTrash} />
</div>
<div className={itemClasses.join(' ')} onClick={this.props.onClick}>
{this.props.item.text}
<div className={todoClasses.join(' ')} onClick={this.props.onClick}>
{this.props.todo.text}
</div>
</li>
);
}
}
Item.propTypes = {
item: PropTypes.shape({
Todo.propTypes = {
todo: PropTypes.shape({
id: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired,
@@ -62,4 +62,4 @@ Item.propTypes = {
onClick: PropTypes.func.isRequired,
};
export default Item;
export default Todo;

View File

@@ -1,26 +1,26 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import Item from './Item';
import Todo from './Todo';
export default function ItemsContainer(props) {
const items = props.items.map(item => (
<Item
key={item.id}
item={item}
onClick={() => props.onItemClick(item.list, item.id)}
handleDelete={() => props.handleDelete(item.list, item.id)}
export default function TodosContainer(props) {
const todos = props.todos.map(todo => (
<Todo
key={todo.id}
todo={todo}
onClick={() => props.onTodoClick(todo.id)}
handleDelete={() => props.handleDelete(todo.id)}
/>
));
return <ul id="list">{items}</ul>;
return <ul id="list">{todos}</ul>;
}
ItemsContainer.propTypes = {
items: PropTypes.arrayOf(PropTypes.shape({
TodosContainer.propTypes = {
todos: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired,
})).isRequired,
handleDelete: PropTypes.func.isRequired,
onItemClick: PropTypes.func.isRequired,
onTodoClick: PropTypes.func.isRequired,
};

View File

@@ -3,49 +3,35 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Input from '../components/Input';
import { addItem, VisibilityFilters } from '../actions';
import { addTodo } from '../actions';
import getVisibleTodos from './getVisibleTodos';
const InputContainer = props => (
<Input
inputBottomBorder={props.inputBottomBorder}
onClick={text => props.dispatch(addItem(props.list, text))}
onClick={text => props.dispatch(addTodo(text))}
/>
);
function getVisibleItems(items, filter) {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return items;
case VisibilityFilters.SHOW_ACTIVE:
return items.filter(item => !item.completed);
case VisibilityFilters.SHOW_COMPLETED:
return items.filter(item => item.completed);
default:
return items;
}
}
InputContainer.propTypes = {
list: PropTypes.string.isRequired,
inputBottomBorder: PropTypes.bool.isRequired,
dispatch: PropTypes.func.isRequired,
};
function mapStateToProps(state) {
const { list } = state.lists;
if (!list) {
try {
const listTodos = state.lists.lists[list].todos;
return {
list,
inputBottomBorder: getVisibleTodos(listTodos, state.visibilityFilter).length !== 0,
};
} catch (e) {
return {
list,
inputBottomBorder: false,
};
}
const listItems = state.lists.lists[list].todos;
if (!listItems) {
return { list, inputBottomBorder: false };
}
return {
list,
inputBottomBorder: getVisibleItems(listItems, state.visibilityFilter).length !== 0,
};
}
const InputContainerConnected = connect(mapStateToProps)(InputContainer);

View File

@@ -1,49 +0,0 @@
import { connect } from 'react-redux';
import TodoList from '../components/TodoList';
import { toggleItem, removeItem, VisibilityFilters } from '../actions';
function getVisibleItems(items, filter) {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return items;
case VisibilityFilters.SHOW_ACTIVE:
return items.filter(item => !item.completed);
case VisibilityFilters.SHOW_COMPLETED:
return items.filter(item => item.completed);
default:
return items;
}
}
function mapStateToProps(state) {
const stub = {
list: '',
items: [],
dirty: true,
};
const { list } = state.lists;
const listObj = state.lists.lists[list];
if (!list) {
return stub;
}
const listItems = state.lists.lists[list].todos;
if (!listItems) {
return stub;
}
return {
list,
items: getVisibleItems(listItems, state.visibilityFilter),
dirty: listObj.dirty,
};
}
function mapDispatchToProps(dispatch) {
return {
onItemClick: (list, id) => dispatch(toggleItem(list, id)),
handleDelete: (list, id) => dispatch(removeItem(list, id)),
};
}
const ItemsContainer = connect(mapStateToProps, mapDispatchToProps)(TodoList);
export default ItemsContainer;

View File

@@ -1,16 +1,8 @@
import { connect } from 'react-redux';
import Selector from '../components/Selector';
import { changeList } from '../actions';
import Lists from '../components/Lists';
import { changeList, removeList, addList } from '../actions';
function mapStateToProps(state) {
const { list } = state.lists;
if (!list) {
return {
lists: [],
list: '',
dirty: state.lists.dirty,
};
}
return {
lists: Object.values(state.lists.lists),
list: state.lists.list,
@@ -21,7 +13,9 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
onChange: list => dispatch(changeList(list)),
addList: name => dispatch(addList(name)),
removeList: id => dispatch(removeList(id)),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Selector);
export default connect(mapStateToProps, mapDispatchToProps)(Lists);

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
import { combineReducers } from "redux";
import { combineReducers } from 'redux';
import lists from "./lists";
import visibilityFilter from "./visibilityFilter";
import lists from './lists';
import visibilityFilter from './visibilityFilter';
const todoApp = combineReducers({
lists,
visibilityFilter
visibilityFilter,
});
export default todoApp;

View File

@@ -1,62 +1,58 @@
import {
ADD_ITEM,
REMOVE_ITEM,
TOGGLE_ITEM,
ADD_TODO,
REMOVE_TODO,
TOGGLE_TODO,
RECIEVE_TODOS,
REQUEST_TODOS,
INVALIDATE_TODOS,
VALIDATE_TODOS
} from "../actions";
VALIDATE_TODOS,
EDIT_TODO,
} from '../actions';
export default function items(
state = { dirty: true, fetching: false, todos: [] },
action
) {
export default function todos(state = { dirty: true, fetching: false, todos: [] }, action) {
switch (action.type) {
case RECIEVE_TODOS:
return {
...state,
dirty: false,
fetching: false,
todos: action.todos
todos: action.todos,
};
case ADD_ITEM:
case ADD_TODO:
return {
...state,
todos: [...state.todos, action.todo]
todos: [...state.todos, action.todo],
};
case INVALIDATE_TODOS:
return {
...state,
dirty: true
dirty: true,
};
case VALIDATE_TODOS:
return {
...state,
dirty: false
dirty: false,
};
case EDIT_TODO:
return {
...state,
todos: state.todos.map(todo => (todo.id === action.id ? action.todo : todo)),
};
case REQUEST_TODOS:
return {
...state,
fetching: true
fetching: true,
};
case REMOVE_ITEM:
case REMOVE_TODO:
return {
...state,
todos: state.todos.filter(item => item.id !== action.id)
todos: state.todos.filter(todo => todo.id !== action.id),
};
case TOGGLE_TODO: {
return {
...state,
todos: state.todos.map(todo => (todo.id === action.id ? { ...todo, completed: !todo.completed } : todo)),
};
case TOGGLE_ITEM: {
const itemsArray = [...state.todos];
itemsArray.some((item, i) => {
if (item.id === action.id) {
const newItem = { ...item };
newItem.completed = !item.completed;
itemsArray[i] = newItem;
return true;
}
return false;
});
return { ...state, todos: itemsArray };
}
default:
return state;

View File

@@ -4,22 +4,25 @@ import {
VALIDATE_LISTS,
REQUEST_LISTS,
RECIEVE_TODOS,
ADD_ITEM,
ADD_TODO,
INVALIDATE_TODOS,
VALIDATE_TODOS,
REQUEST_TODOS,
REMOVE_ITEM,
TOGGLE_ITEM,
REMOVE_TODO,
TOGGLE_TODO,
RECIEVE_LISTS,
ADD_LIST,
REMOVE_LIST
} from "../actions";
REMOVE_LIST,
EDIT_LIST,
} from '../actions';
import list from "./list";
import list from './list';
export default function lists(
state = { dirty: true, fetching: false, lists: {} },
action
state = {
dirty: true, fetching: false, lists: {}, list: '',
},
action,
) {
switch (action.type) {
case CHANGE_LIST:
@@ -29,48 +32,55 @@ export default function lists(
...state,
dirty: false,
fetching: false,
lists: action.lists
lists: action.lists,
};
case ADD_LIST:
return {
...state,
lists: { ...state.lists, [action.list.id]: action.list }
lists: { ...state.lists, [action.list.id]: action.list },
};
case REMOVE_LIST:
case REMOVE_LIST: {
const newLists = { ...state.lists };
delete newLists[action.list];
delete newLists[action.id];
return {
...state,
lists: newLists
lists: newLists,
};
}
case EDIT_LIST: {
return {
...state,
lists: { ...state.lists, [action.id]: action.list },
};
}
case INVALIDATE_LISTS:
return {
...state,
dirty: true
dirty: true,
};
case VALIDATE_LISTS:
return {
...state,
dirty: false
dirty: false,
};
case REQUEST_LISTS:
return {
...state,
fetching: true
fetching: true,
};
case RECIEVE_TODOS:
case ADD_ITEM:
case ADD_TODO:
case INVALIDATE_TODOS:
case VALIDATE_TODOS:
case REQUEST_TODOS:
case REMOVE_ITEM:
case TOGGLE_ITEM:
case REMOVE_TODO:
case TOGGLE_TODO:
return {
...state,
lists: {
...state.lists,
[action.list]: list(state.lists[action.list], action)
}
[state.list]: list(state.lists[state.list], action),
},
};
default:
return state;

View File

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