diff --git a/Intl/localizationData/en.js b/Intl/localizationData/en.js
index 79b481d3c..8453c7e2b 100644
--- a/Intl/localizationData/en.js
+++ b/Intl/localizationData/en.js
@@ -17,6 +17,7 @@ export default {
=1 {has # comment}
other {has # comments}
}`,
+ commentContent: 'Comment Content',
HTMLComment: `user {name} {value, plural,
=0 {does not have any comments}
=1 {has # comment}
diff --git a/client/modules/Post/PostActions.js b/client/modules/Post/PostActions.js
index 7e5932fa5..8b5641d56 100644
--- a/client/modules/Post/PostActions.js
+++ b/client/modules/Post/PostActions.js
@@ -4,7 +4,10 @@ import callApi from '../../util/apiCaller';
export const ADD_POST = 'ADD_POST';
export const ADD_POSTS = 'ADD_POSTS';
export const DELETE_POST = 'DELETE_POST';
-
+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 Actions
export function addPost(post) {
return {
@@ -25,6 +28,25 @@ export function addPostRequest(post) {
};
}
+export function addComment(comment) {
+ return {
+ type: ADD_COMMENT,
+ comment,
+ };
+}
+
+export function addCommentRequest(comment) {
+ return (dispatch) => {
+ return callApi('comments', 'post', {
+ comment: {
+ name: comment.name,
+ content: comment.content,
+ postId: comment.postId,
+ },
+ }).then(res => dispatch(addComment(res.comment)));
+ };
+}
+
export function addPosts(posts) {
return {
type: ADD_POSTS,
@@ -40,12 +62,27 @@ export function fetchPosts() {
};
}
+export function addComments(comments) {
+ return {
+ type: ADD_COMMENTS,
+ comments,
+ };
+}
+export function fetchComments(postId) {
+ return (dispatch) => {
+ return callApi(`comments/${postId}`).then(res => {
+ dispatch(addComments(res.comments));
+ });
+ };
+}
+
export function fetchPost(cuid) {
return (dispatch) => {
return callApi(`posts/${cuid}`).then(res => dispatch(addPost(res.post)));
};
}
+
export function deletePost(cuid) {
return {
type: DELETE_POST,
@@ -58,3 +95,33 @@ export function deletePostRequest(cuid) {
return callApi(`posts/${cuid}`, 'delete').then(() => dispatch(deletePost(cuid)));
};
}
+
+export function deleteComment(cuid) {
+ return {
+ type: DELETE_COMMENT,
+ cuid,
+ };
+}
+export function deleteCommentRequest(cuid) {
+ return (dispatch) => {
+ return callApi(`comments/${cuid}`, 'delete').then(() => dispatch(deleteComment(cuid)));
+ };
+}
+
+export function editComment(comment) {
+ return {
+ type: EDIT_COMMENT,
+ comment,
+ };
+}
+
+export function editCommentRequest(comment) {
+ return (dispatch) => {
+ return callApi(`comments/${comment.postId}`, 'post', {
+ comment: {
+ name: comment.name,
+ content: comment.content,
+ },
+ }).then(res => dispatch(editComment(res.comment)));
+ };
+}
diff --git a/client/modules/Post/PostReducer.js b/client/modules/Post/PostReducer.js
index 5a5054369..0004354b9 100644
--- a/client/modules/Post/PostReducer.js
+++ b/client/modules/Post/PostReducer.js
@@ -1,25 +1,47 @@
-import { ADD_POST, ADD_POSTS, DELETE_POST } from './PostActions';
+import { ADD_COMMENT, ADD_COMMENTS, ADD_POST, ADD_POSTS, DELETE_COMMENT, DELETE_POST, EDIT_COMMENT } from './PostActions';
// Initial State
-const initialState = { data: [] };
+const initialState = { data: [], comments: [] };
const PostReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_POST :
return {
+ ...state,
data: [action.post, ...state.data],
};
case ADD_POSTS :
return {
+ ...state,
data: action.posts,
};
+ case ADD_COMMENT :
+ return {
+ ...state,
+ comments: [action.comment, ...state.comments],
+ };
+ case ADD_COMMENTS :
+ return {
+ ...state,
+ comments: action.comments,
+ };
case DELETE_POST :
return {
+ ...state,
data: state.data.filter(post => post.cuid !== action.cuid),
};
-
+ case DELETE_COMMENT :
+ return {
+ ...state,
+ comments: state.comments.filter(comment => comment.cuid !== action.cuid),
+ };
+ case EDIT_COMMENT :
+ return {
+ ...state,
+ comments: state.comments.map((comment) => (comment.cuid === action.comment.cuid ? action.comment : comment)),
+ };
default:
return state;
}
diff --git a/client/modules/Post/components/PostCommentItem/PostCommentItem.css b/client/modules/Post/components/PostCommentItem/PostCommentItem.css
new file mode 100644
index 000000000..a01c36828
--- /dev/null
+++ b/client/modules/Post/components/PostCommentItem/PostCommentItem.css
@@ -0,0 +1,41 @@
+.root {
+ border: 1px solid #ccc;
+ border-radius: 5px;
+ padding: 10px;
+ box-sizing: border-box;
+ -webkit-box-shadow: 0px 5px 5px -4px rgba(0,0,0,0.39);
+ -moz-box-shadow: 0px 5px 5px -4px rgba(0,0,0,0.39);
+ box-shadow: 0px 5px 5px -4px rgba(0,0,0,0.39);
+ margin-bottom: 10px;
+}
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+}
+.headerActions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.headerActions p {
+ cursor: pointer;
+ margin-left: 10px;
+ transition: 0.3s;
+}
+.headerActions p:hover {
+ color: #212121;
+
+}
+
+.name {
+ font-size: 16px;
+ color: darkgray;
+}
+
+.content {
+ font-style: italic;
+}
diff --git a/client/modules/Post/components/PostCommentItem/PostCommentItem.js b/client/modules/Post/components/PostCommentItem/PostCommentItem.js
new file mode 100644
index 000000000..8b5af2711
--- /dev/null
+++ b/client/modules/Post/components/PostCommentItem/PostCommentItem.js
@@ -0,0 +1,34 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+
+import styles from './PostCommentItem.css';
+
+export class PostCommentItem extends PureComponent {
+ render() {
+ const { name, content, deleteComment, editComment, cuid } = this.props;
+ return (
+
-
-
-
{props.post.title}
-
{props.post.name}
-
{props.post.content}
+export class PostDetailPage extends PureComponent {
+ state = {
+ editable: {},
+ }
+ handleAddComment = (name, content, isEdit) => {
+ const postId = this.props.post._id;
+ if (isEdit) {
+ this.props.dispatch(editCommentRequest({ name, content, postId: this.state.editable._id }));
+ } else {
+ this.props.dispatch(addCommentRequest({ name, content, postId }));
+ }
+ };
+
+ handleDeleteComment = (comment) => {
+ this.props.dispatch(deleteCommentRequest(comment));
+ }
+
+ handleEditComment = (comment) => {
+ this.setState({
+ editable: this.props.comments.find(el => el.cuid === comment),
+ });
+ }
+
+ componentDidMount() {
+ this.props.dispatch(fetchComments(this.props.post._id));
+ }
+ render() {
+ const { post, comments } = this.props;
+ return (
+
+
+
+
{post.title}
+
{post.name}
+
{post.content}
+
+ {
+ comments.map((comment, i) =>
+
+ )
+ }
+
+
{isEmpty(this.state.editable) ? 'Add new comment' : 'edit comment'}
+
+
+
-
- );
+ );
+ }
}
// Actions required to provide data for this component to render in server side.
PostDetailPage.need = [params => {
return fetchPost(params.cuid);
-}];
+},
+];
// Retrieve data from store as props
function mapStateToProps(state, props) {
return {
post: getPost(state, props.params.cuid),
+ comments: state.posts.comments,
};
}
@@ -45,7 +95,10 @@ PostDetailPage.propTypes = {
content: PropTypes.string.isRequired,
slug: PropTypes.string.isRequired,
cuid: PropTypes.string.isRequired,
+ _id: PropTypes.string.isRequired,
}).isRequired,
+ dispatch: PropTypes.func.isRequired,
+
};
export default connect(mapStateToProps)(PostDetailPage);
diff --git a/client/store.js b/client/store.js
index b2834c5b4..7f600830e 100644
--- a/client/store.js
+++ b/client/store.js
@@ -4,7 +4,6 @@
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
-
let DevTools;
if (process.env.NODE_ENV === 'development') {
// eslint-disable-next-line global-require
diff --git a/package-lock.json b/package-lock.json
index 14a36b319..a32f76f30 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4840,7 +4840,8 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"aproba": {
"version": "1.2.0",
@@ -4861,12 +4862,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -4881,17 +4884,20 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"core-util-is": {
"version": "1.0.2",
@@ -5008,7 +5014,8 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"ini": {
"version": "1.3.5",
@@ -5020,6 +5027,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -5034,6 +5042,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -5041,12 +5050,14 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@@ -5065,6 +5076,7 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -5145,7 +5157,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"object-assign": {
"version": "4.1.1",
@@ -5157,6 +5170,7 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"wrappy": "1"
}
@@ -5242,7 +5256,8 @@
"safe-buffer": {
"version": "5.1.1",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -5278,6 +5293,7 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -5297,6 +5313,7 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -5340,12 +5357,14 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"yallist": {
"version": "3.0.2",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
}
}
},
diff --git a/server/controllers/comment.controller.js b/server/controllers/comment.controller.js
new file mode 100644
index 000000000..8999ba173
--- /dev/null
+++ b/server/controllers/comment.controller.js
@@ -0,0 +1,89 @@
+import Comment from '../models/comment';
+import cuid from 'cuid';
+import sanitizeHtml from 'sanitize-html';
+/**
+ * Get all comments
+ * @param req
+ * @param res
+ * @returns void
+ */
+export function getComments(req, res) {
+ Comment.find({ postId: req.params.id }).sort('-dateAdded').exec((err, comments) => {
+ if (err) {
+ res.status(500).send(err);
+ }
+ res.json({ comments });
+ });
+}
+
+/**
+ * Save a comment
+ * @param req
+ * @param res
+ * @returns void
+ */
+export function addComment(req, res) {
+ if (!req.body.comment.name || !req.body.comment.content || !req.body.comment.postId) {
+ res.status(403).end();
+ }
+
+ const newComment = new Comment(req.body.comment);
+
+ // Let's sanitize inputs
+ newComment.name = sanitizeHtml(newComment.name);
+ newComment.content = sanitizeHtml(newComment.content);
+ newComment.cuid = cuid();
+ newComment.save((err, saved) => {
+ if (err) {
+ res.status(500).send(err);
+ }
+ res.json({ comment: saved });
+ });
+}
+
+/**
+ * Get a single post
+ * @param req
+ * @param res
+ * @returns void
+ */
+// export function getPost(req, res) {
+// Post.findOne({ cuid: req.params.cuid }).exec((err, post) => {
+// if (err) {
+// res.status(500).send(err);
+// }
+// res.json({ post });
+// });
+// }
+
+/**
+ * Delete a comment
+ * @param req
+ * @param res
+ * @returns void
+ */
+export function deleteComment(req, res) {
+ Comment.findOne({ cuid: req.params.cuid }).exec((err, comment) => {
+ if (err) {
+ res.status(500).send(err);
+ }
+
+ comment.remove(() => {
+ res.status(200).end();
+ });
+ });
+}
+
+
+export async function editComment(req, res) {
+ const { name, content } = req.body.comment;
+ const buffComment = await Comment.findById(req.params.id);
+ const updated = {
+ ...buffComment,
+ name: name || buffComment.name,
+ content: content || buffComment.content,
+ };
+
+ const comment = await Comment.findByIdAndUpdate(req.params.id, { $set: updated }, { useFindAndModify: false, new: true });
+ res.status(200).json({ comment });
+}
diff --git a/server/models/comment.js b/server/models/comment.js
new file mode 100644
index 000000000..058cdce4f
--- /dev/null
+++ b/server/models/comment.js
@@ -0,0 +1,15 @@
+import mongoose from 'mongoose';
+const Schema = mongoose.Schema;
+
+const commentSchema = new Schema({
+ name: { type: 'String', required: true },
+ content: { type: 'String', required: true },
+ cuid: { type: 'String', required: true },
+ postId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'Post',
+ 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..2a8d8132f
--- /dev/null
+++ b/server/routes/comment.routes.js
@@ -0,0 +1,19 @@
+import { Router } from 'express';
+import * as CommentController from '../controllers/comment.controller';
+const router = new Router();
+
+// Get all Comments
+router.route('/:id').get(CommentController.getComments);
+
+// Get one post by cuid
+// router.route('/comments/:cuid').get(PostController.getPost);
+
+// Add a new Post
+router.route('/').post(CommentController.addComment);
+
+// Delete a post by cuid
+router.route('/:cuid').delete(CommentController.deleteComment);
+
+// Edit a post by id
+router.route('/:id').post(CommentController.editComment);
+export default router;
diff --git a/server/routes/index.js b/server/routes/index.js
new file mode 100644
index 000000000..8b0ee40b0
--- /dev/null
+++ b/server/routes/index.js
@@ -0,0 +1,11 @@
+"use strict";
+import {Router} from 'express';
+import postRouter from "./post.routes"
+import commentRouter from "./comment.routes";
+const router = new Router();
+
+
+router.use("/posts", postRouter);
+router.use("/comments", commentRouter);
+
+export default router;
diff --git a/server/routes/post.routes.js b/server/routes/post.routes.js
index 5d62c3018..7b780d254 100644
--- a/server/routes/post.routes.js
+++ b/server/routes/post.routes.js
@@ -3,15 +3,15 @@ import * as PostController from '../controllers/post.controller';
const router = new Router();
// Get all Posts
-router.route('/posts').get(PostController.getPosts);
+router.route('/').get(PostController.getPosts);
// Get one post by cuid
-router.route('/posts/:cuid').get(PostController.getPost);
+router.route('/:cuid').get(PostController.getPost);
// Add a new Post
-router.route('/posts').post(PostController.addPost);
+router.route('/').post(PostController.addPost);
// Delete a post by cuid
-router.route('/posts/:cuid').delete(PostController.deletePost);
+router.route('/:cuid').delete(PostController.deletePost);
export default router;
diff --git a/server/server.js b/server/server.js
index 382249c91..0d76b29db 100644
--- a/server/server.js
+++ b/server/server.js
@@ -45,7 +45,7 @@ import Helmet from 'react-helmet';
// Import required modules
import routes from '../client/routes';
import { fetchComponentData } from './util/fetchData';
-import posts from './routes/post.routes';
+import apiRoutes from './routes';
import dummyData from './dummyData';
import serverConfig from './config';
@@ -70,7 +70,7 @@ app.use(compression());
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', apiRoutes);
// Render Initial HTML
const renderFullPage = (html, initialState) => {