Skip to content

Commit

Permalink
feat(articles): rate articles
Browse files Browse the repository at this point in the history
- rate rendered individual articles
- set the initial rating to the one from the backend

[Delivers #161966573]
  • Loading branch information
SnyderMbishai committed Feb 1, 2019
1 parent 0e234d3 commit 112c279
Show file tree
Hide file tree
Showing 12 changed files with 842 additions and 440 deletions.
848 changes: 448 additions & 400 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"react-scripts": "2.1.3",
"react-semantic-toasts": "^0.4.1",
"react-simplemde-editor": "^3.6.22",
"react-star-rating-component": "^1.4.1",
"react-test-renderer": "^16.7.0",
"redux": "^4.0.1",
"redux-devtools-extension": "^2.13.7",
Expand Down
94 changes: 94 additions & 0 deletions src/actions/__tests__/ratingAction.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import thunk from 'redux-thunk';
import expect from 'expect';
import configureMockStore from 'redux-mock-store';
import moxios from 'moxios';
import * as actions from '../ratingActions';
import * as types from '../actionTypes';

const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);

describe('Rating Actions', () => {
describe('Successful Rating', () => {
it('should create a request action.', () => {
const ratingData = {
request: {
slug: '',
rating: '',
},
};
const expectedAction = {
type: types.RATE_ARTICLE,
payload: ratingData,
};
expect(actions.rate(ratingData)).toEqual(expectedAction);
});

it('should rate successfuly action.', () => {
const message = 'rated';
const expectedAction = {
type: types.RATE_ARTICLE_SUCCESS,
rating: message,
};
expect(actions.ratingSuccess(message)).toEqual(expectedAction);
});

it('should create action for failed rating.', () => {
const message = 'Failed to rate';
const expectedAction = {
type: types.RATE_ARTICLE_FAIL,
error: message,
};
expect(actions.ratingFail(message)).toEqual(expectedAction);
});
});
});

// Mock data
const ratingData = {
slug: 'hello',
rating: { rating: 2 },
};

describe('Rating actions', () => {
beforeEach(() => moxios.install());
afterEach(() => moxios.uninstall());

it('successful async action', () => {
const message = 'Successfully rated article';
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 201,
response: message,
});
});
const expectedActions = [
{ type: types.RATE_ARTICLE, payload: ratingData },
{ type: types.RATE_ARTICLE_SUCCESS, rating: message },
];
const store = mockStore(message);
return store.dispatch(actions.rateArticle(ratingData)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});

it('failing async action', () => {
const message = 'error';
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 403,
response: message,
});
});
const expectedActions = [
{ type: types.RATE_ARTICLE, payload: ratingData },
{ type: types.RATE_ARTICLE_FAIL, error: message },
];
const store = mockStore(message);
return store.dispatch(actions.rateArticle(ratingData)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
});
3 changes: 3 additions & 0 deletions src/actions/actionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ export const FETCH_ARTICLES_FAILS = 'FETCH_ARTICLES_FAILS';
export const CREATE_ARTICLE = 'CREATE_ARTICLE';
export const CREATE_ARTICLE_SUCCESS = 'CREATE_ARTICLE_SUCCESS';
export const CREATE_ARTICLE_FAILS = 'CREATE_ARTICLE_FAILS';
export const RATE_ARTICLE = 'RATE_ARTICLE';
export const RATE_ARTICLE_SUCCESS = 'RATE_ARTICLE_SUCCESS';
export const RATE_ARTICLE_FAIL = 'RATE_ARTICLE_FAIL';
41 changes: 41 additions & 0 deletions src/actions/ratingActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import axios from 'axios';
import Cookies from 'js-cookie';
import * as types from './actionTypes';

export const rate = payload => (
{
type: types.RATE_ARTICLE,
payload,
}
);

export const ratingSuccess = rating => (
{
type: types.RATE_ARTICLE_SUCCESS,
rating,
}
);

export const ratingFail = error => (
{
type: types.RATE_ARTICLE_FAIL,
error,
}
);

const token = Cookies.get('access_token');

export const rateArticle = payload => (dispatch) => {
const { slug, rating } = payload;
dispatch(rate(payload));
return axios.post(`https://ah-technocrats.herokuapp.com/api/articles/${slug}/rate/`, rating, {
headers: {
'Content-Type': 'application/json',
Authorization: `Token ${token}`,
},
})
.then(res => dispatch(ratingSuccess(res.data)))
.catch((error) => {
dispatch(ratingFail(error.response.data));
});
};
87 changes: 50 additions & 37 deletions src/components/Article/ViewSingleArticleComponent.jsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,52 @@
import React, { Component } from 'react';
import {
Loader, Rating, Segment, Sidebar,
} from 'semantic-ui-react';
import { connect } from 'react-redux';
import { Loader, Segment, Sidebar } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import SideBarMenu from '../Menu/Menu';
import Header from '../Header/Header';


import Footer from '../Footer/Footer';
import { rateArticle } from '../../actions/ratingActions';
import rateArticleComponent from './ratingArticleComponent';

class ViewSingleArticleComponent extends Component {
// rating
constructor() {
super();
this.state = {
rating: 0,
};
this.onStarClick = this.onStarClick.bind(this);
}

componentDidMount() {
const { getArticle } = this.props;
const { match } = this.props;
getArticle(match.params.slug);
}

// rating
onStarClick(nextValue) {
this.setState({ rating: nextValue });
const { match } = this.props;
const articleSlug = match.params.slug;
const payload = {
slug: articleSlug, rating: { rating: nextValue },
};
// eslint-disable-next-line no-shadow
const { rateArticle } = this.props;
rateArticle(payload);
}

setInitialState(article) {
const value = article.article.rating.average;
this.state.rating = value;
}

renderActionButton = (buttonClass, content, iconClass) => (
<button type="button" className={buttonClass}>
<i className={iconClass} />
<span>
{' '}
{content}
</span>
<span>{content}</span>
</button>
);

Expand All @@ -42,14 +65,7 @@ class ViewSingleArticleComponent extends Component {
)}
{this.renderActionButton('ui tiny circular yellow button', 'Favorite', 'star icon')}
{this.renderActionButton('ui tiny circular yellow button', 'Bookmark', 'bookmark icon')}
{this.renderLink(
'',
this.renderActionButton(
'ui tiny circular blue icon button',
'Report',
'eye icon',
), `/report/${article.article.article_slug}`,
)}
{this.renderLink('', this.renderActionButton('ui tiny circular blue icon button', 'Report', 'eye icon'), `/report/${article.article.article_slug}`)}
</div>
);
}
Expand Down Expand Up @@ -95,7 +111,6 @@ class ViewSingleArticleComponent extends Component {
</div>
<div className="ui green small labeled submit icon button">
<i className="icon edit" />
{' '}
Add Reply
</div>
</form>
Expand All @@ -121,14 +136,12 @@ class ViewSingleArticleComponent extends Component {
</div>
<div className="middle aligned content">
Written by:
{' '}
{this.renderLink('',
article.article.author.username,
`/profile/${article.article.author.username}`)}
</div>
</div>
</div>

</div>
{this.renderActionButtons(article)}
</div>
Expand Down Expand Up @@ -165,19 +178,11 @@ class ViewSingleArticleComponent extends Component {
/>
);

renderSameLine = () => (
<div className="right float same-line">
<p className="bold">Rate article</p>
<Rating maxRating={5} clearable />
</div>
);

renderFollow(article) {
return this.renderLink('follow',

<div className="ui label space-bottom">
<i className="users icon" />
{' '}
31 Followers
</div>, `/${article.article.author.username}/followers`);
}
Expand All @@ -186,8 +191,7 @@ class ViewSingleArticleComponent extends Component {
<p className="small">
<strong><i className="clock icon" /></strong>
{article.article.read_time}
{' '}
min
min
</p>
);

Expand All @@ -199,21 +203,23 @@ class ViewSingleArticleComponent extends Component {
);

renderContainer(article) {
const { rating } = this.state;
const { history, failing } = this.props;

return (
<div className="ui container">

<div className="ui space borderless">

<h1 className="increase-size">{article.article.title}</h1>
{this.renderWrittenBy(article)}
{this.renderReadTime(article)}
{this.renderFollow(article)}
{this.renderSameLine()}
{rateArticleComponent(this.onStarClick, rating, history, failing)}
{this.renderArticleCover(article)}
{this.renderCenteredGrid(article)}
{this.renderUiGrid(article)}
{this.renderTagSpace()}
{this.renderComments(article)}
{this.setInitialState(article)}
</div>
</div>
);
Expand Down Expand Up @@ -253,15 +259,22 @@ class ViewSingleArticleComponent extends Component {
}
}
ViewSingleArticleComponent.propTypes = {
rateArticle: PropTypes.func.isRequired,
getArticle: PropTypes.func.isRequired,
article: PropTypes.shape().isRequired,
reason: PropTypes.string.isRequired,
success: PropTypes.bool.isRequired,
processing: PropTypes.bool.isRequired,
match: PropTypes.shape().isRequired,
history: PropTypes.shape(),
failing: PropTypes.bool.isRequired,
};
ViewSingleArticleComponent.defaultProps = {
history: null,
};
export default ViewSingleArticleComponent;

ViewSingleArticleComponent.defaultProps = { history: null };

const mapStateToPropsRating = (state, ownProps) => ({
failing: state.ratingReducer.failing,
ownProps,
});

export default connect(mapStateToPropsRating, { rateArticle })(ViewSingleArticleComponent);
Loading

0 comments on commit 112c279

Please sign in to comment.