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: {}
+ }
+};