From 03870b9106449a53a1951c87efca866db16abfb0 Mon Sep 17 00:00:00 2001 From: Gilles Kagarama Date: Wed, 17 Jul 2019 15:53:13 +0200 Subject: [PATCH] [feature #165412889 && #165412878] add article rating && article read time --- .../actions/articles/getPublished.test.js | 12 ++ .../__snapshots__/createRate.test.js.snap | 3 + .../actions/rating/createRate.test.js | 36 ++++++ .../Profile/MyArticles/Published.test.js | 100 +++++++++++++++ .../__snapshots__/Published.test.js.snap | 3 + src/__tests__/components/Routes.test.js | 9 ++ .../reducers/articlesReducers.test.js | 22 +++- .../reducers/rating/ratingReducers.test.js | 42 +++++++ src/actions-types/articlesActionTypes.js | 5 + src/actions-types/index.js | 11 +- src/actions-types/ratingActionsTypes.js | 4 + src/actions/articles/getPublished.js | 11 ++ src/actions/articles/index.js | 4 +- src/actions/index.js | 8 +- src/actions/rating/createRate.js | 12 ++ src/actions/rating/index.js | 3 + src/assets/css/_border.scss | 7 +- src/assets/css/_colors.scss | 6 +- src/assets/images/star.png | Bin 0 -> 410 bytes src/components/Articles/Article/Article.js | 27 +++- src/components/Articles/Article/Rating.js | 119 ++++++++++++++++++ src/components/Articles/Article/Rating.scss | 70 +++++++++++ .../Articles/ListOfArticles/ListOfArticles.js | 4 +- src/components/Articles/Tags/Tags.js | 4 +- src/components/Articles/Tags/Tags.scss | 8 +- .../Header/HeaderUserMenu/HeaderUserMenu.js | 4 +- .../Articles/MyArticles/ArticleMenu.js | 39 ++++++ .../Articles/MyArticles/ArticleMenu.scss | 18 +++ .../Profile/Articles/MyArticles/Published.js | 88 +++++++++++++ .../Profile/Articles/MyArticles/index.js | 3 + .../Articles/PreviewArticle/PreviewArticle.js | 21 +++- src/components/Routes.js | 13 +- src/reducers/articlesReducer.js | 16 +++ src/reducers/index.js | 3 +- src/reducers/rating/createRatingReducer.js | 24 ++++ src/reducers/rating/index.js | 7 ++ src/store/initialState.js | 3 +- src/store/initialStates/ratingInitialState.js | 7 ++ 38 files changed, 747 insertions(+), 29 deletions(-) create mode 100644 src/__tests__/actions/articles/getPublished.test.js create mode 100644 src/__tests__/actions/rating/__snapshots__/createRate.test.js.snap create mode 100644 src/__tests__/actions/rating/createRate.test.js create mode 100644 src/__tests__/components/Profile/MyArticles/Published.test.js create mode 100644 src/__tests__/components/Profile/MyArticles/__snapshots__/Published.test.js.snap create mode 100644 src/__tests__/reducers/rating/ratingReducers.test.js create mode 100644 src/actions-types/ratingActionsTypes.js create mode 100644 src/actions/articles/getPublished.js create mode 100644 src/actions/rating/createRate.js create mode 100644 src/actions/rating/index.js create mode 100644 src/assets/images/star.png create mode 100644 src/components/Articles/Article/Rating.js create mode 100644 src/components/Articles/Article/Rating.scss create mode 100644 src/components/Profile/Articles/MyArticles/ArticleMenu.js create mode 100644 src/components/Profile/Articles/MyArticles/ArticleMenu.scss create mode 100644 src/components/Profile/Articles/MyArticles/Published.js create mode 100644 src/components/Profile/Articles/MyArticles/index.js create mode 100644 src/reducers/rating/createRatingReducer.js create mode 100644 src/reducers/rating/index.js create mode 100644 src/store/initialStates/ratingInitialState.js diff --git a/src/__tests__/actions/articles/getPublished.test.js b/src/__tests__/actions/articles/getPublished.test.js new file mode 100644 index 0000000..7d800b5 --- /dev/null +++ b/src/__tests__/actions/articles/getPublished.test.js @@ -0,0 +1,12 @@ +import { getPublished } from '../../../actions/articles'; +import article from '../../../__mocks__/article'; + +const dispatch = jest.fn(action => action); + +describe('published article articles', () => { + it('returns publish information', async () => { + const result = getPublished()(dispatch); + expect(result).toHaveProperty('type'); + expect(result).toHaveProperty('payload'); + }); +}); diff --git a/src/__tests__/actions/rating/__snapshots__/createRate.test.js.snap b/src/__tests__/actions/rating/__snapshots__/createRate.test.js.snap new file mode 100644 index 0000000..08d8642 --- /dev/null +++ b/src/__tests__/actions/rating/__snapshots__/createRate.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render a component 1`] = `ShallowWrapper {}`; diff --git a/src/__tests__/actions/rating/createRate.test.js b/src/__tests__/actions/rating/createRate.test.js new file mode 100644 index 0000000..6e501ca --- /dev/null +++ b/src/__tests__/actions/rating/createRate.test.js @@ -0,0 +1,36 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'react-thunk'; +import { Provider } from 'react-redux'; +// import article from '../../../__mocks__/article'; +import { createRate } from '../../../actions/rating'; +import { Rating as RatingComponent } from '../../../components/Articles/Article/Rating'; +import { shallow, mount } from '../../../../config/enzymeConfig'; + +describe('', () => { + const props = { + slug: 'slug-slug', + rating: 1, + errors: { error: ['12'] }, + createRate: jest.fn(), + fetchOneArticle: jest.fn() + }; + const rating = { + slug: 'slug-slug', + rating: 1 + }; + const component = shallow(); + it('should render a component ', () => { + expect(component).toMatchSnapshot(); + }); + const dispatch = jest.fn(action => action); + + describe('Create rating', () => { + test('returns rating information', async () => { + const result = createRate(rating)(dispatch); + expect(result).toHaveProperty('type'); + expect(result).toHaveProperty('payload'); + // expect(result.payload).toEqual(rating); + }); + }); +}); diff --git a/src/__tests__/components/Profile/MyArticles/Published.test.js b/src/__tests__/components/Profile/MyArticles/Published.test.js new file mode 100644 index 0000000..a66b5ff --- /dev/null +++ b/src/__tests__/components/Profile/MyArticles/Published.test.js @@ -0,0 +1,100 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'react-thunk'; +import { Provider } from 'react-redux'; +import { MemoryRouter } from 'react-router-dom'; +import URL from '../../../../__mocks__/URL'; +import articles from '../../../../__mocks__/articles'; +import { mockStore, initialState } from '../../../../__mocks__/store'; +import { PublishedArticles as PublishedArticlesComponent } from '../../../../components/Profile/Articles/MyArticles/Published'; +import { mount, shallow } from '../../../../../config/enzymeConfig'; + +const props = { + errors: {}, + article: { + title: 'Hello John Doe', + description: 'John Doe, Mocker', + body: 'body of the article', + slug: 'slug-slug-slug' + }, + message: { message: 'Published' }, + fetchOneArticle: jest.fn(), + history: {}, + match: { params: { slug: 'slug-slug-slug' } }, + publishArticle: jest.fn(), + unpublishArticle: jest.fn(), + deleteArticle: jest.fn(), + fileSelectedHandler: jest.fn(), + createObjectURL: jest.fn(), + getPublished: jest.fn() +}; +const store = mockStore({ + ...initialState, + articles: { articles }, + getPublished: jest.fn(true) +}); +const state = { + article: { + title: 'Hello John Doe', + description: 'John Doe, Mocker', + body: JSON.stringify({ + blocks: [ + { + key: 'cnu26', + text: 'test componentWillReceiveProps failedtest componentWillReceiveProps failed', + type: 'unstyled', + depth: 0, + inlineStyleRanges: [ + { offset: 0, length: 74, style: 'color-rgb(36,41,46)' }, + { offset: 0, length: 74, style: 'bgcolor-rgb(255,255,255)' }, + { offset: 0, length: 74, style: 'fontsize-32' }, + { + offset: 0, + length: 74, + style: + 'fontfamily--apple-system, system-ui, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol' + } + ], + entityRanges: [], + data: { 'text-align': 'start' } + }, + { + key: 'emuik', + text: 'Okey', + type: 'unstyled', + depth: 0, + inlineStyleRanges: [ + { offset: 0, length: 4, style: 'color-rgb(36,41,46)' }, + { offset: 0, length: 4, style: 'bgcolor-rgb(255,255,255)' }, + { offset: 0, length: 4, style: 'fontsize-32' }, + { + offset: 0, + length: 4, + style: + 'fontfamily--apple-system, system-ui, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol' + } + ], + entityRanges: [], + data: {} + } + ], + entityMap: {} + }), + slug: 'slug-slug-slug' + }, + message: { message: 'Published' }, + getPublished: jest.fn() +}; +describe('', () => { + const component = shallow(); + it('should render a component ', () => { + const component = mount( + + + + ); + }); + it('should trigger publish ', () => { + component.setProps({ article: state.article }); + }); +}); diff --git a/src/__tests__/components/Profile/MyArticles/__snapshots__/Published.test.js.snap b/src/__tests__/components/Profile/MyArticles/__snapshots__/Published.test.js.snap new file mode 100644 index 0000000..04b13e7 --- /dev/null +++ b/src/__tests__/components/Profile/MyArticles/__snapshots__/Published.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render a component 1`] = `ShallowWrapper {}`; diff --git a/src/__tests__/components/Routes.test.js b/src/__tests__/components/Routes.test.js index df1aff2..b1555b0 100644 --- a/src/__tests__/components/Routes.test.js +++ b/src/__tests__/components/Routes.test.js @@ -16,6 +16,7 @@ import Article from '../../components/Articles/Article/Article'; import CreateArticle from '../../components/Profile/Articles/CreateArticle'; import EditArticle from '../../components/Profile/Articles/EditArticle'; import PreviewArticle from '../../components/Profile/Articles/PreviewArticle'; +import PublishedArticles from '../../components/Profile/Articles/MyArticles/Published'; describe('', () => { test('renders without crashing', () => { @@ -147,4 +148,12 @@ describe('', () => { ); expect(component.find(PreviewArticle).length).toBe(1); }); + test('renders without crashing', () => { + const component = mount( + + + + ); + expect(component.find(PublishedArticles).length).toBe(1); + }); }); diff --git a/src/__tests__/reducers/articlesReducers.test.js b/src/__tests__/reducers/articlesReducers.test.js index b2c7929..30629de 100644 --- a/src/__tests__/reducers/articlesReducers.test.js +++ b/src/__tests__/reducers/articlesReducers.test.js @@ -130,7 +130,27 @@ describe('Articles reducers', () => { }); expect(reducer.errors).toBeInstanceOf(Object); }); - + it('FETCH_MY_PUBLISHED_ARTICLES_START', () => { + const reducer = articlesReducer(initialState, { + type: articlesType.FETCH_MY_PUBLISHED_ARTICLES_START, + payload: [] + }); + expect(reducer.errors).toBeInstanceOf(Object); + }); + it('FETCH_MY_PUBLISHED_ARTICLES_SUCCESS', () => { + const reducer = articlesReducer(initialState, { + type: articlesType.FETCH_MY_PUBLISHED_ARTICLES_SUCCESS, + payload: { articles } + }); + expect(reducer.articles).toBeInstanceOf(Array); + }); + it('FETCH_MY_PUBLISHED_ARTICLES_FAILURE', () => { + const reducer = articlesReducer(initialState, { + type: articlesType.FETCH_MY_PUBLISHED_ARTICLES_FAILURE, + payload: { errors } + }); + expect(reducer.errors).toBeInstanceOf(Object); + }); it('DEFAULT', () => { articlesReducer(initialState, { type: null, diff --git a/src/__tests__/reducers/rating/ratingReducers.test.js b/src/__tests__/reducers/rating/ratingReducers.test.js new file mode 100644 index 0000000..2d22a27 --- /dev/null +++ b/src/__tests__/reducers/rating/ratingReducers.test.js @@ -0,0 +1,42 @@ +import createRatingReducer from '../../../reducers/rating/createRatingReducer'; +import initialState from '../../../store/initialStates/ratingInitialState'; +import { ratingActionsTypes } from '../../../actions-types'; +// import articles from '../../__mocks__/articles'; +// import article from '../../__mocks__/article'; +// import message from '../../__mocks__/articleMessage'; +// import errors from '../../__mocks__/errors'; + +const rating = { + slug: 'slug-slug', + rating: 1 +}; + +describe('Rating reducers', () => { + it('CREATE_RATING_START', () => { + const reducer = createRatingReducer(initialState, { + type: ratingActionsTypes.CREATE_RATING_START, + payload: {} + }); + expect(reducer.createRate).toBeInstanceOf(Object); + }); + it('CREATE_RATING_SUCCESS', () => { + const reducer = createRatingReducer(initialState, { + type: ratingActionsTypes.CREATE_RATING_SUCCESS, + payload: { rating } + }); + expect(reducer.createRate).toHaveProperty('errors'); + }); + it('CREATE_RATING_FAILURE', () => { + const reducer = createRatingReducer(initialState, { + type: ratingActionsTypes.CREATE_RATING_FAILURE, + payload: {} + }); + expect(reducer.createRate).toHaveProperty('errors'); + }); + it('DEFAULT', () => { + createRatingReducer(initialState, { + type: null, + payload: null + }); + }); +}); diff --git a/src/actions-types/articlesActionTypes.js b/src/actions-types/articlesActionTypes.js index 2652110..82cbd7b 100644 --- a/src/actions-types/articlesActionTypes.js +++ b/src/actions-types/articlesActionTypes.js @@ -32,3 +32,8 @@ export const PUBLISH_ARTICLE_START = 'PUBLISH_ARTICLE_START'; export const PUBLISH_ARTICLE_END = 'PUBLISH_ARTICLE_END'; export const PUBLISH_ARTICLE_SUCCESS = 'PUBLISH_ARTICLE_SUCCESS'; export const PUBLISH_ARTICLE_FAILURE = 'PUBLISH_ARTICLE_FAILURE'; + +export const FETCH_MY_PUBLISHED_ARTICLES_START = 'FETCH_MY_ARTICLES_START'; +export const FETCH_MY_PUBLISHED_ARTICLES_END = 'FETCH_MY_ARTICLES_END'; +export const FETCH_MY_PUBLISHED_ARTICLES_SUCCESS = 'FETCH_MY_ARTICLES_SUCCESS'; +export const FETCH_MY_PUBLISHED_ARTICLES_FAILURE = 'FETCH_MY_ARTICLES_FAILURE'; diff --git a/src/actions-types/index.js b/src/actions-types/index.js index 51c3f5d..aa6a879 100644 --- a/src/actions-types/index.js +++ b/src/actions-types/index.js @@ -3,6 +3,13 @@ import * as apiActionsTypes from './apiActionsTypes'; import * as notificationActionTypes from './notificationActionTypes'; import * as articlesType from './articlesActionTypes'; import * as imagesTypes from './imagesActionTypes'; +import * as ratingActionsTypes from './ratingActionsTypes'; -export { articlesType, imagesTypes }; -export { apiActionsTypes, userActionsTypes, notificationActionTypes }; +export { + articlesType, + imagesTypes, + apiActionsTypes, + userActionsTypes, + notificationActionTypes, + ratingActionsTypes +}; diff --git a/src/actions-types/ratingActionsTypes.js b/src/actions-types/ratingActionsTypes.js new file mode 100644 index 0000000..7df73e1 --- /dev/null +++ b/src/actions-types/ratingActionsTypes.js @@ -0,0 +1,4 @@ +export const CREATE_RATING_START = 'CREATE_RATING_START'; +export const CREATE_RATING_END = 'CREATE_RATING_END'; +export const CREATE_RATING_SUCCESS = 'CREATE_RATING_SUCCESS'; +export const CREATE_RATING_FAILURE = 'CREATE_RATING_FAILURE'; diff --git a/src/actions/articles/getPublished.js b/src/actions/articles/getPublished.js new file mode 100644 index 0000000..228273f --- /dev/null +++ b/src/actions/articles/getPublished.js @@ -0,0 +1,11 @@ +import { articlesType } from '../../actions-types'; +import { apiAction } from '../../helpers'; + +export const getPublished = () => dispatch => dispatch(apiAction({ + method: 'get', + url: '/articles/published', + onStart: articlesType.FETCH_MY_PUBLISHED_ARTICLES_START, + onEnd: articlesType.FETCH_MY_PUBLISHED_ARTICLES_END, + onSuccess: articlesType.FETCH_MY_PUBLISHED_ARTICLES_SUCCESS, + onFailure: articlesType.FETCH_MY_PUBLISHED_ARTICLES_FAILURE +})); diff --git a/src/actions/articles/index.js b/src/actions/articles/index.js index 6e6f22d..7101744 100644 --- a/src/actions/articles/index.js +++ b/src/actions/articles/index.js @@ -5,6 +5,7 @@ import { getAllArticles } from './getAllArticles'; import { deleteArticle } from './deleteArticle'; import { publishArticle } from './publishArticle'; import { unpublishArticle } from './unpublishArticle'; +import { getPublished } from './getPublished'; export { createPost, @@ -13,5 +14,6 @@ export { publishArticle, unpublishArticle, getAllArticles, - fetchOneArticle + fetchOneArticle, + getPublished }; diff --git a/src/actions/index.js b/src/actions/index.js index 26b276f..6e24651 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -7,8 +7,10 @@ import { editPost, deleteArticle, publishArticle, - unpublishArticle + unpublishArticle, + getPublished } from './articles'; +import { createRate } from './rating'; import { uploadImage } from './images'; export { @@ -21,5 +23,7 @@ export { deleteArticle, publishArticle, unpublishArticle, - uploadImage + uploadImage, + getPublished, + createRate }; diff --git a/src/actions/rating/createRate.js b/src/actions/rating/createRate.js new file mode 100644 index 0000000..26f2496 --- /dev/null +++ b/src/actions/rating/createRate.js @@ -0,0 +1,12 @@ +import { ratingActionsTypes } from '../../actions-types'; +import { apiAction } from '../../helpers'; + +export const createRate = (slug, rating) => dispatch => dispatch(apiAction({ + data: { rating }, + method: 'post', + url: `/rating/${slug}/article`, + onStart: ratingActionsTypes.CREATE_RATING_START, + onEnd: ratingActionsTypes.CREATE_RATING_END, + onSuccess: ratingActionsTypes.CREATE_RATING_SUCCESS, + onFailure: ratingActionsTypes.CREATE_RATING_FAILURE +})); diff --git a/src/actions/rating/index.js b/src/actions/rating/index.js new file mode 100644 index 0000000..96703cf --- /dev/null +++ b/src/actions/rating/index.js @@ -0,0 +1,3 @@ +import { createRate } from './createRate'; + +export { createRate }; diff --git a/src/assets/css/_border.scss b/src/assets/css/_border.scss index ab0f7c0..dd83f87 100644 --- a/src/assets/css/_border.scss +++ b/src/assets/css/_border.scss @@ -2,7 +2,6 @@ .border { border: 1px solid; } - .b-light { border-color: $light; } @@ -29,6 +28,12 @@ .b-primary { border-color: $primary; } +.b-danger { + border-color: $danger; +} +.b-light-grey { + border-color: $light-grey; +} .b-bottom-light-grey { border-bottom: 1px solid $light-grey; } diff --git a/src/assets/css/_colors.scss b/src/assets/css/_colors.scss index f5d46d8..8df61e0 100644 --- a/src/assets/css/_colors.scss +++ b/src/assets/css/_colors.scss @@ -7,8 +7,7 @@ $secondary: #1843b8; $danger: #d64c4c; $success: #1c8a4a; $info: #1cbdee; -$light-grey: #ccc; -$light-grey: #e6e6e6; +$light-grey: #cbcbcb; $yellow: #f9d342; /* background anim colors */ $anim-palette-1: #ffb426; @@ -47,6 +46,9 @@ $twitter-color: #50abf1; .light-grey { background: $light-grey; } +.text-light-grey { + color: $light-grey; +} .text-grey { color: $grey; } diff --git a/src/assets/images/star.png b/src/assets/images/star.png new file mode 100644 index 0000000000000000000000000000000000000000..6267daa4e42fe85b9aceb74baffe52addee46150 GIT binary patch literal 410 zcmV;L0cHM)P)Cm;G_VFbu^-7a-jrBd|fUN;hBxM#u&=lQc{KBcu~F zo#6F+6>YFATXD?sN8g<;KH2h*r+oO>002EePmh4oO7I6qk=C^Om9KAN#-LL07ZEi) zo``5GR%BG#>RuU}RpQvYZP_WY^Kq%`636`vN<3cdy2SBw048?*DOM)5)bTL_CC(j1 zHYM0J0vG_azClW~yk@lQ7`FPv4~mb=>{_B7KZMt-kC)$lWEej!d~?eDjL$sRDz#GL z1-n%TpaF0YGrA63)2S;AACMlvB(ix;T&wDz{dXdwZz$Vhk9_AXH2?qr07*qoM6N<$ Ef}WhRVE_OC literal 0 HcmV?d00001 diff --git a/src/components/Articles/Article/Article.js b/src/components/Articles/Article/Article.js index 406b8f1..9d3b024 100644 --- a/src/components/Articles/Article/Article.js +++ b/src/components/Articles/Article/Article.js @@ -6,18 +6,22 @@ import 'dotenv/config'; import MetaTags from 'react-meta-tags'; import LazyLoad from 'react-lazyload'; import { Editor, EditorState, convertFromRaw } from 'draft-js'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faClock } from '@fortawesome/free-solid-svg-icons'; import { fetchOneArticle } from '../../../actions'; import avatar from '../../../assets/images/user.png'; import timeStamp from '../../../helpers/timeStamp'; import { NotFound } from '../../common'; import Layout from '../../Layout'; import './Article.scss'; +import Rating from './Rating'; const { REACT_APP_IMAGE_BASE_URL } = process.env; export class Article extends Component { state = { article: {}, + loaded: false, imageRectangle: 'c_fill,g_auto,h_350,w_970/b_rgb:000000,e_gradient_fade,y_-0.20/c_scale,co_rgb:ffffff', editorState: EditorState.createEmpty() @@ -31,7 +35,7 @@ export class Article extends Component { const editorState = article.body ? EditorState.createWithContent(convertFromRaw(JSON.parse(article.body))) : EditorState.createEmpty(); - + this.setState({ loaded: true }); return this.setState({ article, editorState }); } @@ -41,7 +45,7 @@ export class Article extends Component { } render() { - const { imageRectangle, article, editorState } = this.state; + const { imageRectangle, article, editorState, loaded } = this.state; return (
@@ -86,6 +90,14 @@ export class Article extends Component { Follow {timeStamp(article.createdAt)} + + + {' '} + {article.readTime} min read + +
+
+
@@ -99,7 +111,7 @@ export class Article extends Component { ) : ( -
{!Object.keys(article).length ? :
{''}
}
+
{loaded && !Object.keys(article).length ? :
{''}
}
)}
@@ -115,11 +127,16 @@ Article.propTypes = { editorState: PropTypes.func, match: PropTypes.object, slug: PropTypes.string, + rating: PropTypes.number, params: PropTypes.object, message: PropTypes.object, - errors: PropTypes.object + errors: PropTypes.object, + loaded: PropTypes.bool }; -const mapStateToProps = ({ articles: { article, errors } }) => ({ article, errors }); +const mapStateToProps = ({ articles: { article, errors } }) => ({ + article, + errors +}); export default connect( mapStateToProps, diff --git a/src/components/Articles/Article/Rating.js b/src/components/Articles/Article/Rating.js new file mode 100644 index 0000000..75998c3 --- /dev/null +++ b/src/components/Articles/Article/Rating.js @@ -0,0 +1,119 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { Link } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import { createRate, fetchOneArticle } from '../../../actions'; +import './Rating.scss'; + +export class Rating extends Component { + state = { rating: 0, slug: '' }; + + componentDidMount() { + this.setState({ rating: this.props.rating }); + } + + componentWillReceiveProps(nextProps) { + const { errors, message } = nextProps; + if (nextProps.message) { + this.setState({ message }); + } + if (nextProps.errors) { + this.setState({ errors }); + } + this.setState({ loaded: true }); + } + + async getSpecificArticle(slug) { + const { fetchOneArticle } = this.props; + await fetchOneArticle(slug); + + const { article } = this.props; + this.setState({ rating: article.rating }); + } + + submitRate = async (rating) => { + const { isAuth } = this.props; + if (isAuth) { + await this.props.createRate(this.props.slug, rating); + const { slug } = this.props; + this.getSpecificArticle(slug); + } else { + this.setState({ errors: { token: 'Failed to authenticate token' } }); + } + }; + + closeMessage = () => { + this.setState({ message: '' }); + this.setState({ errors: '' }); + }; + + ratingStars = () => ( +
+ Rating +
+
+ {Array.from(Array(5), (e, i) => ( +
this.submitRate(i + 1)} + key={i + 1} + className={this.state.rating > i ? 'one-star' : 'one-star'} + > + {this.state.rating > i} +
+ ))} +
+ {this.state.rating} +
+ ); + + render() { + const { errors, message } = this.state; + return ( + + {this.ratingStars()} + {(errors && Object.keys(errors).length && !errors.token) + || (message && Object.keys(message).length) ? ( + + {errors.rating || message} + this.closeMessage()}>Close + + ) : ( + '' + )} + {errors && errors.token ? ( + + Login first to perform this action.{' '} + + Login + + this.closeMessage()}>Close + + ) : ( + '' + )} + + ); + } +} + +Rating.propTypes = { + rating: PropTypes.any, + errors: PropTypes.any, + slug: PropTypes.string, + ratingStars: PropTypes.func, + createRate: PropTypes.func, + message: PropTypes.string, + fetchOneArticle: PropTypes.func.isRequired, + article: PropTypes.object, + isAuth: PropTypes.bool +}; +const mapStateToProps = ({ + user: { isAuth }, + articles: { article }, + rating: { createRate: { loading, message, errors } } +}) => ({ createRate, loading, message, errors, article, isAuth }); + +export default connect( + mapStateToProps, + { createRate, fetchOneArticle } +)(Rating); diff --git a/src/components/Articles/Article/Rating.scss b/src/components/Articles/Article/Rating.scss new file mode 100644 index 0000000..269fd41 --- /dev/null +++ b/src/components/Articles/Article/Rating.scss @@ -0,0 +1,70 @@ +@import '../../../assets/css/_pixels'; +@import '../../../assets/css/_colors'; +@import '../../../assets/css/_border'; +@import '../../../assets/css/_radius'; +@import '../../../assets/css/_shadow'; +#wrap-ratings { + position: relative; + .wrap-stars { + height: 30px; + float: left; + position: relative; + background: #dedede; + } + .rating-percent { + position: absolute; + content: ''; + top: 0; + left: 0; + height: 30px; + z-index: 1; + @extend .primary; + } + .wrap-stars .one-star { + height: 30px; + width: 30px; + position: relative; + z-index: 2; + float: left; + cursor: pointer; + background-image: url('../../../assets/images/star.png'); + background-repeat: no-repeat; + background-position: 0 0; + background-size: 100% 100%; + } + i { + margin: auto 10px; + padding: $small/2 $medium; + width: 30px; + text-align: center; + background: #42413e; + color: #ffffff; + border-radius: 3px; + font-style: normal !important; + float: left; + } + .rating-errors { + position: absolute; + top: 40px; + color: $grey; + right: $small; + padding: $small $medium; + display: block; + background: $white; + z-index: 1500000000; + @extend .border, .b-grey, .radius-1, .shadow-2; + } + .rating-errors i { + content: 'Close'; + cursor: pointer; + text-align: center; + color: $white !important; + background: $black; + font-size: 10px; + position: absolute; + top: 10px; + padding: 7px; + right: -40px; + height: 10px; + } +} diff --git a/src/components/Articles/ListOfArticles/ListOfArticles.js b/src/components/Articles/ListOfArticles/ListOfArticles.js index fd64fdb..a74cfe0 100644 --- a/src/components/Articles/ListOfArticles/ListOfArticles.js +++ b/src/components/Articles/ListOfArticles/ListOfArticles.js @@ -50,7 +50,9 @@ export class ListsOfArticles extends Component {
{article.description}
- {article.author.username} {timeStamp(article.createdAt)}{' '} + {article.author ? article.author.username : ''}{' '} + {timeStamp(article.createdAt)} + {article.readTime} min read
diff --git a/src/components/Articles/Tags/Tags.js b/src/components/Articles/Tags/Tags.js index 973e8ab..0bdaef3 100644 --- a/src/components/Articles/Tags/Tags.js +++ b/src/components/Articles/Tags/Tags.js @@ -8,9 +8,9 @@ export default class Tags extends Component { render() { const { page } = this.state; return ( -
+
-
    +
    • Culture
    • diff --git a/src/components/Articles/Tags/Tags.scss b/src/components/Articles/Tags/Tags.scss index 2492e3a..aa80487 100644 --- a/src/components/Articles/Tags/Tags.scss +++ b/src/components/Articles/Tags/Tags.scss @@ -1,16 +1,16 @@ @import '../../../assets/css/_colors.scss'; @import '../../../assets/css/_pixels.scss'; -.grabTags { +.grab-tags { border-bottom: 1px solid $grey; } -.tagsMenu { +.tags-menu { padding: ($xsmall * 1.5) 0; } -.tagsMenu li { +.tags-menu li { padding: auto 0.4em; } -.tagsMenu a { +.tags-menu a { padding: 0.5em; font-size: 1em; } diff --git a/src/components/Header/HeaderUserMenu/HeaderUserMenu.js b/src/components/Header/HeaderUserMenu/HeaderUserMenu.js index 15c0c22..9d89e90 100644 --- a/src/components/Header/HeaderUserMenu/HeaderUserMenu.js +++ b/src/components/Header/HeaderUserMenu/HeaderUserMenu.js @@ -54,8 +54,8 @@ class HeaderUserMenu extends Component { {isAuth && (
    • - - My stories + + My Articles
    • )} diff --git a/src/components/Profile/Articles/MyArticles/ArticleMenu.js b/src/components/Profile/Articles/MyArticles/ArticleMenu.js new file mode 100644 index 0000000..ce4fce8 --- /dev/null +++ b/src/components/Profile/Articles/MyArticles/ArticleMenu.js @@ -0,0 +1,39 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faPlus } from '@fortawesome/free-solid-svg-icons'; +import './ArticleMenu.scss'; + +const ArticleMenu = () => ( +
      +
      +
        +
      • + + Create New + +
      • +
      • + Published +
      • +
      • + Drafts +
      • +
      • + Comments +
      • +
      • + Highlights +
      • +
      • + Bookmarks +
      • +
      • + favorites +
      • +
      +
      +
      +); + +export default ArticleMenu; diff --git a/src/components/Profile/Articles/MyArticles/ArticleMenu.scss b/src/components/Profile/Articles/MyArticles/ArticleMenu.scss new file mode 100644 index 0000000..fc8a187 --- /dev/null +++ b/src/components/Profile/Articles/MyArticles/ArticleMenu.scss @@ -0,0 +1,18 @@ +@import '../../../../assets/css/colors'; +@import '../../../../assets/css/pixels'; +.grab-article-menu { + border-bottom: 1px solid $light; + + .list-inline { + padding: 0; + } + .list-inline li { + padding: 0.5em 0.7em; + } + .list-inline li:nth-child(1) { + padding-left: 0 !important; + } + .list-inline a { + font-size: 1em; + } +} diff --git a/src/components/Profile/Articles/MyArticles/Published.js b/src/components/Profile/Articles/MyArticles/Published.js new file mode 100644 index 0000000..b6a599b --- /dev/null +++ b/src/components/Profile/Articles/MyArticles/Published.js @@ -0,0 +1,88 @@ +import React, { Component } from 'react'; +import { Link } from 'react-router-dom'; +import { connect } from 'react-redux'; +import 'dotenv/config'; +import PropTypes from 'prop-types'; +import { getPublished } from '../../../../actions'; +import placeholder from '../../../../assets/images/placeholder.png'; +import timeStamp from '../../../../helpers/timeStamp'; +import { Img } from '../../../common'; +import Layout from '../../../Layout'; +import ArticleMenu from './ArticleMenu'; + +const { REACT_APP_IMAGE_BASE_URL } = process.env; +export class PublishedArticles extends Component { + state = { imageRectangle: 'w_400,ar_16:9,c_fill,g_auto,e_sharpen', articles: [], errors: {} }; + + componentDidMount() { + const { isAuth } = this.props; + this.props.getPublished(isAuth); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.articles) { + this.setState({ articles: nextProps.articles }); + } + if (nextProps.errors) { + this.setState({ errors: nextProps.errors }); + } + } + + render() { + const { articles, imageRectangle } = this.state; + return ( + +
      +
      + + {(articles || []).map((article, key) => ( +
      +
      +
      +
      + + {article.title.substring(20, + +
      +
      +
      +

      + {article.title} +

      +
      {article.description}
      +
      + {timeStamp(article.createdAt)}{' '} + {article.readTime} min read +
      +
      +
      +
      + ))} +
      +
      +
      + + ); + } +} + +PublishedArticles.propTypes = { + articles: PropTypes.array, + getPublished: PropTypes.func.isRequired, + isAuth: PropTypes.bool, + errors: PropTypes.object +}; +const mapStateToProps = ({ user: { isAuth }, articles: { articles } }) => ({ articles, isAuth }); + +export default connect( + mapStateToProps, + { getPublished } +)(PublishedArticles); diff --git a/src/components/Profile/Articles/MyArticles/index.js b/src/components/Profile/Articles/MyArticles/index.js new file mode 100644 index 0000000..524bcb8 --- /dev/null +++ b/src/components/Profile/Articles/MyArticles/index.js @@ -0,0 +1,3 @@ +import PublishedArticles from './Published'; + +export default PublishedArticles; diff --git a/src/components/Profile/Articles/PreviewArticle/PreviewArticle.js b/src/components/Profile/Articles/PreviewArticle/PreviewArticle.js index 2772328..cfac4a1 100644 --- a/src/components/Profile/Articles/PreviewArticle/PreviewArticle.js +++ b/src/components/Profile/Articles/PreviewArticle/PreviewArticle.js @@ -5,10 +5,17 @@ import PropTypes from 'prop-types'; import 'dotenv/config'; import { Editor, EditorState, convertFromRaw } from 'draft-js'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTrash, faPen, faTimesCircle, faCircle } from '@fortawesome/free-solid-svg-icons'; +import { + faTrash, + faPen, + faTimesCircle, + faCircle, + faGlobeAfrica +} from '@fortawesome/free-solid-svg-icons'; import { MetaTags } from 'react-meta-tags'; import { NotFound } from '../../../common'; import Heading from '../../../common/Heading/Heading'; +import ArticleMenu from '../MyArticles/ArticleMenu'; import { fetchOneArticle, deleteArticle, @@ -80,12 +87,13 @@ export class PreviewArticle extends Component { }; render() { - const { message } = this.props; + const { message, loading } = this.props; const { imageRectangle, article, errors, status, loaded, editorState } = this.state; return (
      + {loaded && article && Object.keys(article).length > 0 ? (
      @@ -151,6 +159,13 @@ export class PreviewArticle extends Component { > Delete + + View Public +
      {errors && errors.error ? (
      @@ -192,7 +207,7 @@ export class PreviewArticle extends Component {
      ) : (
      - {loaded && article && Object.keys(article).length < 1 ? ( + {loaded && loading && article && Object.keys(article).length < 1 ? ( ) : (
      {''}
      diff --git a/src/components/Routes.js b/src/components/Routes.js index 299b8d0..cb9fe44 100644 --- a/src/components/Routes.js +++ b/src/components/Routes.js @@ -15,6 +15,7 @@ import Article from './Articles/Article'; import CreateArticle from './Profile/Articles/CreateArticle/CreateArticle'; import PreviewArticle from './Profile/Articles/PreviewArticle'; import EditArticle from './Profile/Articles/EditArticle'; +import PublishedArticles from './Profile/Articles/MyArticles/Published'; const Routes = ({ isAuth }) => ( @@ -30,7 +31,7 @@ const Routes = ({ isAuth }) => ( /> } /> } /> -
      } /> +
      } /> ( path="/profile/article/edit/:slug" render={props => (isAuth ? : )} /> + (isAuth ? ( + + ) : ( + + )) + } + /> ); diff --git a/src/reducers/articlesReducer.js b/src/reducers/articlesReducer.js index 0b29453..0008345 100644 --- a/src/reducers/articlesReducer.js +++ b/src/reducers/articlesReducer.js @@ -89,6 +89,22 @@ export default function (state = initialState, { type, payload }) { ...state, errors: { ...state.errors, ...payload } }; + case articlesType.FETCH_MY_PUBLISHED_ARTICLES_START: + return { + ...state, + articles: [] + }; + case articlesType.FETCH_MY_PUBLISHED_ARTICLES_SUCCESS: + return { + ...state, + articles: [...state.articles, ...payload.articles] + }; + case articlesType.FETCH_MY_PUBLISHED_ARTICLES_FAILURE: + return { + ...state, + errors: payload, + article: {} + }; default: return state; } diff --git a/src/reducers/index.js b/src/reducers/index.js index d00c24f..486017f 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -2,5 +2,6 @@ import user from './user'; import notification from './notifications'; import articles from './articlesReducer'; import images from './imagesReducer'; +import rating from './rating'; -export default { user, notification, articles, images }; +export default { user, notification, articles, images, rating }; diff --git a/src/reducers/rating/createRatingReducer.js b/src/reducers/rating/createRatingReducer.js new file mode 100644 index 0000000..327f1f4 --- /dev/null +++ b/src/reducers/rating/createRatingReducer.js @@ -0,0 +1,24 @@ +import { ratingActionsTypes } from '../../actions-types'; +import { rating as initialState } from '../../store/initialState'; + +export default function (state = initialState, { type, payload }) { + switch (type) { + case ratingActionsTypes.CREATE_RATING_START: + return { + ...state, + createRate: { loading: true, message: payload.message, errors: {} } + }; + case ratingActionsTypes.CREATE_RATING_SUCCESS: + return { + ...state, + createRate: { loading: false, message: payload.rating.message, errors: {} } + }; + case ratingActionsTypes.CREATE_RATING_FAILURE: + return { + ...state, + createRate: { loading: false, message: '', errors: payload.errors } + }; + default: + return state; + } +} diff --git a/src/reducers/rating/index.js b/src/reducers/rating/index.js new file mode 100644 index 0000000..98b8340 --- /dev/null +++ b/src/reducers/rating/index.js @@ -0,0 +1,7 @@ +import { user as initialState } from '../../store/initialState'; +import createRatingReducer from './createRatingReducer'; + +export default (state = initialState, action) => { + const createRating = createRatingReducer(state, action); + return createRating || state; +}; diff --git a/src/store/initialState.js b/src/store/initialState.js index e615be7..e00448c 100644 --- a/src/store/initialState.js +++ b/src/store/initialState.js @@ -2,5 +2,6 @@ const notification = require('./initialStates/notification'); const user = require('./initialStates/userInitialState'); const articles = require('./initialStates/articlesInitialState'); const images = require('./initialStates/imagesInitialState'); +const rating = require('./initialStates/ratingInitialState'); -module.exports = { user, notification, articles, images }; +module.exports = { user, notification, articles, images, rating }; diff --git a/src/store/initialStates/ratingInitialState.js b/src/store/initialStates/ratingInitialState.js new file mode 100644 index 0000000..e718b4a --- /dev/null +++ b/src/store/initialStates/ratingInitialState.js @@ -0,0 +1,7 @@ +module.exports = { + createRate: { + loading: false, + message: '', + errors: {} + } +};