[MERN⑩] Posts & Comments

~~~~~~~~~~ (Contents) MERN ~~~~~~~~~~~
[MERN①] Express & MongoDB Setup
https://qiita.com/niyomong/private/3281af84486876f897f7
[MERN②]User API Routes & JWT Authentication
https://qiita.com/niyomong/private/c11616ff7b64925f9a2b
[MERN③] Profile API Routes
https://qiita.com/niyomong/private/8cff4e6fa0e81b92cb49
[MERN④] Post API
https://qiita.com/niyomong/private/3ce66f15375ad04b8989
[MERN⑤] Getting Started With React & The Frontend
https://qiita.com/niyomong/private/a5759e2fb89c9f222b6b
[MERN⑥] Redux Setup & Alerts
https://qiita.com/niyomong/private/074c27259924c7fd306b
[MERN⑦] React User Authentication
https://qiita.com/niyomong/private/37151784671eff3b92b6
[MERN⑧] Dashboard & Profile Management
https://qiita.com/niyomong/private/ab7e5da1b1983a226aca
[MERN⑨] Profile Display
https://qiita.com/niyomong/private/42426135e959c7844dcb
[MERN⑩] Posts & Comments
https://qiita.com/niyomong/private/19c78aea482b734c3cf5
[MERN11] デプロイ
https://qiita.com/niyomong/private/150f9000ce51548134ad
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

##1. Post Reducer, Action & Initial Component

###① GET_POSTSとPOST_ERRORアクションを設置

```actions/types
//...
export const ACCOUNT_DELETED = 'ACCOUNT_DELETED';
+ export const GET_POSTS = 'GET_POSTS';
+ export const POST_ERROR = 'POST_ERROR';
```

###② postReducerを生成

```reducers/post.js
//...
import profile from './profile';
+ import post from './post';

export default combineReducers({
  alert,
  auth,
  profile,
+  post,
});
```
```reducers/post.js
import { GET_POSTS, POST_ERROR } from '../actions/types';

const initialState = {
  posts: [],
  post: null,
  loading: true,
  error: {},
};

export default function (state = initialState, action) {
  const { type, payload } = action;

  switch (type) {
    case GET_POSTS:
      return {
        ...state,
        posts: payload,
        loading: false,
      };
    case POST_ERROR:
      return {
        ...state,
        error: payload,
        loading: false,
      };
    default:
      return state;
  }
}
```

###③ postアクション作成

```actions/post.js
import axios from 'axios';
import { setAlert } from './alert';
import { GET_POSTS, POST_ERROR } from './types';

// Get posts
export const getPosts = () => async (dispatch) => {
  try {
    const res = await axios.get('/api/posts');
    dispatch({
      type: GET_POSTS,
      payload: res.data,
    });
  } catch (err) {
    dispatch({
      type: POST_ERROR,
      payload: { msg: err.response.statusText, status: err.response.status },
    });
  }
};
```

###④ `posts`ルートをApp.jsに設置

```src/App.js
//...
+ import Posts from './components/posts/Posts';
import PrivateRoute from './components/routing/PrivateRoute';
//...
      <Switch>
        <Route exact path="/register" component={Register} />
        <Route exact path="/login" component={Login} />
+       <Route exact path="/posts" component={Posts} />
        <PrivateRoute exact path="/dashboard" component={Dashboard} />
//...
```

###⑤ 左上のサービス名にPosts一覧リンクを設置

```components/layout/Navbar.js
//...
  return (
    <nav className="navbar bg-dark">
      <h1>
+       <Link to="/posts">
          <i className="fas fa-code" /> Refnote
        </Link>
      </h1>

      {!loading && (
        <Fragment>{isAuthenticated ? authLinks : guestLinks}</Fragment>
//...
```

##2. Post Item Component

###① Postsコンポーネントを生成。

```components/posts/Posts.js
import React, { Fragment, useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Spinner from '../layout/Spinner';
import PostItem from './PostItem';
import { getPosts } from '../../actions/post';

const Posts = ({ getPosts, post: { posts, loading } }) => {
  useEffect(() => {
    getPosts();
  }, [getPosts]);

  return loading ? (
    <Spinner />
  ) : (
    <Fragment>
      <h1 className="large text-primary">Posts</h1>
      <p className="lead">
        <i className="fas fa-user" /> Welcome to the Posts
      </p>
      {/* PostForm */}
      <div className="Posts">
        {posts.map((post) => (
          <PostItem key={post._id} post={post} />
        ))}
      </div>
    </Fragment>
  );
};

Posts.propTypes = {
  getPosts: PropTypes.func.isRequired,
  post: PropTypes.object.isRequired,
};

const mapStateToProps = (state) => ({
  post: state.post,
});

export default connect(mapStateToProps, { getPosts })(Posts);
```
###② PostItemコンポーネントを生成。
以下項目追加。
・写真{avatar}、ユーザ名{name}、投稿日時{date}、投稿内容{text}、likes数{likes.length}、comments数{comments.length}、認証済の場合の削除ボタン{!auth.loading && user ===...}
・ProfileDisplayリンク設置{`/profile/${user}`}

```components/posts/PostItem.js
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import Moment from 'react-moment';
import { connect } from 'react-redux';

const PostItem = ({
  auth,
  post: { _id, text, name, avatar, user, likes, comments, date },
}) => {
  return (
    <div class="post bg-white p-1 my-1">
      <div>
        <Link to={`/profile/${user}`}>
          <img class="round-img" src={avatar} alt="" />
          <h4>{name}</h4>
        </Link>
      </div>
      <div>
        <p class="my-1">{text}</p>
        <p class="post-date">
          Posted on <Moment format="YYY/MM/DD">{date}</Moment>
        </p>
        <button type="button" class="btn btn-light">
          <i class="fas fa-thumbs-up" />{' '}
          {likes.length > 0 && <span>{likes.length}</span>}
        </button>
        <button type="button" class="btn btn-light">
          <i class="fas fa-thumbs-down"></i>
        </button>
        <Link to={`/posts/${_id}`} class="btn btn-primary">
          Discussion{' '}
          {comments.length > 0 && (
            <span class="comment-count">{comments.length}</span>
          )}
        </Link>
              {auth.isAuthenticated === false
                ? null
                : !auth.loading &&
                  user === auth.user._id && (
                    <button
                      onClick={(e) => deletePost(_id)}
                      type="button"
                      className="btn btn-danger"
                    >
                      <i className="fas fa-times"></i>
                    </button>
                  )}
      </div>
    </div>
  );
};

PostItem.propTypes = {
  post: PropTypes.object.isRequired,
  auth: PropTypes.object.isRequired,
};

const mapStateToProps = (state) => ({
  auth: state.auth,
});

export default connect(mapStateToProps, {})(PostItem);
```

##3. Like & Unlike Functionality

① TYPEアクションにUPDATE_LIKES設置
```actions/types.js
//...
+ export const UPDATE_LIKES = 'UPDATE_LIKES';
```

② Likeアクションを設置
・Get postsをコピペして`AddLike`を加工
・api/postsAPIにputしたい（payloadは、postのidとlikes）
・removeLikeはaddLikeとほぼ同じ。

```actions/post.js
//...
+ import { GET_POSTS, POST_ERROR, UPDATE_LIKES } from './types';
//...
// 以下、Get postsをコピペして加工
// Add like
export const AddLike = (id) => async (dispatch) => {
  try {
    const res = await axios.put(`/api/posts/like/${id}`);
    dispatch({
      type: UPDATE_LIKES,
      payload: { id, likes: res.data },
    });
  } catch (err) {
    dispatch({
      type: POST_ERROR,
      payload: { msg: err.response.statusText, status: err.response.status },
    });
  }
};

// Remove like
export const removeLike = (id) => async (dispatch) => {
  try {
    const res = await axios.put(`/api/posts/unlike/${id}`);
    dispatch({
      type: UPDATE_LIKES,
      payload: { id, likes: res.data },
    });
  } catch (err) {
    dispatch({
      type: POST_ERROR,
      payload: { msg: err.response.statusText, status: err.response.status },
    });
  }
};
```

###③ UPDATE_LIKE Reducerを設置

・postsを操作する。each post
・postのidとpayloadのid(actionで設置)がマッチしていれば、postとlikesを返す。マッチしていなければ、do nothing(ただpostを返す)

```reducers/post.js
+ import { GET_POSTS, POST_ERROR, UPDATE_LIKES } from '../actions/types';
//...以下追加。
    case UPDATE_LIKES:
      return {
        ...state,
        posts: state.posts.map((post) =>
          post._id === payload.id ? { ...post, likes: payload.likes } : post
        ),
        loading: false,
      };
    default:
      return state;
  }
}
```

###④ PostItemコンポーネントにLikeボタン設置

```components/posts/PostItem.js
//...
+ import { addLike, removeLike } from '../../actions/post';

const PostItem = ({
+  addLike,
+  removeLike,
  auth,
  post: { _id, text, name, avatar, user, likes, comments, date },
}) => {
  return (
//...
        <button
+          onClick={(e) => addLike(_id)}
          type="button"
          className="btn btn-light"
        >
          <i className="fas fa-thumbs-up" />{' '}
          {likes.length > 0 && <span>{likes.length}</span>}
        </button>
        <button
+          onClick={(e) => removeLike(_id)}
          type="button"
          className="btn btn-light"
        >
          <i className="fas fa-thumbs-down"></i>
        </button>
//...
PostItem.propTypes = {
  post: PropTypes.object.isRequired,
  auth: PropTypes.object.isRequired,
+  addLike: PropTypes.func.isRequired,
+  removeLike: PropTypes.func.isRequired,
};

+ export default connect(mapStateToProps, { addLike, removeLike })(PostItem);
```

##4. Deleting Posts

###① DELETE_POSTタイプ

```actions/types.js
//...
+ export const DELETE_POST = 'DELETE_POST';
```

###② deletePostアクション設置
・removeLikeアクションをコピペして更新

```actions/post.js
//...
+ import { DELETE_POST, GET_POSTS, POST_ERROR, UPDATE_LIKES } from './types';
//...
// Delete post
export const deletePost = (id) => async (dispatch) => {
  try {
    await axios.delete(`/api/posts/${id}`);
    dispatch({
      type: DELETE_POST,
      payload: id,
    });

    dispatch(setAlert('Post Removed', 'success'));
  } catch (err) {
    dispatch({
      type: POST_ERROR,
      payload: { msg: err.response.statusText, status: err.response.status },
    });
  }
};
```
###③ DELETE_POST reducer

```reducers/post.js
//..
  UPDATE_LIKES,
+  DELETE_POST,
} from '../actions/types';
//...
+   case DELETE_POST:
+     return {
+       ...state,
+       posts: state.posts.filter((post) => post._id !== payload),
+       loading: false,
+      };
//...
```
###④ Deleteボタン設置。

```components/posts/PostItem.js
//...
+ import { addLike, removeLike, deletePost } from '../../actions/post';

const PostItem = ({
  addLike,
  removeLike,
+  deletePost,
  auth,
  post: { _id, text, name, avatar, user, likes, comments, date },
}) => {
  return (
//...
         {!auth.loading && user === auth.user._id && (
           <button
+            onClick={(e) => deletePost(_id)}
             type="button"
             className="btn btn-danger"
           >
             <i className="fas fa-times"></i>
           </button>
         )}
//...
PostItem.propTypes = {
  post: PropTypes.object.isRequired,
  auth: PropTypes.object.isRequired,
  addLike: PropTypes.func.isRequired,
+  removeLike: PropTypes.func.isRequired,
+  deletePost: PropTypes.func.isRequired,
};

const mapStateToProps = (state) => ({
  auth: state.auth,
});

+ export default connect(mapStateToProps, { addLike, removeLike, deletePost })(
  PostItem
);
```
##5. Adding Posts

###① ADD_POSTアクションタイプ設置

```actions/types.js
//...
+ export const ADD_POST = 'ADD_POST';
```
###② addPostアクション
・deletePostアクションをコピペして編集。

```actions/post.js
//...
import {
+  ADD_POST,
  DELETE_POST,
  GET_POSTS,
  POST_ERROR,
  UPDATE_LIKES,
} from './types';
//...
// Add post
export const addPost = (formData) => async (dispatch) => {
  const config = {
    headers: {
      'Content-Type': 'application/json',
    },
  };

  try {
    const res = await axios.post('/api/posts', formData, config);

    dispatch({
      type: ADD_POST,
      payload: res.data,
    });

    dispatch(setAlert('Post Created', 'success'));
  } catch (err) {
    dispatch({
      type: POST_ERROR,
      payload: { msg: err.response.statusText, status: err.response.status },
    });
  }
};
```
###④ ADD_POST Reducer
~~~ 説明 ~~~
posts: [payload(Newポスト), ...state.posts(既存ポスト配列)]
→ 配列の順番もNewポストが一番上に来るようになる。

```reducers/post.js
import {
  GET_POSTS,
  POST_ERROR,
  UPDATE_LIKES,
  DELETE_POST,
+  ADD_POST,
} from '../actions/types';
//...
  switch (type) {
//...
+   case ADD_POST:
+     return {
+       ...state,
+       posts: [payload, ...state.posts],
+       loading: false,
+     };
//...
```
###⑤ポストフォームコンポーネントを生成
【前提】 rafcp + htmlテンプレ + Fragment+className以外の追加を +　で表記。

```components/posts/PostForm.js
+ import React, { Fragment, useState } from 'react';
import PropTypes from 'prop-types';
+ import { connect } from 'react-redux';
+ import { addPost } from '../../actions/post';

+ const PostForm = ({ addPost }) => {
+  const [text, setText] = useState('');

  return (
    <Fragment>
      <div className="post-form">
        <div className="bg-primary p">
          <h3>Say Something...</h3>
        </div>
        <form
          className="form my-1"
+          onSubmit={(e) => {
+            e.preventDefault();
+            addPost({ text });
+            setText('');
+          }}
        >
          <textarea
            name="text"
            cols="30"
            rows="5"
            placeholder="Create a post"
+            value={text}
+            onChange={(e) => setText(e.target.value)}
            required
          ></textarea>
          <input type="submit" className="btn btn-dark my-1" value="Submit" />
        </form>
      </div>
    </Fragment>
  );
};

PostForm.propTypes = {
+  addPost: PropTypes.func.isRequired,
};

+ export default connect(null, { addPost })(PostForm);
```
###⑥Postコンポーネントに<PostForm />を追加。

```components/posts/Post.js
//...
import PostItem from './PostItem';
+ import PostForm from './PostForm';
import { getPosts } from '../../actions/post';
//...
  return loading ? (
    <Spinner />
  ) : (
    <Fragment>
      <h1 className="large text-primary">Posts</h1>
      <p className="lead">
        <i className="fas fa-user" /> Welcome to the Posts
      </p>
+      <PostForm />
      <div className="Posts">
        {posts.map((post) => (
          <PostItem key={post._id} post={post} />
        ))}
      </div>
    </Fragment>
  );
};
//...
```

##6. Single Post Display

###GET_POSTアクションタイプ

```actions/types.js
//...
export const GET_POSTS = 'GET_POSTS';
+ export const GET_POST = 'GET_POST';
//...
```
###① getPost
・getPostsをコピペして一部修正。

```actions/post.js
//...
import {
  GET_POSTS,
  POST_ERROR,
  UPDATE_LIKES,
  DELETE_POST,
  ADD_POST,
+  GET_POST,
} from './types';
//...
//以下すべて追加。
// Get post
export const getPost = (id) => async (dispatch) => {
  try {
    const res = await axios.get('/api/posts/${id}');
    dispatch({
      type: GET_POST,
      payload: res.data,
    });
  } catch (err) {
    dispatch({
      type: POST_ERROR,
      payload: { msg: err.response.statusText, status: err.response.status },
    });
  }
};
```
###② GET_POSTリデューサーを設置。

```reducers/post.js
import {
  GET_POSTS,
  POST_ERROR,
  UPDATE_LIKES,
  DELETE_POST,
  ADD_POST,
+  GET_POST,
} from '../actions/types';
//...
  switch (type) {
    case GET_POSTS:
      return {
        ...state,
        posts: payload,
        loading: false,
      };
+   case GET_POST:
+     return {
+       ...state,
+       post: payload,
+       loading: false,
+     };
```
###③ Postルート設置。

```src/App.js
//...
import Posts from './components/posts/Posts';
+ import Post from './components/post/Post';
//...
          <Route exact path="/posts" component={Posts} />
+         <Route exact path="/posts/:id" component={Post} />
//...
```
###④ PostItemに`showActions`を設置
~~~ 説明 ~~~
showActions: 他のコンポーネントでshowActions内のFragmentは表示させないようにする
・PostItemコンポーネント上では表示させたいので、`true`設定
・Postコンポーネント上では非表示にしたいので、`false`設定

```components/posts/PostItem.js
//...
const PostItem = ({
  addLike,
  removeLike,
  deletePost,
  auth,
  post: { _id, text, name, avatar, user, likes, comments, date },
+  showActions,
}) => {
  return (
//...
+         {showActions && (
+           <Fragment>
             <button
               onClick={(e) => addLike(_id)}
               type="button"
               className="btn btn-light"
             >
//...
             {!auth.loading && user === auth.user._id && (
               <button
                 onClick={(e) => deletePost(_id)}
                 type="button"
                 className="btn btn-danger"
               >
                 <i className="fas fa-times"></i>
               </button>
             )}
+           </Fragment>
+         )}
        </div>
      </div>
    </Fragment>
  );
};

+ PostItem.defaultProps = {
+  showActions: true,
+ };

PostItem.propTypes = {
  post: PropTypes.object.isRequired,
  auth: PropTypes.object.isRequired,
  addLike: PropTypes.func.isRequired,
  removeLike: PropTypes.func.isRequired,
  deletePost: PropTypes.func.isRequired,
};
//...
```
###⑤ Postコンポーネント
PostItemコンポーネントを持ってくる（しかし、showActionsをfalseにする）

```components/post/Post.js
import React, { Fragment, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import Spinner from '../layout/Spinner';
import PostItem from '../posts/PostItem';
import { getPost } from '../../actions/post';

const Post = ({ getPost, post: { post, loading }, match }) => {
  useEffect(() => {
    getPost(match.params.id);
  }, [getPost]);

  return loading || post === null ? (
    <Spinner />
  ) : (
    <Fragment>
      <Link to="/posts" className="btn">
        Back To Posts
      </Link>
      <PostItem post={post} showActions={false} />
    </Fragment>
  );
};

Post.propTypes = {
  getPost: PropTypes.func.isRequired,
  post: PropTypes.object.isRequired,
};

const mapStateToProps = (state) => ({
  post: state.post,
});

export default connect(mapStateToProps, { getPost })(Post);
```
##7. Adding Comments

###① アクションタイプ設定

```actions/types.js
//...
export const ADD_POST = 'ADD_POST';
+ export const ADD_COMMENT = 'ADD_COMMENT';
+ export const REMOVE_COMMENT = 'REMOVE_COMMENT';
```
###② addComment + deleteComment アクションを追加。
・addComment: addPostアクションをコピペして修正。
・deleteComment: addCommentアクションをコピペして修正。

```actions/post.js
import {
  GET_POSTS,
  POST_ERROR,
  UPDATE_LIKES,
  DELETE_POST,
  ADD_POST,
  GET_POST,
+  ADD_COMMENT,
+  REMOVE_COMMENT,
} from './types';
//...
//以下全て追加。
// Add comment
export const addComment = (postId, formData) => async (dispatch) => {
  const config = {
    headers: {
      'Content-Type': 'application/json',
    },
  };

  try {
    const res = await axios.post(
      `/api/posts/comment/${postId}`,
      formData,
      config
    );

    dispatch({
      type: ADD_COMMENT,
      payload: res.data,
    });

    dispatch(setAlert('Comment Added', 'success'));
  } catch (err) {
    dispatch({
      type: POST_ERROR,
      payload: { msg: err.response.statusText, status: err.response.status },
    });
  }
};

// Delete comment
export const deleteComment = (postId, commentId) => async (dispatch) => {
  try {
    const res = await axios.delete(`/api/posts/comment/${postId}/${commentId}`);

    dispatch({
      type: REMOVE_COMMENT,
      payload: commentId,
    });

    dispatch(setAlert('Comment Removed', 'success'));
  } catch (err) {
    dispatch({
      type: POST_ERROR,
      payload: { msg: err.response.statusText, status: err.response.status },
    });
  }
};
```


###③ ADD_COMMENT + REMOVE_COMMENT　リデューサー設置
・`comments: payload` ->　postのすべてのコメントデータを持ってくる。
・`comments: state.post.comments.filter((comment) => comment._id !== payload)` -> 削除したcomment._id以外のすべてのコメントデータを持ってくる。

```reducers/post.js
import {
  GET_POSTS,
  POST_ERROR,
  UPDATE_LIKES,
  DELETE_POST,
  ADD_POST,
  GET_POST,
+  ADD_COMMENT,
+  REMOVE_COMMENT,
} from '../actions/types';
//...
    case ADD_COMMENT:
      return {
        ...state,
        post: { ...state.post, comments: payload },
        loading: false,
      };
    case REMOVE_COMMENT:
      return {
        ...state,
        post: {
          ...state.post,
          comments: state.post.comments.filter(
            (comment) => comment._id !== payload
          ),
        },
        loading: false,
      };
    default:
      return state;
  }
}
```
###④ コメントフォームを設置。

・Fragment内は、PostFormからコピペして修正。

```components/post/CommentForm.js
import React, { Fragment, useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { addComment } from '../../actions/post';

const CommentForm = ({ postId, addComment }) => {
  const [text, setText] = useState('');

  return (
    <Fragment>
      <div className="post-form">
        <div className="bg-primary p">
          <h3>Leave a Comment</h3>
        </div>
        <form
          className="form my-1"
          onSubmit={(e) => {
            e.preventDefault();
            addComment(postId, { text });
            setText('');
          }}
        >
          <textarea
            name="text"
            cols="30"
            rows="5"
            placeholder="Create a post"
            value={text}
            onChange={(e) => setText(e.target.value)}
            required
          ></textarea>
          <input type="submit" className="btn btn-dark my-1" value="Submit" />
        </form>
      </div>
    </Fragment>
  );
};

CommentForm.propTypes = {
  addComment: PropTypes.func.isRequired,
};

export default connect(null, { addComment })(CommentForm);
```

###⑤ PostページにCommentFormを設置。

```components/post/Post.js
//...
import PostItem from '../posts/PostItem';
+ import CommentForm from '../post/CommentForm';
import { getPost } from '../../actions/post';
//...
  return loading || post === null ? (
    <Spinner />
  ) : (
    <Fragment>
      <Link to="/posts" className="btn">
        Back To Posts
      </Link>
      <PostItem post={post} showActions={false} />
+      <CommentForm postId={post._id} />
    </Fragment>
  );
};
//...
```
##8. Comment Display & Delete

 <Link to={`/profile/${user}`}>
`use`IDは`comment`から持ってくる。

```components/post/CommentItem.js
import React, { Fragment } from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Moment from 'react-moment';
import { deleteComment } from '../../actions/post';

const CommentItem = ({
  postId,
  comment: { _id, text, name, avatar, user, date },
  auth,
  deleteComment,
}) => {
  return (
    <Fragment>
      <div class="post bg-white p-1 my-1">
        <div>
          <Link to={`/profile/${user}`}>
            <img class="round-img" src={avatar} alt="" />
            <h4>{name}</h4>
          </Link>
        </div>
        <div>
          <p class="my-1">{text}</p>
          <p class="post-date">
            Posted on <Moment format="YYY/MM/DD">{date}</Moment>
          </p>
          {!auth.loading && user === auth.user._id && (
            <button
              onClick={(e) => deleteComment(postId, _id)}
              type="button"
              className="btn btn-danger"
            >
              <i className="fas fa-times" />
            </button>
          )}
        </div>
      </div>
    </Fragment>
  );
};

CommentItem.propTypes = {
  postId: PropTypes.string.isRequired,
  comment: PropTypes.object.isRequired,
  auth: PropTypes.object.isRequired,
  deleteComment: PropTypes.func.isRequired,
};

const mapStateToProps = (state) => ({
  auth: state.auth,
});

export default connect(mapStateToProps, { deleteComment })(CommentItem);
```