diff --git a/README.md b/README.md index 4944ab6..ff75343 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,8 @@ All of our frontend files are located in the `/client` folder. This folder also We will be using **CSS modules** for our styling. All **GLOBAL** styles should be kept under the `/styles` folder. Our app is separated into "domains" with the following folder structure: - - Dumb Components live in `/components` - - Smart Containers live in `/scenes` - - Reducers & Actions live in `/` + +- Dumb Components live in `/components` +- Smart Containers live in `/scenes` +- Reducers & Actions live in `/` diff --git a/client/app/index.js b/client/app/index.js index f2de3ec..9bb8ffe 100644 --- a/client/app/index.js +++ b/client/app/index.js @@ -4,7 +4,7 @@ import { Router, Route, IndexRoute } from 'react-router'; import { Provider } from 'react-redux'; import PostList from 'post/scenes/PostList'; -import PostInformation from 'post/scenes/PostInformation'; +// import PostInformation from 'post/scenes/PostInformation'; import App from './components/App'; import { store, history } from './createStore'; @@ -26,7 +26,7 @@ ReactDOM.render( - + { /* /post/3 */ } , diff --git a/client/app/rootReducer.js b/client/app/rootReducer.js index 92b3b86..274738c 100644 --- a/client/app/rootReducer.js +++ b/client/app/rootReducer.js @@ -3,4 +3,7 @@ import { routerReducer as routing } from 'react-router-redux'; import post from 'post/reducer'; -export default combineReducers({ post, routing }); +export default combineReducers({ + post: post, + routing: routing +}); diff --git a/client/post/actions.js b/client/post/actions.js index 1966090..d239c68 100644 --- a/client/post/actions.js +++ b/client/post/actions.js @@ -2,39 +2,53 @@ import { createAction } from 'redux-actions'; import fetcher from 'utils/fetcher'; // Actions we dispatch in our Action Creators -export const readPostByIdSucceeded = createAction('READ_POST_BY_ID_SUCCEEDED'); -export const readPostsSucceeded = createAction('READ_POSTS_SUCCEEDED'); -export const votePostByIdSucceeded = createAction('VOTE_POST_BY_ID_SUCCEEDED'); +export const readPostByIdSucceeded = 'READ_POST_BY_ID_SUCCEEDED'; +export const readPostsSucceeded = 'READ_POSTS_SUCCEEDED'; +export const votePostByIdSucceeded = 'VOTE_POST_BY_ID_SUCCEEDED'; // Action Creators export const readPostById = (postId) => { - return async (dispatch) => { - const post = await fetcher.get(`/api/posts/${postId}`); - dispatch(readPostByIdSucceeded({ post })); - }; }; export const readPosts = () => { return async (dispatch) => { - const body = await fetcher.get('/api/posts'); - const posts = body.results; - - dispatch(readPostsSucceeded({ posts })); + try { + const response = await fetcher.get('/api/posts'); + + dispatch({ + type: readPostsSucceeded, + payload: { + posts: response.results + }, + }); + } + catch (e) { + console.error(e); + } }; }; export const votePostById = (postId, isUpvote) => { return async (dispatch) => { - let body; - - if (isUpvote) { - body = await fetcher.post(`/api/posts/${postId}/vote`); + try { + let response; + if (isUpvote) { + response = await fetcher.post(`/api/posts/${postId}/vote`); + } + else { + response = await fetcher.delete(`/api/posts/${postId}/vote`); + } + + dispatch({ + type: votePostByIdSucceeded, + payload: { + post: response, + }, + }); } - else { - body = await fetcher.delete(`/api/posts/${postId}/vote`); + catch (e) { + console.error(e); } - - dispatch(votePostByIdSucceeded({ post: body })); }; -}; +} diff --git a/client/post/components/Post/index.js b/client/post/components/Post/index.js index c65875f..255df81 100644 --- a/client/post/components/Post/index.js +++ b/client/post/components/Post/index.js @@ -33,7 +33,6 @@ function Post({ id, title, user, votes, upvotePost, downvotePost }) { static propTypes = { ... }; - We have to manually set it outside the function. */ diff --git a/client/post/reducer.js b/client/post/reducer.js index 351db58..d0cf469 100644 --- a/client/post/reducer.js +++ b/client/post/reducer.js @@ -1,56 +1,30 @@ -import { handleActions, combineActions } from 'redux-actions'; -import _ from 'lodash'; import { readPostByIdSucceeded, readPostsSucceeded, votePostByIdSucceeded } from './actions'; -const defaultState = { - byId: {}, - allIds: [], -}; +const defaultState = []; -/* - We use redux-actions to handle our actions. - Normally this would look like: +export default function postReducer(state = defaultState, action) { + switch (action.type) { + case readPostsSucceeded: + return action.payload.posts; + case votePostByIdSucceeded: + let foundPost = false; + const newState = [...state]; - function (state = defaultState, action) { - switch (action.type) { - case readPostsByIdSucceeded: - return {...}; - case readPostsSucceeded: - return {...}; - default: - return state; - } - } -} -*/ - -export default handleActions({ - [combineActions(readPostByIdSucceeded, votePostByIdSucceeded)]: (state, { payload }) => { - const post = payload.post; - return { - ...state, - byId: { - ...state.byId, - [post.id]: post, - }, - allIds: _.union(state.allIds, [post.id]), - }; - }, + for (let k = 0; k < newState.length; ++k) { + const post = newState[k]; + if (post.id === action.payload.post.id) { + foundPost = true; + newState[k] = action.payload.post; + break; + } + } - [readPostsSucceeded]: (state, { payload }) => { - const posts = payload.posts; - const byId = {}; - const allIds = []; + if (!foundPost) { + newState.push(action.payload.post); + } - posts.forEach((post) => { - byId[post.id] = post; - allIds.push(post.id); - }); - - return { - ...state, - byId, - allIds, - }; - }, -}, defaultState); + return newState; + default: + return state; + } +} diff --git a/client/post/scenes/PostInformation/index.js b/client/post/scenes/PostInformation/index.js index 89fdff7..cdec0db 100644 --- a/client/post/scenes/PostInformation/index.js +++ b/client/post/scenes/PostInformation/index.js @@ -1,11 +1,10 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import _ from 'lodash'; import { readPostById } from '../../actions'; -export class PostInformation extends Component { +class PostInformation extends Component { static propTypes = { params: PropTypes.shape({ id: PropTypes.string, @@ -18,25 +17,17 @@ export class PostInformation extends Component { }), votes: PropTypes.number, }), - readPostById: PropTypes.func, + dispatch: PropTypes.func, } /* Load the data before the actual component is rendered. */ componentDidMount() { - this.props.readPostById(this.props.params.id); } render() { const { post = {} } = this.props; - /* - This is shorthand for saying: const post = this.props.post || {}; - By wrapping the 'post' variable with curly braces, we destructuring the - 'this.props' object and extracting the 'post' attribute out of it. - NOTE: This only works if the name of your local variable is the same as the - key within the object. - */ return (
@@ -55,39 +46,10 @@ export class PostInformation extends Component { Since your Redux Store is supposed to act as an "overall data store", you use the mapStateToProps function to cherry-pick out which slices of the state you want to put in your component. - Here, this.props.post will be populated with 'posts[ownProps.params.id]'. */ function mapStateToProps(state, ownProps) { - const posts = _.get(state, 'post.byId'); - /* - Here, we're using Lodash's get function to retrieve state.post.byId. - We use this to guard against undefined. Since we're reaching two levels deep in the - object, we want to be sure that state.post is defined before we try to reach state.post.byId. - If post === undefined or null, then an error is thrown. - */ return { - post: posts[ownProps.params.id], - }; -} -/* - mapDispatchToProps is an OPTIONAL secondary parameter passed into connect(). It essentially injects - functions into 'this.props' that make dispatching Redux actions much easier. In this case, if we call - this.props.readPostById(postId), we will actually dispatch the 'readPostById' action with the parameter - postId. This is convenient because otherwise, we would have to manually write: - this.props.dispatch(readPostById(postId)) - versus: - this.props.readPostById(postId). - Although this may not seem like much of an improvement, it is important if we want to modify some of the parameters - being passed into our function before we dispatch our action. Think of it as a layer of abstraction. -*/ -function mapDispatchToProps(dispatch) { - return { - readPostById: (postId) => dispatch(readPostById(postId)), }; } -/* - We're not exporting PostInformation, but rather a "connected" version of the component. -*/ -export default connect(mapStateToProps, mapDispatchToProps)(PostInformation); diff --git a/client/post/scenes/PostList/index.js b/client/post/scenes/PostList/index.js index 1abb4ff..da9a3a4 100644 --- a/client/post/scenes/PostList/index.js +++ b/client/post/scenes/PostList/index.js @@ -5,7 +5,7 @@ import { connect } from 'react-redux'; import Post from '../../components/Post'; import { readPosts, votePostById } from '../../actions'; -export class PostList extends Component { +class PostList extends Component { static propTypes = { posts: PropTypes.shape({ byId: PropTypes.objectOf(PropTypes.shape({ @@ -18,8 +18,7 @@ export class PostList extends Component { })), allIds: PropTypes.arrayOf(PropTypes.number), }), - readPosts: PropTypes.func, - votePostById: PropTypes.func, + dispatch: PropTypes.func, } /* @@ -28,24 +27,23 @@ export class PostList extends Component { it with data */ componentDidMount() { - this.props.readPosts(); + this.props.dispatch(readPosts()); } votePost = (id, isUpvote) => { - this.props.votePostById(id, isUpvote); + this.props.dispatch(votePostById(id, isUpvote)); } render() { /* Object destructuring. This is equivalent to: const posts = this.props.posts; */ - const { posts } = this.props; - const postList = posts.allIds.map((id) => posts.byId[id]); + const posts = this.props.posts; return (

Posts

- { postList.map((p) => ( + { posts.map((p) => ( ); - /* - In this render function, we're iterating over the entire array 'postList' and creating a - 'Post' component for each of them. Notice that the upvotePost and downvotePost are also being passed in - an anonymous arrow function. We have to do this because we need to bind each function to the context of its own - 'Post' component. - - For example, if we had Post1 and Post2, we need the upvote and downvote functions to only apply to their specific posts. - Any function of the signature () => ... automatically binds 'this' to its parent context. - */ } } -/* Notice how we don't pass the second parameter ownProps here, because it's not necessary. */ function mapStateToProps(state) { return { posts: state.post, }; } -function mapDispatchToProps(dispatch) { - return { - readPosts: () => dispatch(readPosts()), - votePostById: (postId, isUpvote) => dispatch(votePostById(postId, isUpvote)), - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(PostList); +export default connect(mapStateToProps)(PostList);