diff --git a/src/assets/MainStyle.scss b/src/assets/MainStyle.scss index f88f770..01c9f5e 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.js b/src/components/Apps/App.js index c24ea38..e7d8187 100644 --- a/src/components/Apps/App.js +++ b/src/components/Apps/App.js @@ -5,6 +5,7 @@ import PasswordResetPage from 'pages/PasswordReset'; import PageNotFound from 'pages/Error'; import 'assets/MainStyle.scss'; +import Comment from 'components/Comment'; class App extends Component { render() { @@ -12,6 +13,7 @@ class App extends Component { } /> + } /> } /> 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..19f28ab --- /dev/null +++ b/src/components/Comment/CommentBox.js @@ -0,0 +1,50 @@ +// react +import React from 'react'; +import PropTypes from 'prop-types'; +import DeleteComment from 'components/Comment/DeleteComment'; + + +const date = timestamp => (new Date(timestamp).toString().slice(0, 25)); +const CommentBox = ( + { + id, author: { username, image }, + created_at, body, articleSlug, deleteComment, authenticatedUsername + } +) => ( +
+ author +
+

{username}

+ {date(created_at)} +
+
{body}
+
+ {' '} + {} +
+
+ +
+ +
+); + +CommentBox.propTypes = { + authenticatedUsername: PropTypes.string.isRequired, + deleteComment: PropTypes.func.isRequired, + articleSlug: PropTypes.string.isRequired, + id: 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..d239d70 --- /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' }, + id: 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..38be385 --- /dev/null +++ b/src/components/Comment/CommentForm.js @@ -0,0 +1,107 @@ +import React, { Component } from 'react'; + +// third party libraries +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +// comment actions +import createComment from 'store/actions/commentActions'; +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)} + +
+
+