diff --git a/.travis.yml b/.travis.yml index 1b546fb..11b5908 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ env: global: - NODE_ENV=test - - CI=true - REACT_APP_URL_BACKEND=http://localhost:3000 - REACT_APP_URL_FRONTEND=http://localhost:5000 - CC_TEST_REPORTER_ID=d43031f55998206172c77fb2bc3c560af8d54e2747e10927abeea54ca5740e95 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..dd6053f --- /dev/null +++ b/src/__tests__/actions/rating/createRate.test.js @@ -0,0 +1,34 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'react-thunk'; +import { Provider } from 'react-redux'; +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'); + }); + }); +}); diff --git a/src/__tests__/components/Articles/Rating.test.js b/src/__tests__/components/Articles/Rating.test.js new file mode 100644 index 0000000..675975c --- /dev/null +++ b/src/__tests__/components/Articles/Rating.test.js @@ -0,0 +1,87 @@ +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 { mockStore, initialState } from '../../../__mocks__/store'; +import { + Rating as RatingComponent, + mapStateToProps +} from '../../../components/Articles/Article/Rating'; +import { shallow, mount } from '../../../../config/enzymeConfig'; + +describe('
', () => { + const props = { + message: 'Thank you for rating this article', + errors: { token: 'Failed to authenticate token' }, + submitRate: jest.fn(), + createRate: jest.fn(), + closeMessage: jest.fn(), + ratingStars: jest.fn(), + fetchOneArticle: jest.fn(), + getSpecificArticle: jest.fn(), + isAuth: true, + slug: 'slug-article-12fg51x', + article: { + title: 'yes man', + rating: 1, + description: 'yes' + }, + loading: false + }; + const component = shallow(); + it('should create snapshot ', () => { + expect(component).toMatchSnapshot(); + }); + it('should trigger submit rate ', () => { + component.setProps({ message: props.message, slug: props.slug, isAuth: true }); + component.instance().submitRate(4); + }); + it('should trigger close message ', () => { + component.setState({ message: { rating: 'updated' } }); + component + .find('.rating-errors i') + .at(1) + .simulate('click'); + component.instance().closeMessage(); + }); + it('should trigger close message ', () => { + component.setState({ message: { rating: 'updated' } }); + component + .find('.rating-errors i') + .at(0) + .simulate('click'); + component.instance().closeMessage(); + }); + it('should trigger submit rate ', () => { + component.setProps({ message: props.message, slug: props.slug, isAuth: false }); + component.setState({ errors: { token: 'some' } }); + component.instance().submitRate(4); + }); + it('should trigger submit rate ', () => { + component.setProps({ message: props.message, slug: props.slug, isAuth: false }); + component.setState({ errors: { token: 'token' } }); + component.instance().submitRate(4); + }); + it('should trigger submit rate ', () => { + component + .find('.one-star') + .at(1) + .simulate('click', 5); + }); + + it('should test map state', () => { + mapStateToProps({ + user: { isAuth: true }, + articles: { article: props.article }, + rating: { + createRate: { + loading: false, + message: 'yes', + errors: { token: 'token' } + } + } + }); + expect(mapStateToProps).toBeDefined(); + }); +}); diff --git a/src/__tests__/components/Articles/__snapshots__/Rating.test.js.snap b/src/__tests__/components/Articles/__snapshots__/Rating.test.js.snap new file mode 100644 index 0000000..b9bde7f --- /dev/null +++ b/src/__tests__/components/Articles/__snapshots__/Rating.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`
should create snapshot 1`] = `ShallowWrapper {}`; 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..42cd619 --- /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({ articles }); + }); +}); diff --git a/src/__tests__/components/Routes.test.js b/src/__tests__/components/Routes.test.js index 01d9f6c..d712d44 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..7cb358b --- /dev/null +++ b/src/__tests__/reducers/rating/ratingReducers.test.js @@ -0,0 +1,38 @@ +import createRatingReducer from '../../../reducers/rating/createRatingReducer'; +import initialState from '../../../store/initialStates/ratingInitialState'; +import { ratingActionsTypes } from '../../../actions-types'; + +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 ff612ac..9e79bb0 100644 --- a/src/assets/css/_colors.scss +++ b/src/assets/css/_colors.scss @@ -7,7 +7,7 @@ $secondary: #1843b8; $danger: #d64c4c; $success: #1c8a4a; $info: #1cbdee; -$light-grey: #e6e6e6; +$light-grey: #cbcbcb; $light-red: #f5eeee; $yellow: #f9d342; /* background anim colors */ @@ -53,6 +53,9 @@ $twitter-color: #50abf1; .light-red { background: $light-red; } +.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 0000000..6267daa Binary files /dev/null and b/src/assets/images/star.png differ 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..16688d9 --- /dev/null +++ b/src/components/Articles/Article/Rating.js @@ -0,0 +1,113 @@ +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, article } = nextProps; + if (message) { + this.setState({ message }); + } + if (errors) { + this.setState({ errors }); + } + if (Object.keys(article).length !== 0) { + this.setState({ rating: article.rating }); + } + this.setState({ loaded: true }); + } + + submitRate = async (rating) => { + const { isAuth, slug, fetchOneArticle } = this.props; + if (isAuth) { + await this.props.createRate(slug, rating); + await fetchOneArticle(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 +}; +export const mapStateToProps = ({ + user: { isAuth }, + articles: { article }, + rating: { createRate: { loading, message, errors } } +}) => ({ 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..7e589d1 --- /dev/null +++ b/src/components/Articles/Article/Rating.scss @@ -0,0 +1,71 @@ +@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: 55px; + 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: -20px; + padding: 7px; + right: -12px; + height: 10px; + @extend .shadow-4; + } +} diff --git a/src/components/Articles/ListOfArticles/ListOfArticles.js b/src/components/Articles/ListOfArticles/ListOfArticles.js index 8766d81..40e462b 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..f579c1e --- /dev/null +++ b/src/components/Profile/Articles/MyArticles/Published.js @@ -0,0 +1,105 @@ +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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faPen, faGlobeAfrica } from '@fortawesome/free-solid-svg-icons'; +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 + + + View on public + {' '} + + Edit + {' '} + +
      +
      +
      +
      + ))} +
      +
      +
      + + ); + } +} + +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..b26cc2b 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 ? ( ) : (
      {''}
      @@ -223,7 +238,8 @@ PreviewArticle.propTypes = { params: PropTypes.object, message: PropTypes.object, errors: PropTypes.object, - isAuth: PropTypes.bool + isAuth: PropTypes.bool, + loading: PropTypes.bool, }; export const mapStateToProps = ({ user: { isAuth }, articles: { article, message, errors } }) => ({ diff --git a/src/components/Routes.js b/src/components/Routes.js index 39e240d..b6ad06a 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: {} + } +};