diff --git a/Intl/localizationData/en.js b/Intl/localizationData/en.js
index 79b481d3c..5b4b8ed71 100644
--- a/Intl/localizationData/en.js
+++ b/Intl/localizationData/en.js
@@ -3,12 +3,16 @@ export default {
messages: {
siteTitle: 'MERN Starter Blog',
addPost: 'Add Post',
+ addComment: 'Add comment',
+ singleComment: 'Comment',
+ confirmDeleteComment: 'Are you sure you want to delete this comment?',
switchLanguage: 'Switch Language',
twitterMessage: 'We are on Twitter',
by: 'By',
deletePost: 'Delete Post',
createNewPost: 'Create new post',
authorName: 'Author\'s Name',
+ commentContent: 'Write your comment',
postTitle: 'Post Title',
postContent: 'Post Content',
submit: 'Submit',
diff --git a/Intl/localizationData/fr.js b/Intl/localizationData/fr.js
index 7e5b81b3f..87e3cb833 100644
--- a/Intl/localizationData/fr.js
+++ b/Intl/localizationData/fr.js
@@ -3,12 +3,16 @@ export default {
messages: {
siteTitle: 'MERN blog de démarrage',
addPost: 'Ajouter Poster',
+ addComment: 'Ajouter un commentaire',
+ singleComment: 'Сommentaire',
+ confirmDeleteComment: 'Étes-vous sûr de vouloir supprimer ce commentaire?',
switchLanguage: 'Changer de langue',
twitterMessage: 'Nous sommes sur Twitter',
by: 'Par',
deletePost: 'Supprimer le message',
createNewPost: 'Créer un nouveau message',
authorName: 'Nom de l\'auteur',
+ commentContent: 'Écrivez votre commentaire',
postTitle: 'Titre de l\'article',
postContent: 'Contenu après',
submit: 'Soumettre',
diff --git a/client/modules/Comment/AddCommentWidget/AddCommentWidget.css b/client/modules/Comment/AddCommentWidget/AddCommentWidget.css
new file mode 100644
index 000000000..07da87e69
--- /dev/null
+++ b/client/modules/Comment/AddCommentWidget/AddCommentWidget.css
@@ -0,0 +1,31 @@
+.comment-form {
+ margin-top: 5rem;
+}
+.comment-submit-button {
+ display: inline-block;
+ padding: 8px 16px;
+ font-size: 18px;
+ color: #FFF;
+ background: #03A9F4;
+ text-decoration: none;
+ border-radius: 4px;
+ cursor: pointer;
+}
+.comment-form-title {
+ font-size: 16px;
+ font-weight: 700;
+ margin-bottom: 16px;
+ color: #757575;
+}
+.comment-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;
+}
\ No newline at end of file
diff --git a/client/modules/Comment/AddCommentWidget/AddCommentWidget.jsx b/client/modules/Comment/AddCommentWidget/AddCommentWidget.jsx
new file mode 100644
index 000000000..af9490d06
--- /dev/null
+++ b/client/modules/Comment/AddCommentWidget/AddCommentWidget.jsx
@@ -0,0 +1,61 @@
+import React, { useRef } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
+
+// Import Style
+import styles from './AddCommentWidget.css';
+
+// Import components
+import CommentList from '../CommentList';
+
+// Import actions
+import { addComment } from '../CommentActions';
+
+// Import selectors
+import { getComments } from '../CommentReducer';
+
+const AddCommentWidget = (props) => {
+ const authorRef = useRef();
+ const contentRef = useRef();
+
+ const addNewComment = () => {
+ authorRef.current.focus();
+ contentRef.current.focus();
+
+ if (authorRef.current.value && contentRef.current.value) {
+ props.addComment(authorRef.current.value, contentRef.current.value);
+ authorRef.current.value = contentRef.current.value = '';
+ }
+ };
+
+ const onKeyDownHandler = (e) => {
+ if (e.keyCode === 13) {
+ addNewComment();
+ }
+ };
+
+ return (
+
+
+
+
+
+
addNewComment()} className={styles['comment-submit-button']} href="#">
+
+ );
+};
+
+AddCommentWidget.propTypes = {
+ intl: intlShape.isRequired,
+ comments: PropTypes.object.isRequired,
+ addComment: PropTypes.func.isRequired,
+};
+
+const mapStateToProps = (state) => {
+ return {
+ comments: getComments(state),
+ };
+};
+
+export default injectIntl(connect(mapStateToProps, { addComment })(AddCommentWidget));
diff --git a/client/modules/Comment/CommentActions.js b/client/modules/Comment/CommentActions.js
new file mode 100644
index 000000000..21b62c31a
--- /dev/null
+++ b/client/modules/Comment/CommentActions.js
@@ -0,0 +1,29 @@
+// Export Constants
+export const ADD_COMMENT = 'ADD_COMMENT';
+export const EDIT_COMMENT = 'EDIT_COMMENT';
+export const DELETE_COMMENT = 'DELETE_COMMENT';
+
+// Export Actions
+export const addComment = (author, content) => {
+ return {
+ type: ADD_COMMENT,
+ author,
+ content,
+ };
+};
+
+export const editComment = (author, content, cid) => {
+ return {
+ type: EDIT_COMMENT,
+ author,
+ content,
+ cid,
+ };
+};
+
+export const deleteComment = (cid) => {
+ return {
+ type: DELETE_COMMENT,
+ cid,
+ };
+};
diff --git a/client/modules/Comment/CommentList.jsx b/client/modules/Comment/CommentList.jsx
new file mode 100644
index 000000000..ff8697e7c
--- /dev/null
+++ b/client/modules/Comment/CommentList.jsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+// Import Components
+import CommentListItem from './CommentListItem/CommentListItem';
+
+const CommentList = (props) => {
+ return (
+
+ {
+ props.comments.map(comment => (
+
+ ))
+ }
+
+ );
+};
+//onDelete={() => props.handleDeletePost(post.cuid)}
+CommentList.propTypes = {
+ comments: PropTypes.arrayOf(PropTypes.shape({
+ cid: PropTypes.number.isRequired,
+ author: PropTypes.string.isRequired,
+ content: PropTypes.string.isRequired,
+ })).isRequired,
+};
+
+export default CommentList;
diff --git a/client/modules/Comment/CommentListItem/CommentListItem.css b/client/modules/Comment/CommentListItem/CommentListItem.css
new file mode 100644
index 000000000..3f425d231
--- /dev/null
+++ b/client/modules/Comment/CommentListItem/CommentListItem.css
@@ -0,0 +1,68 @@
+.comment-item-container {
+ margin: 1rem 0 1rem 0;
+ background-color: whitesmoke;
+ padding: 1rem;
+ border-radius: 10px;
+ position: relative;
+}
+
+.author-name {
+ display: flex;
+ margin: 0.5rem 0 0 0.5rem;
+ font-family: 'Lato', sans-serif;
+}
+.comment-content {
+ margin: 0 0 0 0.5rem;
+ font-family: 'Lato', sans-serif;
+ font-size: 14px;
+ color: #424242;
+}
+.comment-delete-button {
+ position: absolute;
+ right: 0;
+ margin: 0.2rem 1rem 0 0;
+ top: 0;
+ font-size: 26px;
+ cursor: pointer;
+}
+.comment-delete-button:hover {
+ color: red;
+}
+.comment-edit-button {
+ position: absolute;
+ right: 0;
+ top: 0;
+ margin: 0.5rem 2.5rem 0 0;
+ font-size: 18px;
+ cursor: pointer;
+}
+.comment-edit-button:hover {
+ color: green;
+}
+.edit-form-field {
+ width: 100%;
+ margin-bottom: 5px;
+ margin-top: 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;
+}
+.edit-button {
+ margin: 0.2rem 1rem 0 0;
+ font-size: 14px;
+ border-radius: 5px;
+ cursor: pointer;
+ padding: 7px;
+ outline: none !important;
+ border: none;
+ background-color: rgb(209, 209, 209);
+ width: 70px;
+}
+.edit-button:hover {
+ background-color: white;
+}
\ No newline at end of file
diff --git a/client/modules/Comment/CommentListItem/CommentListItem.jsx b/client/modules/Comment/CommentListItem/CommentListItem.jsx
new file mode 100644
index 000000000..8d1844919
--- /dev/null
+++ b/client/modules/Comment/CommentListItem/CommentListItem.jsx
@@ -0,0 +1,82 @@
+import React, { useState, useRef } from 'react';
+import PropTypes from 'prop-types';
+import { intlShape, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
+
+// Import Style
+import styles from './CommentListItem.css';
+
+// Import selectors
+import { getComment } from '../CommentReducer';
+
+// Import actions
+import { editComment, deleteComment } from '../CommentActions';
+
+const CommentListItem = (props) => {
+ const [edit, setEdit] = useState(false);
+
+ const editAuthorRef = useRef();
+ const editContentRef = useRef();
+
+ const onClickSaveButton = () => {
+ editAuthorRef.current.focus();
+ editContentRef.current.focus();
+
+ if (editAuthorRef.current.value && editContentRef.current.value) {
+ props.editComment(editAuthorRef.current.value, editContentRef.current.value, props.comment.cid);
+ setEdit(false);
+ }
+ };
+
+ const onClickOnDeleteButton = () => {
+ if (confirm(props.intl.messages.confirmDeleteComment)) { // eslint-disable-line
+ props.deleteComment(props.comment.cid);
+ }
+ };
+
+ return (
+
+ {
+ !edit ?
+
+
#{props.comment.cid}
+
+
+
+
{props.comment.author}
+
+
{props.comment.content}
+
onClickOnDeleteButton()}className={styles['comment-delete-button']}>×
+
setEdit(true)} className={styles['comment-edit-button']}>✎
+
:
+
+
#{props.comment.cid}
+
+
+
+
+
+ }
+
+ );
+};
+
+CommentListItem.propTypes = {
+ comment: PropTypes.shape({
+ author: PropTypes.string.isRequired,
+ content: PropTypes.string.isRequired,
+ cid: PropTypes.number.isRequired,
+ }).isRequired,
+ editComment: PropTypes.func.isRequired,
+ deleteComment: PropTypes.func.isRequired,
+ intl: intlShape.isRequired,
+};
+
+const mapStateToProps = (state, props) => {
+ return {
+ getComment: getComment(state, props.comment.cid),
+ };
+};
+
+export default injectIntl(connect(mapStateToProps, { editComment, deleteComment })(CommentListItem));
diff --git a/client/modules/Comment/CommentReducer.js b/client/modules/Comment/CommentReducer.js
new file mode 100644
index 000000000..6702037ed
--- /dev/null
+++ b/client/modules/Comment/CommentReducer.js
@@ -0,0 +1,56 @@
+import { ADD_COMMENT, EDIT_COMMENT, DELETE_COMMENT } from './CommentActions';
+
+// Initial State
+const initialState = {
+ comments: [{
+ cid: 1,
+ author: 'Alexey',
+ content: 'Hello world!',
+ }],
+};
+
+const CommentReducer = (state = initialState, action) => {
+ switch (action.type) {
+ case ADD_COMMENT:
+ return {
+ comments: [...state.comments, {
+ cid: state.comments.length > 0 ? state.comments[state.comments.length - 1].cid + 1 : 1,
+ author: action.author,
+ content: action.content,
+ }],
+ };
+ case EDIT_COMMENT:
+ return {
+ comments: state.comments.map((comment) => {
+ if (comment.cid === action.cid) {
+ const newComment = {
+ cid: comment.cid,
+ author: action.author,
+ content: action.content,
+ };
+ return newComment;
+ }
+ return comment;
+ }),
+ };
+
+ case DELETE_COMMENT:
+ return {
+ comments: state.comments.filter(comment => comment.cid !== action.cid),
+ };
+
+ default:
+ return state;
+ }
+};
+
+// Selectors
+export const getComments = state => state.comments;
+
+// Get comment by cid
+export const getComment = (state, cid) => {
+ state.comments.comments.filter(comment => comment.cid === cid);
+};
+
+// Export Reducer
+export default CommentReducer;
diff --git a/client/modules/Post/pages/PostDetailPage/PostDetailPage.js b/client/modules/Post/pages/PostDetailPage/PostDetailPage.js
index 32c1e1c11..04fa94829 100644
--- a/client/modules/Post/pages/PostDetailPage/PostDetailPage.js
+++ b/client/modules/Post/pages/PostDetailPage/PostDetailPage.js
@@ -4,6 +4,9 @@ import { connect } from 'react-redux';
import Helmet from 'react-helmet';
import { FormattedMessage } from 'react-intl';
+// Import Components
+import AddCommentWidget from '../../../Comment/AddCommentWidget/AddCommentWidget';
+
// Import Style
import styles from '../../components/PostListItem/PostListItem.css';
@@ -21,6 +24,7 @@ export function PostDetailPage(props) {
{props.post.title}
{props.post.name}
{props.post.content}
+
);
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..df3138d4b 100644
--- a/package.json
+++ b/package.json
@@ -37,20 +37,20 @@
"cross-env": "^1.0.8",
"cuid": "^1.3.8",
"express": "^4.13.4",
- "intl": "^1.2.4",
+ "intl": "^1.2.5",
"intl-locales-supported": "^1.0.0",
"isomorphic-fetch": "^2.2.1",
"limax": "^1.3.0",
"mongoose": "^4.4.20",
"prop-types": "^15.6.2",
- "react": "^16.4.1",
- "react-dom": "^16.4.1",
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1",
"react-helmet": "^5.2.0",
"react-intl": "^2.1.2",
"react-redux": "^4.4.5",
"react-router": "^3.2.1",
"redux": "^3.5.2",
- "redux-thunk": "^2.1.0",
+ "redux-thunk": "^2.3.0",
"sanitize-html": "^1.11.4"
},
"devDependencies": {