diff --git a/package.json b/package.json index f45faa9..fe2fd35 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "goals-todos-api": "^1.0.0", "react": "^16.10.1", "react-dom": "^16.10.1", "react-redux": "^7.1.1", diff --git a/src/actions/goals.js b/src/actions/goals.js new file mode 100644 index 0000000..7d9ce81 --- /dev/null +++ b/src/actions/goals.js @@ -0,0 +1,43 @@ +import API from 'goals-todos-api' + +export const ADD_GOAL = 'ADD_GOAL' +export const REMOVE_GOAL = 'REMOVE_GOAL' + +function addGoal(goal) { + return { + type: ADD_GOAL, + goal + } +} + +function removeGoal(id) { + return { + type: REMOVE_GOAL, + id + } +} + +export function handleAddGoal(name, callback) { + return (dispatch) => { + return API.saveGoal(name) + .then((goal) => { + dispatch(addGoal(goal)) + callback() + }) + .catch(() => { + alert('An error occurred. Try again.') + }) + } +} + +export function handleDeleteGoal(goal) { + return (dispatch) => { + dispatch(removeGoal(goal.id)) + return API.deleteGoal(goal.id) + .catch(() => { + dispatch(addGoal(goal)) + alert('An error occurred. Try again.') + }) + } +} + diff --git a/src/actions/shared.js b/src/actions/shared.js new file mode 100644 index 0000000..abe46e0 --- /dev/null +++ b/src/actions/shared.js @@ -0,0 +1,22 @@ +import API from 'goals-todos-api' + +export const RECEIVE_DATA = 'RECEIVE_DATA' + +function receiveDataAction(todos, goals) { + return { + type: RECEIVE_DATA, + todos, + goals + } +} + +export function handleInitialData() { + return (dispatch) => { + return Promise.all([ + API.fetchTodos(), + API.fetchGoals() + ]).then(([todos, goals]) => { + dispatch(receiveDataAction(todos, goals)) + }) + } +} \ No newline at end of file diff --git a/src/actions/todos.js b/src/actions/todos.js new file mode 100644 index 0000000..75cc9ff --- /dev/null +++ b/src/actions/todos.js @@ -0,0 +1,61 @@ +import API from 'goals-todos-api' + +export const ADD_TODO = 'ADD_TODO' +export const REMOVE_TODO = 'REMOVE_TODO' +export const TOGGLE_TODO = 'TOGGLE_TODO' + +function addTodo(todo) { + return { + type: ADD_TODO, + todo + } +} + +function removeTodo(id) { + return { + type: REMOVE_TODO, + id + } +} + +function toggleTodo(id) { + return { + type: TOGGLE_TODO, + id + } +} + +export function handleDeleteTodo(todo) { + return (dispatch) => { + dispatch(removeTodo(todo.id)) + return API.deleteTodo(todo.id) + .catch(() => { + dispatch(addTodo(todo)) + alert('An error occurred. Try again.') + }) + } +} + +export function handleAddTodo(name, callback) { + return (dispatch) => { + return API.saveTodo(name) + .then((todo) => { + dispatch(addTodo(todo)) + callback() + }) + .catch(() => { + alert('An error occurred. Try again.') + }) + } +} + +export function handleToggle(id) { + return (dispatch) => { + dispatch(toggleTodo(id)) + return API.saveTodoToggle(id) + .catch(() => { + dispatch(toggleTodo(id)) + alert('An error occurred. Try again.') + }) + } +} \ No newline at end of file diff --git a/src/components/App.js b/src/components/App.js index 59dd663..19b0c20 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -1,11 +1,33 @@ -import React from 'react'; - -function App() { - return ( -
- Hello world -
- ); +import React from 'react' +import ConnectedTodos from './Todos' +import ConnectedGoals from './Goals' +import { connect } from 'react-redux' +import { + handleInitialData +} from '../actions/shared' + +class App extends React.Component { + componentDidMount() { + const { dispatch } = this.props + + dispatch(handleInitialData()) + } + render() { + const { loading } = this.props + + if (loading === true) { + return

Loading

+ } + + return ( +
+ + +
+ ) + } } -export default App; +export default connect((state) => ({ + loading: state.loading +}))(App) diff --git a/src/components/Goals.js b/src/components/Goals.js new file mode 100644 index 0000000..8862520 --- /dev/null +++ b/src/components/Goals.js @@ -0,0 +1,46 @@ +import React from 'react' +import { connect } from 'react-redux' +import List from './List' +import { + handleAddGoal, + handleDeleteGoal +} from '../actions/goals' + +class Goals extends React.Component { + addItem = (event) => { + event.preventDefault() + + this.props.dispatch(handleAddGoal( + this.input.value, + () => this.input.value = '' + )) + } + + removeItem = (goal) => { + this.props.dispatch(handleDeleteGoal(goal)) + } + + render() { + return ( +
+

Goals

+ this.input = input} + /> + + +
+ ) + } +} + +export default connect((state) => ({ + goals: state.goals +}))(Goals) \ No newline at end of file diff --git a/src/components/List.js b/src/components/List.js new file mode 100644 index 0000000..1a846e8 --- /dev/null +++ b/src/components/List.js @@ -0,0 +1,18 @@ +import React from 'react' + +export default function List(props) { + return ( + + ) +} \ No newline at end of file diff --git a/src/components/Todos.js b/src/components/Todos.js new file mode 100644 index 0000000..a1780cc --- /dev/null +++ b/src/components/Todos.js @@ -0,0 +1,53 @@ +import React from 'react' +import { connect } from 'react-redux' +import List from './List' +import { + handleAddTodo, + handleDeleteTodo, + handleToggle +} from '../actions/todos' + +class Todos extends React.Component { + addItem = (event) => { + event.preventDefault() + + this.props.dispatch(handleAddTodo( + this.input.value, + () => this.input.value = '' + )) + } + + removeItem = (todo) => { + this.props.dispatch(handleDeleteTodo(todo)) + } + + toggleItem = (id) => { + this.props.dispatch(handleToggle(id)) + } + + render() { + const { todos } = this.props + + return ( +
+

Todo List

+ this.input = input} /> + + +
+ ) + } +} + +export default connect((state) => ({ + todos: state.todos +}))(Todos) \ No newline at end of file diff --git a/src/index.js b/src/index.js index 4c4b3f6..7f64681 100644 --- a/src/index.js +++ b/src/index.js @@ -2,5 +2,16 @@ import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './components/App'; +import reducer from './reducers' +import middleware from './middleware' +import { Provider } from 'react-redux' +import { createStore } from 'redux' -ReactDOM.render(, document.getElementById('root')); +const store = createStore(reducer, middleware) + +ReactDOM.render( + + + + , + document.getElementById('root')); diff --git a/src/middleware/checker.js b/src/middleware/checker.js new file mode 100644 index 0000000..771a7f8 --- /dev/null +++ b/src/middleware/checker.js @@ -0,0 +1,21 @@ +import { ADD_TODO } from '../actions/todos' +import { ADD_GOAL } from '../actions/goals' + +const checker = (store) => (next) => (action) => { + if ( + action.type === ADD_TODO && + action.todo.name.toLowerCase().indexOf('bitcoin') !== -1 + ) { + return alert("Nope. That's a bad idea.") + } + if ( + action.type === ADD_GOAL && + action.goal.name.toLowerCase().indexOf('bitcoin') !== -1 + ) { + return alert("Nope. That's a bad idea.") + } + + return next(action) +} + +export default checker \ No newline at end of file diff --git a/src/middleware/index.js b/src/middleware/index.js new file mode 100644 index 0000000..35b4dc1 --- /dev/null +++ b/src/middleware/index.js @@ -0,0 +1,10 @@ +import checker from './checker' +import logger from './logger' +import thunk from 'redux-thunk' +import { applyMiddleware } from 'redux' + +export default applyMiddleware( + thunk, + checker, + logger +) \ No newline at end of file diff --git a/src/middleware/logger.js b/src/middleware/logger.js new file mode 100644 index 0000000..145de59 --- /dev/null +++ b/src/middleware/logger.js @@ -0,0 +1,10 @@ +const logger = (store) => (next) => (action) => { + console.group(action.type) + console.log('The action: ', action) + const result = next(action) + console.log('The new state: ', store.getState()) + console.groupEnd() + return result +} + +export default logger \ No newline at end of file diff --git a/src/reducers/goals.js b/src/reducers/goals.js new file mode 100644 index 0000000..e996a23 --- /dev/null +++ b/src/reducers/goals.js @@ -0,0 +1,16 @@ +import { ADD_GOAL, REMOVE_GOAL } from '../actions/goals' + +import { RECEIVE_DATA } from '../actions/shared' + +export default function goals(state = [], action) { + switch (action.type) { + case ADD_GOAL: + return state.concat([action.goal]) + case REMOVE_GOAL: + return state.filter((goal) => goal.id !== action.id) + case RECEIVE_DATA: + return action.goals + default: + return state + } +} \ No newline at end of file diff --git a/src/reducers/index.js b/src/reducers/index.js new file mode 100644 index 0000000..231c82e --- /dev/null +++ b/src/reducers/index.js @@ -0,0 +1,11 @@ +import { combineReducers } from 'redux' + +import todos from './todos' +import goals from './goals' +import loading from './loading' + +export default combineReducers({ + todos, + goals, + loading +}) \ No newline at end of file diff --git a/src/reducers/loading.js b/src/reducers/loading.js new file mode 100644 index 0000000..d8c5cac --- /dev/null +++ b/src/reducers/loading.js @@ -0,0 +1,10 @@ +import { RECEIVE_DATA } from '../actions/shared' + +export default function loading(state = true, action) { + switch (action.type) { + case RECEIVE_DATA: + return false + default: + return state + } +} \ No newline at end of file diff --git a/src/reducers/todos.js b/src/reducers/todos.js new file mode 100644 index 0000000..ee748f9 --- /dev/null +++ b/src/reducers/todos.js @@ -0,0 +1,20 @@ +import { ADD_TODO, REMOVE_TODO, TOGGLE_TODO } from '../actions/todos' + +import { RECEIVE_DATA } from '../actions/shared' + +export default function todos(state = [], action) { + switch (action.type) { + case ADD_TODO: + return state.concat([action.todo]) + case REMOVE_TODO: + return state.filter((todo) => todo.id !== action.id) + case TOGGLE_TODO: + return state.map((todo) => todo.id !== action.id ? todo : + Object.assign({}, todo, { complete: !todo.complete }) + ) + case RECEIVE_DATA: + return action.todos + default: + return state + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index db97090..9a08e0b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4344,6 +4344,11 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" +goals-todos-api@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/goals-todos-api/-/goals-todos-api-1.0.0.tgz#1086716697ad41b519b899a55e050773d98ffbff" + integrity sha512-mrKMYU7VCtz4a6K1Ba1CKxuNuyrnw0OyiMOgORm2p5WR1FvCA5JOOc3nBhEYgZFN2AEmnUSU/tmh0JmNJremlA== + graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.2" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02"