diff --git a/client/modules/Post/CommentActions.js b/client/modules/Post/CommentActions.js
new file mode 100644
index 000000000..9c15a4cf2
--- /dev/null
+++ b/client/modules/Post/CommentActions.js
@@ -0,0 +1,76 @@
+import callApi from '../../util/apiCaller';
+
+// Export Constants
+export const ADD_COMMENT = 'ADD_COMMENT';
+export const ADD_COMMENTS = 'ADD_COMMENTS';
+export const DELETE_COMMENT = 'DELETE_COMMENT';
+export const EDIT_COMMENT = 'EDIT_COMMENT';
+export const UPDATE_COMMENT = 'UPDATE_COMMENT';
+
+// Export Actions
+export function addComment(comment) {
+ return {
+ type: ADD_COMMENT,
+ comment,
+ };
+}
+
+export function addCommentRequest(comment, postId) {
+ return dispatch => {
+ return callApi(`posts/${postId}/comment`, 'post', {
+ comment: {
+ author: comment.author,
+ content: comment.content,
+ },
+ }).then(res => dispatch(addComment(res.comment)));
+ };
+}
+
+export function addComments(comments) {
+ return {
+ type: ADD_COMMENTS,
+ comments,
+ };
+}
+
+export function fetchComments(postId) {
+ return (dispatch) => {
+ return callApi(`posts/${postId}/comments`).then(res => {
+ dispatch(addComments(res.comments));
+ });
+ };
+}
+
+export function deleteComment(cuid) {
+ return {
+ type: DELETE_COMMENT,
+ cuid,
+ };
+}
+
+export function editComment(cuid) {
+ return {
+ type: EDIT_COMMENT,
+ cuid,
+ };
+}
+
+
+export function updateComment(comment) {
+ return {
+ type: UPDATE_COMMENT,
+ comment,
+ };
+}
+
+export function deleteCommentRequest(cuid) {
+ return (dispatch) => {
+ return callApi(`comment/${cuid}`, 'delete').then(() => dispatch(deleteComment(cuid)));
+ };
+}
+
+export function updateCommentRequest(comment) {
+ return (dispatch) => {
+ return callApi(`comment/${comment.cuid}`, 'put', { comment }).then(() => dispatch(updateComment(comment)));
+ };
+}
diff --git a/client/modules/Post/CommentReducer.js b/client/modules/Post/CommentReducer.js
new file mode 100644
index 000000000..55acdc809
--- /dev/null
+++ b/client/modules/Post/CommentReducer.js
@@ -0,0 +1,60 @@
+/* eslint-disable no-param-reassign */
+/* eslint-disable no-unused-expressions */
+import {
+ ADD_COMMENTS,
+ ADD_COMMENT,
+ DELETE_COMMENT,
+ EDIT_COMMENT,
+ UPDATE_COMMENT,
+} from './CommentActions';
+
+// Initial State
+const initialState = { data: [] };
+
+const CommentReducer = (state = initialState, action) => {
+ switch (action.type) {
+ case ADD_COMMENT:
+ return {
+ data: [action.comment, ...state.data],
+ };
+ case ADD_COMMENTS:
+ return {
+ data: action.comments,
+ };
+ case DELETE_COMMENT:
+ return {
+ data: state.data.filter(comment => comment.cuid !== action.cuid),
+ };
+ case EDIT_COMMENT:
+ return {
+ data: state.data.map(comment => {
+ comment.cuid === action.cuid
+ ? (comment.isEdit = true)
+ : (comment.isEdit = false);
+ return comment;
+ }),
+ };
+ case UPDATE_COMMENT:
+ return {
+ data: state.data.map(comment => {
+ comment.isEdit = false;
+ if (comment.cuid === action.comment.cuid) {
+ comment.content = action.comment.content;
+ return comment;
+ }
+ return comment;
+ }),
+ };
+
+ default:
+ return state;
+ }
+};
+
+/* Selectors */
+
+// Get all Comments
+export const getComments = state => state.comments.data;
+
+// Export Reducer
+export default CommentReducer;
diff --git a/client/modules/Post/components/CommentCreateWidget/CommentCreateWidget.css b/client/modules/Post/components/CommentCreateWidget/CommentCreateWidget.css
new file mode 100644
index 000000000..4c9b84ec5
--- /dev/null
+++ b/client/modules/Post/components/CommentCreateWidget/CommentCreateWidget.css
@@ -0,0 +1,45 @@
+.form {
+ background: #FAFAFA;
+ padding: 32px;
+ border: 1px solid #eee;
+ border-radius: 4px;
+}
+
+.form-content{
+ width: 100%;
+ font-size: 14px;
+}
+
+.form-title{
+ font-size: 16px;
+ font-weight: 700;
+ margin-bottom: 16px;
+ color: #757575;
+}
+
+.form-field{
+ width: 100%;
+ margin-bottom: 16px;
+ font-family: 'Lato', sans-serif;
+ font-size: 16px;
+ line-height: normal;
+ padding: 12px 16px;
+ border-radius: 4px;
+ border: 1px solid #ddd;
+ outline: none;
+ color: #212121;
+}
+
+textarea {
+ min-height: 200px;
+}
+
+.post-submit-button {
+ display: inline-block;
+ padding: 8px 16px;
+ font-size: 18px;
+ color: #FFF;
+ background: #03A9F4;
+ text-decoration: none;
+ border-radius: 4px;
+}
diff --git a/client/modules/Post/components/CommentCreateWidget/CommentCreateWidget.js b/client/modules/Post/components/CommentCreateWidget/CommentCreateWidget.js
new file mode 100644
index 000000000..83ddf66a8
--- /dev/null
+++ b/client/modules/Post/components/CommentCreateWidget/CommentCreateWidget.js
@@ -0,0 +1,37 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { injectIntl, FormattedMessage } from 'react-intl';
+
+// Import Style
+import styles from './CommentCreateWidget.css';
+
+export class CommentCreateWidget extends Component {
+ addComment = () => {
+ const authorRef = this.refs.author;
+ const contentRef = this.refs.content;
+ if (authorRef.value && contentRef.value) {
+ this.props.addComment(authorRef.value, contentRef.value);
+ authorRef.value = contentRef.value = '';
+ }
+ };
+
+ render() {
+ const cls = `${styles.form}`;
+ return (
+
+ );
+ }
+}
+
+CommentCreateWidget.propTypes = {
+ addComment: PropTypes.func.isRequired,
+};
+
+export default injectIntl(CommentCreateWidget);
diff --git a/client/modules/Post/components/CommentEditWidget/CommentEditWidget.css b/client/modules/Post/components/CommentEditWidget/CommentEditWidget.css
new file mode 100644
index 000000000..783573fe2
--- /dev/null
+++ b/client/modules/Post/components/CommentEditWidget/CommentEditWidget.css
@@ -0,0 +1,33 @@
+.form {
+ background: #FAFAFA;
+ padding: 32px;
+ border: 1px solid #eee;
+ border-radius: 4px;
+}
+
+.form-field{
+ width: 100%;
+ margin-bottom: 16px;
+ font-family: 'Lato', sans-serif;
+ font-size: 16px;
+ line-height: normal;
+ padding: 12px 16px;
+ border-radius: 4px;
+ border: 1px solid #ddd;
+ outline: none;
+ color: #212121;
+}
+
+textarea {
+ min-height: 200px;
+}
+
+.post-submit-button {
+ display: inline-block;
+ padding: 8px 16px;
+ font-size: 18px;
+ color: #FFF;
+ background: #03A9F4;
+ text-decoration: none;
+ border-radius: 4px;
+}
diff --git a/client/modules/Post/components/CommentEditWidget/CommentEditWidget.js b/client/modules/Post/components/CommentEditWidget/CommentEditWidget.js
new file mode 100644
index 000000000..ea791aabc
--- /dev/null
+++ b/client/modules/Post/components/CommentEditWidget/CommentEditWidget.js
@@ -0,0 +1,43 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { injectIntl, FormattedMessage } from 'react-intl';
+
+// Import Style
+import styles from './CommentEditWidget.css';
+
+export class CommentEditWidget extends Component {
+ updateComment = () => {
+ const contentRef = this.refs.content;
+ if (contentRef.value) {
+ this.props.onUpdate(contentRef.value);
+ }
+ };
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+CommentEditWidget.propTypes = {
+ onUpdate: PropTypes.func.isRequired,
+ comment: PropTypes.shape({
+ content: PropTypes.string.isRequired,
+ }).isRequired,
+};
+
+export default injectIntl(CommentEditWidget);
diff --git a/client/modules/Post/components/CommentList.js b/client/modules/Post/components/CommentList.js
new file mode 100644
index 000000000..c11111403
--- /dev/null
+++ b/client/modules/Post/components/CommentList.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+// Import Components
+import CommentListItem from './CommentListItem/CommentListItem';
+
+function CommentList(props) {
+ return (
+
+ {props.comments.map(comment => (
+
+ props.handleOnUpdate(comment.cuid, content)}
+ onEdit={() => props.handleOnEdit(comment.cuid)}
+ onDelete={() => props.handleDeleteComment(comment.cuid)}
+ />
+
+ ))}
+
+ );
+}
+
+CommentList.propTypes = {
+ comments: PropTypes.arrayOf(
+ PropTypes.shape({
+ author: PropTypes.string.isRequired,
+ content: PropTypes.string.isRequired,
+ slug: PropTypes.string.isRequired,
+ cuid: PropTypes.string.isRequired,
+ })
+ ).isRequired,
+ handleDeleteComment: PropTypes.func.isRequired,
+};
+
+export default CommentList;
diff --git a/client/modules/Post/components/CommentListItem/CommentListItem.css b/client/modules/Post/components/CommentListItem/CommentListItem.css
new file mode 100644
index 000000000..909f0d577
--- /dev/null
+++ b/client/modules/Post/components/CommentListItem/CommentListItem.css
@@ -0,0 +1,45 @@
+.single-comment {
+ background: #fafafa;
+ padding: 32px;
+ border: 1px solid #eee;
+ border-radius: 4px;
+ margin: 16px 0px;
+}
+
+.author-name {
+ font-size: 16px;
+ margin-bottom: 16px;
+ color: #757575;
+}
+
+.post-submit-button {
+ display: inline-block;
+ padding: 8px 16px;
+ font-size: 18px;
+ color: #fff;
+ background: #03a9f4;
+ text-decoration: none;
+ border-radius: 4px;
+}
+
+.comment-delete-button {
+ color: red;
+}
+
+.comment-edit-button {
+ color: #03a9f4;
+ margin-right: 8px;
+}
+
+.form-field {
+ width: 100%;
+ margin-bottom: 16px;
+ font-family: "Lato", sans-serif;
+ font-size: 16px;
+ line-height: normal;
+ padding: 12px 16px;
+ border-radius: 4px;
+ border: 1px solid #ddd;
+ outline: none;
+ color: #212121;
+}
diff --git a/client/modules/Post/components/CommentListItem/CommentListItem.js b/client/modules/Post/components/CommentListItem/CommentListItem.js
new file mode 100644
index 000000000..6b8c95b36
--- /dev/null
+++ b/client/modules/Post/components/CommentListItem/CommentListItem.js
@@ -0,0 +1,36 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+// Import Style
+import styles from './CommentListItem.css';
+import { CommentEditWidget } from '../CommentEditWidget/CommentEditWidget';
+
+function CommentListItem(props) {
+ return (
+
+
{props.comment.author}
+ {props.comment.isEdit ? (
+
props.onUpdate(content)} comment={props.comment} />
+ ) : (
+
+
{props.comment.content}
+
+
+
+ )}
+
+ );
+}
+
+CommentListItem.propTypes = {
+ comment: PropTypes.shape({
+ author: PropTypes.string.isRequired,
+ content: PropTypes.string.isRequired,
+ isEdit: PropTypes.bool.isRequired,
+ }).isRequired,
+ onDelete: PropTypes.func.isRequired,
+ onEdit: PropTypes.func.isRequired,
+ onUpdate: PropTypes.func.isRequired,
+};
+
+export default CommentListItem;
diff --git a/client/modules/Post/pages/PostDetailPage/PostDetailPage.js b/client/modules/Post/pages/PostDetailPage/PostDetailPage.js
index 32c1e1c11..e4524bde4 100644
--- a/client/modules/Post/pages/PostDetailPage/PostDetailPage.js
+++ b/client/modules/Post/pages/PostDetailPage/PostDetailPage.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Helmet from 'react-helmet';
@@ -9,21 +9,52 @@ import styles from '../../components/PostListItem/PostListItem.css';
// Import Actions
import { fetchPost } from '../../PostActions';
+import { addCommentRequest, fetchComments, deleteCommentRequest, editComment, updateCommentRequest } from '../../CommentActions';
// Import Selectors
import { getPost } from '../../PostReducer';
+import { getComments } from '../../CommentReducer';
-export function PostDetailPage(props) {
- return (
-
-
-
-
{props.post.title}
-
{props.post.name}
-
{props.post.content}
+// Import Components
+import { CommentCreateWidget } from '../../components/CommentCreateWidget/CommentCreateWidget';
+import CommentList from '../../components/CommentList';
+
+class PostDetailPage extends Component {
+ componentDidMount() {
+ this.props.dispatch(fetchComments(this.props.post.cuid));
+ }
+
+ handleOnEdit = cuid => {
+ this.props.dispatch(editComment(cuid));
+ }
+
+ handleAddComment = (author, content) => {
+ this.props.dispatch(addCommentRequest({ author, content }, this.props.post.cuid));
+ }
+
+ handleOnUpdate = (cuid, content) => {
+ this.props.dispatch(updateCommentRequest({ cuid, content }));
+ }
+
+ handleDeleteComment = comment => {
+ if (confirm('Do you want to delete this comment')) { // eslint-disable-line
+ this.props.dispatch(deleteCommentRequest(comment));
+ }
+ };
+ render() {
+ return (
+
+
+
+
{this.props.post.title}
+
{this.props.post.name}
+
{this.props.post.content}
+
+
+
-
- );
+ );
+ }
}
// Actions required to provide data for this component to render in server side.
@@ -34,6 +65,7 @@ PostDetailPage.need = [params => {
// Retrieve data from store as props
function mapStateToProps(state, props) {
return {
+ comments: getComments(state),
post: getPost(state, props.params.cuid),
};
}
@@ -46,6 +78,11 @@ PostDetailPage.propTypes = {
slug: PropTypes.string.isRequired,
cuid: PropTypes.string.isRequired,
}).isRequired,
+ comments: PropTypes.shape({
+ author: PropTypes.isRequired,
+ content: PropTypes.isRequired,
+ }).isRequired,
+ dispatch: PropTypes.func.isRequired,
};
export default connect(mapStateToProps)(PostDetailPage);
diff --git a/client/reducers.js b/client/reducers.js
index 2aa143142..1d66c4464 100644
--- a/client/reducers.js
+++ b/client/reducers.js
@@ -6,11 +6,13 @@ import { combineReducers } from 'redux';
// Import Reducers
import app from './modules/App/AppReducer';
import posts from './modules/Post/PostReducer';
+import comments from './modules/Post/CommentReducer';
import intl from './modules/Intl/IntlReducer';
// Combine all reducers into one root reducer
export default combineReducers({
app,
posts,
+ comments,
intl,
});
diff --git a/server/controllers/post.controller.js b/server/controllers/post.controller.js
index e62804c41..2fb7e8eec 100644
--- a/server/controllers/post.controller.js
+++ b/server/controllers/post.controller.js
@@ -1,4 +1,5 @@
import Post from '../models/post';
+import Comment from '../models/comment';
import cuid from 'cuid';
import slug from 'limax';
import sanitizeHtml from 'sanitize-html';
@@ -10,12 +11,14 @@ import sanitizeHtml from 'sanitize-html';
* @returns void
*/
export function getPosts(req, res) {
- Post.find().sort('-dateAdded').exec((err, posts) => {
- if (err) {
- res.status(500).send(err);
- }
- res.json({ posts });
- });
+ Post.find()
+ .sort('-dateAdded')
+ .exec((err, posts) => {
+ if (err) {
+ res.status(500).send(err);
+ }
+ res.json({ posts });
+ });
}
/**
@@ -78,3 +81,85 @@ export function deletePost(req, res) {
});
});
}
+
+/**
+ * Save a post comment
+ * @param req
+ * @param res
+ * @returns void
+ */
+export function addPostComment(req, res) {
+ if (!req.body.comment.author || !req.body.comment.content) {
+ res.status(403).end();
+ }
+
+ const newComment = new Comment(req.body.comment);
+
+ // Let's sanitize inputs
+ newComment.author = sanitizeHtml(newComment.author);
+ newComment.content = sanitizeHtml(newComment.content);
+
+ newComment.slug = slug(newComment.author.toLowerCase(), { lowercase: true });
+ newComment.cuid = cuid();
+ newComment.postId = req.params.cuid;
+
+ newComment.save((err, saved) => {
+ if (err) {
+ res.status(500).send(err);
+ }
+ res.json({ comment: saved });
+ });
+}
+
+/**
+ * Get all comments
+ * @param req
+ * @param res
+ * @returns void
+ */
+export function getPostComments(req, res) {
+ Comment.find({ postId: req.params.cuid })
+ .sort('-dateAdded')
+ .exec((err, comments) => {
+ if (err) {
+ res.status(500).send(err);
+ }
+ res.json({ comments });
+ });
+}
+
+/**
+ * Delete a comment of post
+ * @param req
+ * @param res
+ * @returns void
+ */
+export function deletePostComment(req, res) {
+ Comment.findOne({ cuid: req.params.cuid }).exec((err, comment) => {
+ if (err) {
+ res.status(500).send(err);
+ }
+
+ comment.remove(() => {
+ res.status(200).end();
+ });
+ });
+}
+
+/**
+ * Edit a comment of post
+ * @param req
+ * @param res
+ * @returns void
+ */
+export function editPostComment(req, res) {
+ Comment.updateOne({ cuid: req.params.cuid }, { content: req.body.comment.content }).exec(
+ (err, comment) => {
+ if (err) {
+ res.status(500).send(err);
+ }
+
+ res.json(comment);
+ }
+ );
+}
diff --git a/server/models/comment.js b/server/models/comment.js
new file mode 100644
index 000000000..0f641eb31
--- /dev/null
+++ b/server/models/comment.js
@@ -0,0 +1,13 @@
+import mongoose from 'mongoose';
+const Schema = mongoose.Schema;
+
+const postSchema = new Schema({
+ author: { type: 'String', required: true },
+ content: { type: 'String', required: true },
+ slug: { type: 'String', required: true },
+ cuid: { type: 'String', required: true },
+ postId: { type: 'String', required: true },
+ dateAdded: { type: 'Date', default: Date.now, required: true },
+});
+
+export default mongoose.model('Comment', postSchema);
diff --git a/server/routes/post.routes.js b/server/routes/post.routes.js
index 5d62c3018..6d6b193d5 100644
--- a/server/routes/post.routes.js
+++ b/server/routes/post.routes.js
@@ -14,4 +14,16 @@ router.route('/posts').post(PostController.addPost);
// Delete a post by cuid
router.route('/posts/:cuid').delete(PostController.deletePost);
+// Get all comments of post
+router.route('/posts/:cuid/comments').get(PostController.getPostComments);
+
+// Add a new comment of post
+router.route('/posts/:cuid/comment').post(PostController.addPostComment);
+
+// Edit comment of post
+router.route('/comment/:cuid').put(PostController.editPostComment);
+
+// Delete comment of post
+router.route('/comment/:cuid').delete(PostController.deletePostComment);
+
export default router;