diff --git a/Intl/localizationData/en.js b/Intl/localizationData/en.js
index 79b481d3c..8018a7560 100644
--- a/Intl/localizationData/en.js
+++ b/Intl/localizationData/en.js
@@ -12,6 +12,12 @@ export default {
postTitle: 'Post Title',
postContent: 'Post Content',
submit: 'Submit',
+ cancel: 'Cancel',
+ deleteComment: 'Delete Comment',
+ showComments: 'Show Comments',
+ hideComments: 'Hide Comments',
+ editComment: 'Edit Comment',
+ createNewComment: 'Create New Comment',
comment: `user {name} {value, plural,
=0 {does not have any comments}
=1 {has # comment}
diff --git a/client/modules/Comment/CommentActions.js b/client/modules/Comment/CommentActions.js
new file mode 100644
index 000000000..93a3354bc
--- /dev/null
+++ b/client/modules/Comment/CommentActions.js
@@ -0,0 +1,70 @@
+import callApi from '../../util/apiCaller';
+
+// Export Constants
+export const GET_COMMENTS = 'GET_COMMENTS';
+export const ADD_COMMENT = 'ADD_COMMENT';
+export const DELETE_COMMENT = 'DELETE_COMMENT';
+export const EDIT_COMMENT = 'EDIT_COMMENT';
+
+// Export Actions
+
+export function getComments(comments) {
+ return {
+ type: GET_COMMENTS,
+ comments,
+ };
+}
+
+export function fetchComments(id) {
+ return (dispatch) => {
+ return callApi(`/posts/${id}/comments`).then(res => {
+ dispatch(getComments(res.comments));
+ });
+ };
+}
+
+export function addComment(comment) {
+ return {
+ type: ADD_COMMENT,
+ comment,
+ };
+}
+
+export function addCommentRequest(comment, id) {
+ return (dispatch) => {
+ return callApi(`posts/${id}/comment`, 'post', {
+ comment: {
+ content: comment.content,
+ createdBy: comment.createdBy,
+ },
+ }).then(res => dispatch(addComment(res.comment)));
+ };
+}
+
+
+export function editComment(id, content) {
+ return {
+ type: EDIT_COMMENT,
+ id,
+ content,
+ };
+}
+
+export function editCommentRequest(id, content) {
+ return (dispatch) => {
+ return callApi(`comment/${id}`, 'put', { content }).then(() => dispatch(editComment(id, content)));
+ };
+}
+
+export function deleteComment(id) {
+ return {
+ type: DELETE_COMMENT,
+ id,
+ };
+}
+
+export function deleteCommentRequest(id) {
+ return (dispatch) => {
+ return callApi(`comment/${id}`, 'delete').then(() => dispatch(deleteComment(id)));
+ };
+}
diff --git a/client/modules/Comment/CommentReducer.js b/client/modules/Comment/CommentReducer.js
new file mode 100644
index 000000000..609b89c6e
--- /dev/null
+++ b/client/modules/Comment/CommentReducer.js
@@ -0,0 +1,46 @@
+import { GET_COMMENTS, ADD_COMMENT, DELETE_COMMENT, EDIT_COMMENT } from './CommentActions';
+
+// Initial State
+const initialState = { comments: [] };
+
+const CommentReducer = (state = initialState, action) => {
+ switch (action.type) {
+ case GET_COMMENTS :
+ return {
+ comments: action.comments,
+ };
+
+ case ADD_COMMENT :
+ return {
+ comments: [action.comment, ...state.comments],
+ };
+
+ case EDIT_COMMENT :
+ return {
+ comments: state.comments.map(comment => {
+ if (comment._id === action.id) {
+ const newComment = {
+ content: action.content,
+ postId: comment.postId,
+ createdBy: comment.createdBy,
+ dateAdded: comment.dateAdded,
+ };
+
+ return newComment;
+ }
+ return comment;
+ }),
+ };
+
+ case DELETE_COMMENT :
+ return {
+ comments: state.comments.filter(comment => comment._id !== action.id),
+ };
+
+ default:
+ return state;
+ }
+};
+
+// Export Reducer
+export default CommentReducer;
diff --git a/client/modules/Comment/components/CommentCreateWidget/CommentCreateWidget.css b/client/modules/Comment/components/CommentCreateWidget/CommentCreateWidget.css
new file mode 100644
index 000000000..7d6c370ff
--- /dev/null
+++ b/client/modules/Comment/components/CommentCreateWidget/CommentCreateWidget.css
@@ -0,0 +1,55 @@
+.form {
+ display: block;
+ background: #FAFAFA;
+ padding: 32px 0;
+ border: 1px solid #eee;
+ border-radius: 4px;
+}
+
+.form-content{
+ width: 100%;
+ margin: auto;
+ font-size: 14px;
+}
+
+.form-title{
+ font-size: 16px;
+ font-weight: 700;
+ margin-bottom: 16px;
+ color: #757575;
+}
+
+.form-field{
+ width: 100%;
+ margin-bottom: 12px;
+ font-family: 'Lato', sans-serif;
+ font-size: 13px;
+ line-height: normal;
+ padding: 6px 10px;
+ border-radius: 4px;
+ border: 1px solid #ddd;
+ outline: none;
+ color: #212121;
+}
+
+.form-field-large {
+ min-height: 120px;
+}
+
+.comment-button {
+ display: inline-block;
+ padding: 6px 10px;
+ margin-left: 10px;
+ font-size: 14px;
+ color: #FFF;
+ text-decoration: none;
+ border-radius: 4px;
+}
+
+.comment-submit-button {
+ background: #03A9F4;
+}
+
+.comment-cancel-button {
+ background: #9a533c;
+}
diff --git a/client/modules/Comment/components/CommentCreateWidget/CommentCreateWidget.js b/client/modules/Comment/components/CommentCreateWidget/CommentCreateWidget.js
new file mode 100644
index 000000000..db886b959
--- /dev/null
+++ b/client/modules/Comment/components/CommentCreateWidget/CommentCreateWidget.js
@@ -0,0 +1,49 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import { FormattedMessage } from 'react-intl';
+
+// Import Style
+import styles from './CommentCreateWidget.css';
+
+const CommentCreateWidget = ({ addComment, closeForm }) => {
+ const [authorName, changeAuthor] = useState('');
+ const [content, changeContent] = useState('');
+
+ const addNewComment = () => {
+ addComment(authorName, content);
+ closeForm();
+ };
+
+ return (
+
{
@@ -13,6 +24,13 @@ function PostList(props) {
post={post}
key={post.cuid}
onDelete={() => props.handleDeletePost(post.cuid)}
+ handleAddComment={(authorName, content, id) => props.handleAddComment(authorName, content, id)}
+ handleFetchComments={id => props.handleFetchComments(id)}
+ comments={props.comments}
+ showComments={id => showComments(id)}
+ isCommentsOpened={post.cuid === openedCommentId}
+ handleDeleteComment={id => props.handleDeleteComment(id)}
+ handleEditComment={(id, content) => props.handleEditComment(id, content)}
/>
))
}
@@ -28,7 +46,15 @@ PostList.propTypes = {
slug: PropTypes.string.isRequired,
cuid: PropTypes.string.isRequired,
})).isRequired,
+ comments: PropTypes.arrayOf(PropTypes.shape({
+ content: PropTypes.string.isRequired,
+ createdBy: PropTypes.string.isRequired,
+ })),
handleDeletePost: PropTypes.func.isRequired,
+ handleAddComment: PropTypes.func.isRequired,
+ handleFetchComments: PropTypes.func.isRequired,
+ handleDeleteComment: PropTypes.func.isRequired,
+ handleEditComment: PropTypes.func.isRequired,
};
export default PostList;
diff --git a/client/modules/Post/components/PostListItem/PostListItem.css b/client/modules/Post/components/PostListItem/PostListItem.css
index 49b3345f2..80f5a551e 100644
--- a/client/modules/Post/components/PostListItem/PostListItem.css
+++ b/client/modules/Post/components/PostListItem/PostListItem.css
@@ -16,6 +16,21 @@
color: #616161;
}
+.post-main-line {
+ display: flex;
+ justify-content: space-between;
+}
+
+.comment-create-button {
+ height: fit-content;
+ padding: 5px 10px;
+ font-size: 12px;
+ color: #FFF;
+ background: #03A9F4;
+ text-decoration: none;
+ border-radius: 4px;
+}
+
.author-name{
font-size: 16px;
margin-bottom: 16px;
@@ -28,6 +43,11 @@
margin-bottom: 8px;
}
+.post-actions {
+ display: flex;
+ justify-content: space-between;
+}
+
.post-action a{
color: #555;
text-decoration: none;
@@ -36,6 +56,10 @@
}
.post-action a:hover{
+ color: #03A9F4;
+}
+
+.post-delete a:hover{
color: #EF5350;
}
diff --git a/client/modules/Post/components/PostListItem/PostListItem.js b/client/modules/Post/components/PostListItem/PostListItem.js
index 2925e2199..8e6b2b9bc 100644
--- a/client/modules/Post/components/PostListItem/PostListItem.js
+++ b/client/modules/Post/components/PostListItem/PostListItem.js
@@ -1,26 +1,71 @@
-import React from 'react';
+import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router';
import { FormattedMessage } from 'react-intl';
+import CommentsList from '../../../Comment/components/CommentsList';
+
// Import Style
import styles from './PostListItem.css';
+import CommentCreateWidget from '../../../Comment/components/CommentCreateWidget/CommentCreateWidget';
+
+const PostListItem = props => {
+ const [isCommentAddingFormOpen, openCommentsForm] = useState(false);
+ const currentComments = props.comments && props.comments.filter(el => el.postId === props.post.cuid);
-function PostListItem(props) {
return (
-
+
{props.post.title}
-
{props.post.name}
+
{props.post.content}
-
+
+ {
+ isCommentAddingFormOpen === true && (
+
props.handleAddComment(authorName, content, props.post.cuid)}
+ closeForm={() => openCommentsForm(false)}
+ />
+ )
+ }
+ {
+ props.isCommentsOpened && (
+ props.handleDeleteComment(id)}
+ handleEditComment={(id, content) => props.handleEditComment(id, content)}
+ />
+ )
+ }
);
-}
+};
PostListItem.propTypes = {
post: PropTypes.shape({
@@ -30,7 +75,17 @@ PostListItem.propTypes = {
slug: PropTypes.string.isRequired,
cuid: PropTypes.string.isRequired,
}).isRequired,
+ comments: PropTypes.arrayOf(PropTypes.shape({
+ content: PropTypes.string.isRequired,
+ createdBy: PropTypes.string.isRequired,
+ })).isRequired,
onDelete: PropTypes.func.isRequired,
+ handleAddComment: PropTypes.func.isRequired,
+ handleFetchComments: PropTypes.func.isRequired,
+ showComments: PropTypes.func.isRequired,
+ isCommentsOpened: PropTypes.bool.isRequired,
+ handleDeleteComment: PropTypes.func.isRequired,
+ handleEditComment: PropTypes.func.isRequired,
};
export default PostListItem;
diff --git a/client/modules/Post/pages/PostListPage/PostListPage.js b/client/modules/Post/pages/PostListPage/PostListPage.js
index 7c31229e6..00750d986 100644
--- a/client/modules/Post/pages/PostListPage/PostListPage.js
+++ b/client/modules/Post/pages/PostListPage/PostListPage.js
@@ -8,6 +8,7 @@ import PostCreateWidget from '../../components/PostCreateWidget/PostCreateWidget
// Import Actions
import { addPostRequest, fetchPosts, deletePostRequest } from '../../PostActions';
+import { addCommentRequest, deleteCommentRequest, editCommentRequest, fetchComments } from '../../../Comment/CommentActions';
import { toggleAddPost } from '../../../App/AppActions';
// Import Selectors
@@ -30,11 +31,35 @@ class PostListPage extends Component {
this.props.dispatch(addPostRequest({ name, title, content }));
};
+ handleFetchComments = (id) => {
+ this.props.dispatch(fetchComments(id));
+ };
+
+ handleAddComment = (authorName, content, id) => {
+ this.props.dispatch(addCommentRequest({ createdBy: authorName, content }, id));
+ };
+
+ handleDeleteComment = id => {
+ this.props.dispatch(deleteCommentRequest(id));
+ };
+
+ handleEditComment = (id, content) => {
+ this.props.dispatch(editCommentRequest(id, content));
+ };
+
render() {
return (
-
+
this.handleAddComment(authorName, content, id)}
+ handleFetchComments={id => this.handleFetchComments(id)}
+ handleDeleteComment={id => this.handleDeleteComment(id)}
+ handleEditComment={(id, content) => this.handleEditComment(id, content)}
+ comments={this.props.comments}
+ />
);
}
@@ -48,6 +73,7 @@ function mapStateToProps(state) {
return {
showAddPost: getShowAddPost(state),
posts: getPosts(state),
+ comments: state.comments.comments,
};
}
@@ -57,6 +83,10 @@ PostListPage.propTypes = {
title: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
})).isRequired,
+ comments: PropTypes.arrayOf(PropTypes.shape({
+ content: PropTypes.string.isRequired,
+ createdBy: PropTypes.string.isRequired,
+ })).isRequired,
showAddPost: PropTypes.bool.isRequired,
dispatch: PropTypes.func.isRequired,
};
diff --git a/client/reducers.js b/client/reducers.js
index 2aa143142..9aadfe79d 100644
--- a/client/reducers.js
+++ b/client/reducers.js
@@ -7,10 +7,12 @@ import { combineReducers } from 'redux';
import app from './modules/App/AppReducer';
import posts from './modules/Post/PostReducer';
import intl from './modules/Intl/IntlReducer';
+import comments from './modules/Comment/CommentReducer';
// Combine all reducers into one root reducer
export default combineReducers({
app,
posts,
intl,
+ comments,
});
diff --git a/package.json b/package.json
index f0fa1f581..38e382cc0 100644
--- a/package.json
+++ b/package.json
@@ -43,8 +43,8 @@
"limax": "^1.3.0",
"mongoose": "^4.4.20",
"prop-types": "^15.6.2",
- "react": "^16.4.1",
- "react-dom": "^16.4.1",
+ "react": "16.7.0-alpha.0",
+ "react-dom": "16.7.0-alpha.0",
"react-helmet": "^5.2.0",
"react-intl": "^2.1.2",
"react-redux": "^4.4.5",
diff --git a/server/controllers/comment.controller.js b/server/controllers/comment.controller.js
new file mode 100644
index 000000000..31006192d
--- /dev/null
+++ b/server/controllers/comment.controller.js
@@ -0,0 +1,83 @@
+import sanitizeHtml from 'sanitize-html';
+
+import Comment from '../models/comment';
+
+/**
+ * Get all comments for some post
+ * @param req
+ * @param res
+ * @returns void
+ */
+export const getComments = (req, res) => {
+ Comment.find({ postId: req.params.id })
+ .exec((err, comments) => {
+ if (err) {
+ res.status(500)
+ .send(err);
+ }
+ res.json({ comments });
+ });
+};
+
+/**
+ * Save a comment
+ * @param req
+ * @param res
+ * @returns void
+ */
+export const addComment = (req, res) => {
+ if (!req.body.comment || !req.body.comment.content || !req.body.comment.createdBy) {
+ res.status(403).send();
+ }
+ const newComment = new Comment(req.body.comment);
+
+ newComment.content = sanitizeHtml(newComment.content);
+ newComment.createdBy = sanitizeHtml(newComment.createdBy);
+ newComment.postId = req.params.id;
+
+ newComment.save((error, saved) => {
+ if (error) {
+ res.status(500).send(error);
+ }
+ res.json({ comment: saved });
+ });
+};
+
+/**
+ * Delete a post
+ * @param req
+ * @param res
+ * @returns void
+ */
+export function deleteComment(req, res) {
+ Comment.findOne({ _id: req.params.id })
+ .exec((err, comment) => {
+ if (err || !comment) {
+ res.status(500).send(err);
+ }
+
+ comment.remove(() => {
+ res.status(200).end();
+ });
+ });
+}
+
+/**
+ * Edit a post
+ * @param req
+ * @param res
+ * @returns void
+ */
+export function editComment(req, res) {
+ if (!req.body.content) {
+ res.status(403).send();
+ }
+
+ Comment.updateOne({ _id: req.params.id }, { content: sanitizeHtml(req.body.content) })
+ .exec((err, comment) => {
+ if (err || !comment) {
+ res.status(500).send(err);
+ }
+ res.status(200).end();
+ });
+}
diff --git a/server/models/comment.js b/server/models/comment.js
new file mode 100644
index 000000000..0ec510ad4
--- /dev/null
+++ b/server/models/comment.js
@@ -0,0 +1,11 @@
+import mongoose from 'mongoose';
+const Schema = mongoose.Schema;
+
+const commentSchema = new Schema({
+ content: { type: 'String', required: true },
+ postId: { type: 'String' },
+ createdBy: { type: 'String', required: true },
+ dateAdded: { type: 'Date', default: Date.now, required: true },
+});
+
+export default mongoose.model('Comment', commentSchema);
diff --git a/server/routes/comment.routes.js b/server/routes/comment.routes.js
new file mode 100644
index 000000000..2b5f4a31d
--- /dev/null
+++ b/server/routes/comment.routes.js
@@ -0,0 +1,17 @@
+import { Router } from 'express';
+import * as CommentController from '../controllers/comment.controller';
+const router = new Router();
+
+// Get all Comments for current post
+router.route('/posts/:id/comments').get(CommentController.getComments);
+
+// Add a new Comment
+router.route('/posts/:id/comment').post(CommentController.addComment);
+
+// Delete a Comment
+router.route('/comment/:id').delete(CommentController.deleteComment);
+
+// Edit a Comment
+router.route('/comment/:id').put(CommentController.editComment);
+
+export default router;
diff --git a/server/server.js b/server/server.js
index 382249c91..34f1b613a 100644
--- a/server/server.js
+++ b/server/server.js
@@ -46,6 +46,7 @@ import Helmet from 'react-helmet';
import routes from '../client/routes';
import { fetchComponentData } from './util/fetchData';
import posts from './routes/post.routes';
+import comments from './routes/comment.routes';
import dummyData from './dummyData';
import serverConfig from './config';
@@ -71,6 +72,8 @@ app.use(bodyParser.json({ limit: '20mb' }));
app.use(bodyParser.urlencoded({ limit: '20mb', extended: false }));
app.use(Express.static(path.resolve(__dirname, '../dist/client')));
app.use('/api', posts);
+app.use('/api', comments);
+
// Render Initial HTML
const renderFullPage = (html, initialState) => {