From 12e69d738c68ce04e1a8dd4412e2634122ee1e6f Mon Sep 17 00:00:00 2001 From: Arthur Kalule Date: Fri, 14 Jun 2019 09:21:57 +0300 Subject: [PATCH] feat(comment): add comment on article - add comment reducer and actions tests and implementation - add comment form [starts #165273535] --- src/assets/MainStyle.scss | 15 ++ src/components/Apps/App.test.js | 1 - src/components/Button/index.js | 8 +- src/components/Comment/Comment.scss | 106 +++++++++++ src/components/Comment/Comment.test.js | 54 ++++++ src/components/Comment/CommentBox.js | 55 ++++++ src/components/Comment/CommentBox.test.js | 25 +++ src/components/Comment/CommentForm.js | 110 +++++++++++ src/components/Comment/CommentForm.test.js | 108 +++++++++++ src/components/Comment/DeleteComment.js | 37 ++++ src/components/Comment/DeleteComment.test.js | 55 ++++++ .../__snapshots__/Comment.test.js.snap | 71 +++++++ .../__snapshots__/CommentBox.test.js.snap | 47 +++++ .../__snapshots__/CommentForm.test.js.snap | 31 ++++ .../__snapshots__/DeleteComment.test.js.snap | 26 +++ src/components/Comment/index.js | 104 +++++++++++ .../Login/__snapshots__/login.test.js.snap | 2 + src/components/NavBar/NavBar.test.js | 32 ++++ .../__snapshots__/Settings.test.js.snap | 1 + .../__snapshots__/SignupForm.test.js.snap | 1 + src/store/actions/__mocks__/index.js | 37 +++- .../commentActions/commentActions.test.js | 175 ++++++++++++++++++ src/store/actions/commentActions/index.js | 128 +++++++++++++ src/store/actions/commentTypes.js | 5 + .../reducers/commentReducer/__mocks__.js | 38 ++++ .../commentReducer/commentReducer.test.js | 110 +++++++++++ src/store/reducers/commentReducer/index.js | 50 +++++ src/store/rootReducer.js | 2 + 28 files changed, 1431 insertions(+), 3 deletions(-) create mode 100644 src/components/Comment/Comment.scss create mode 100644 src/components/Comment/Comment.test.js create mode 100644 src/components/Comment/CommentBox.js create mode 100644 src/components/Comment/CommentBox.test.js create mode 100644 src/components/Comment/CommentForm.js create mode 100644 src/components/Comment/CommentForm.test.js create mode 100644 src/components/Comment/DeleteComment.js create mode 100644 src/components/Comment/DeleteComment.test.js create mode 100644 src/components/Comment/__snapshots__/Comment.test.js.snap create mode 100644 src/components/Comment/__snapshots__/CommentBox.test.js.snap create mode 100644 src/components/Comment/__snapshots__/CommentForm.test.js.snap create mode 100644 src/components/Comment/__snapshots__/DeleteComment.test.js.snap create mode 100644 src/components/Comment/index.js create mode 100644 src/store/actions/commentActions/commentActions.test.js create mode 100644 src/store/actions/commentActions/index.js create mode 100644 src/store/actions/commentTypes.js create mode 100644 src/store/reducers/commentReducer/__mocks__.js create mode 100644 src/store/reducers/commentReducer/commentReducer.test.js create mode 100644 src/store/reducers/commentReducer/index.js diff --git a/src/assets/MainStyle.scss b/src/assets/MainStyle.scss index 03491bd..23c1873 100644 --- a/src/assets/MainStyle.scss +++ b/src/assets/MainStyle.scss @@ -16,3 +16,18 @@ $shadow: #D3D6DB; padding: 0; } + +.btn { + background: $white; + padding: 0.2rem 0.5rem; + border: 0.06rem $primary solid; + border-radius: 50rem; + color: $primary; + cursor: pointer; + + &:hover { + background: $primary; + color: $white; + outline: none; + } +} diff --git a/src/components/Apps/App.test.js b/src/components/Apps/App.test.js index 3741ab4..d9b8759 100644 --- a/src/components/Apps/App.test.js +++ b/src/components/Apps/App.test.js @@ -3,7 +3,6 @@ import { Provider } from 'react-redux'; import { shallow, mount } from 'enzyme'; import store from 'store/store'; import { LandingPage } from 'pages/Landing'; -import PageNotFound from 'pages/Error'; import { MemoryRouter } from 'react-router'; import App from './App'; diff --git a/src/components/Button/index.js b/src/components/Button/index.js index 793d309..7378f0f 100644 --- a/src/components/Button/index.js +++ b/src/components/Button/index.js @@ -7,7 +7,7 @@ import PropTypes from 'prop-types'; const Button = (props) => { // Destructure props const { - btnClass, btnName, btnEvent, + btnClass, btnName, btnEvent, disabled, } = props; return ( @@ -16,17 +16,23 @@ const Button = (props) => { className={btnClass} type="button" onClick={btnEvent} + disabled={disabled} > {btnName} ); }; +Button.defaultProps = { + disabled: false, +}; + // Props validation Button.propTypes = { btnName: PropTypes.string.isRequired, btnClass: PropTypes.string.isRequired, btnEvent: PropTypes.func.isRequired, + disabled: PropTypes.bool, }; export default Button; diff --git a/src/components/Comment/Comment.scss b/src/components/Comment/Comment.scss new file mode 100644 index 0000000..30553c7 --- /dev/null +++ b/src/components/Comment/Comment.scss @@ -0,0 +1,106 @@ +@import 'assets/MainStyle.scss'; + +.comment_form { + > form { + width: 100%; + //padding: none; + background: none; + + & textarea { + outline: none; + width: 100%; + border: 0.06rem #2D658623 solid; + background: $white; + border-radius: 0.7rem; + padding: 0.5rem; + height: 4rem; + + &:hover, &:focus { + border: 0.06rem #2D658656 solid; + } + } + + & i { + color: #C21F39; + } + } +} + +.comment { + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-gap: 1.2rem; + //box-shadow: 0 0 0 0.1rem rgba(34, 34, 34, 0.06); + border-radius: 5%; + padding: 0.2rem; + margin-bottom: 0.8rem; + + &__img { + grid-column: 1/ span 1; + grid-row: 1; + justify-self: end; + top: 50%; + width: 4.375rem; + box-shadow: 0 0 0.4rem rgba(0,0,0,.6); + border-radius: 50%; + } + + &__header { + grid-column: 2 /span 3; + grid-row: 1; + display: inline-grid; + grid-template-columns: repeat(3, 1fr); + align-items: center; + border-bottom: 1px solid #E5E5E5; + + &__author { + grid-column: span 1; + grid-row: 1; + color: $primary; + transition: color .3s ease-in-out; + justify-self: start; + + &:hover { + color: #222; + text-decoration: underline; + } + + } + + &__timestamp { + grid-column: 2/span 2; + grid-row: 1; + color: #AEADAD; + justify-self: end; + } + + } + + &__body { + grid-column: 2/span 3; + grid-row: 2; + display: block; + color: rgba(34, 34, 34, 0.64); + } + + &__footer { + grid-column: 2/span 3; + grid-row: 3; + padding-bottom: 0.8rem; + + > .delete { + color: #FF492E; + border: none; + + &:hover { + border: none; + background-color: transparent; + } + + } + + &__actions { + justify-self: end; + } + } +} diff --git a/src/components/Comment/Comment.test.js b/src/components/Comment/Comment.test.js new file mode 100644 index 0000000..af4c512 --- /dev/null +++ b/src/components/Comment/Comment.test.js @@ -0,0 +1,54 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { Comment, mapStateToProps, mapDispatchToProps } from 'components/Comment/'; +import commentTestData from 'store/reducers/commentReducer/__mocks__'; + + +describe('Comment Box', () => { + let wrapper; + const getCommentsFn = jest.fn(); + const deleteCommentFn = jest.fn(); + const props = { + getComments: getCommentsFn, + deleteComment: deleteCommentFn, + comments: commentTestData, + articleSlug: 'javascript-code', + commentsCount: commentTestData.length, + username: 'zack' + }; + + it('should render without crushing', () => { + wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should map state to props', () => { + const mockedState = { + loginReducer: { + user: { + username: 'arthur' + } + }, + commentReducer: { + comments: commentTestData, + commentsCount: commentTestData.length, + } + }; + + const state = mapStateToProps(mockedState); + + expect(state.username).toEqual('arthur'); + expect(state.commentsCount).toBe(2); + }); + + it('should map dispatch to props', () => { + mapDispatchToProps(getCommentsFn).getComments(); + expect(getCommentsFn).toHaveBeenCalled(); + + mapDispatchToProps(deleteCommentFn).deleteComment(); + expect(deleteCommentFn).toHaveBeenCalled(); + }); +}); diff --git a/src/components/Comment/CommentBox.js b/src/components/Comment/CommentBox.js new file mode 100644 index 0000000..563beb2 --- /dev/null +++ b/src/components/Comment/CommentBox.js @@ -0,0 +1,55 @@ +// react +import React from 'react'; + +// Third-party library +import PropTypes from 'prop-types'; + +// Components +import DeleteComment from 'components/Comment/DeleteComment'; + + +const date = timestamp => (new Date(timestamp).toString().slice(0, 25)); +const CommentBox = ( + { + commentId, author: { username, image }, + created_at, body, articleSlug, deleteComment, authenticatedUsername + } +) => ( +
+ author +
+

{username}

+ {date(created_at)} +
+
{body}
+
+ {' '} + {} +
+
+ +
+ +
+); + +// PropType validation +CommentBox.propTypes = { + authenticatedUsername: PropTypes.string.isRequired, + deleteComment: PropTypes.func.isRequired, + articleSlug: PropTypes.string.isRequired, + commentId: PropTypes.number.isRequired, + body: PropTypes.string.isRequired, + created_at: PropTypes.string.isRequired, + author: PropTypes.shape({ + username: PropTypes.string.isRequired, + }).isRequired, +}; + +export default CommentBox; diff --git a/src/components/Comment/CommentBox.test.js b/src/components/Comment/CommentBox.test.js new file mode 100644 index 0000000..3c6173a --- /dev/null +++ b/src/components/Comment/CommentBox.test.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import CommentBox from 'components/Comment/CommentBox'; + + +describe('Comment Box', () => { + let wrapper; + const props = { + author: { username: 'arthur' }, + commentId: 1, + created_at: '2019-06-15T06:02:00.086733Z', + body: 'my first comment', + articleSlug: 'my_article_slug', + deleteComment: jest.fn(), + authenticatedUsername: 'arthur', + }; + + it('should render without crushing', () => { + wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/components/Comment/CommentForm.js b/src/components/Comment/CommentForm.js new file mode 100644 index 0000000..10b8e22 --- /dev/null +++ b/src/components/Comment/CommentForm.js @@ -0,0 +1,110 @@ +// React library +import React, { Component } from 'react'; + +// third party libraries +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +// Actions +import createComment from 'store/actions/commentActions'; + +// Components +import Button from 'components/Button'; + +export class CommentForm extends Component { + constructor(props) { + super(props); + this.state = { + commentBody: '', + error: '', + }; + } + + handleCommentSubmit = (e) => { + e.preventDefault(); + + + const { commentBody } = this.state; + const { createNewComment, articleSlug } = this.props; + + if (articleSlug && commentBody) { + createNewComment(articleSlug, commentBody); + this.setState({ commentBody: '' }); + } + }; + + handleChange = (e) => { + const { name, value } = e.target; + + this.setState({ [name]: value }); + }; + + renderCommentError = error => ( + error ?
{ error }
: '' + ); + + renderCommentSuccess = success => (success ?
Comment added successfully
: ''); + + + render() { + const { commentBody, error } = this.state; + const { success, username } = this.props; + + return ( +
+ {this.renderCommentError(error)} + {this.renderCommentSuccess(success)} + +
+
+