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 05ba569 commit a70da43
Show file tree
Hide file tree
Showing 12 changed files with 711 additions and 374 deletions.
650 changes: 316 additions & 334 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 @@ -32,6 +32,7 @@
"react-semantic-toasts": "^0.4.1",
"react-simplemde-editor": "^3.6.22",
"react-share": "^2.4.0",
"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 @@ -28,3 +28,6 @@ export const UPDATE_SUCCESSFUL = 'UPDATE_SUCCESSFUL';
export const UPDATE_REJECTED = 'UPDATE_REJECTED';
export const UPDATE_RESET = 'UPDATE_RESET';
export const ARTICLE_UPDATED = 'ARTICLE SUCCESSFULLY UPDATED';
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));
});
};
85 changes: 50 additions & 35 deletions src/components/Article/ViewSingleArticleComponent.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
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 '../ResetPassword/resetpassword.scss';
import './viewsinglearticle.scss';
Expand All @@ -17,21 +16,47 @@ 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 @@ -50,14 +75,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 @@ -103,7 +121,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 @@ -129,14 +146,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 @@ -226,19 +241,11 @@ ${window.location.href}`})
/>
);

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 @@ -247,8 +254,7 @@ ${window.location.href}`})
<p className="small">
<strong><i className="clock icon" /></strong>
{article.article.read_time}
{' '}
min
min
</p>
);

Expand All @@ -260,21 +266,23 @@ ${window.location.href}`})
);

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 @@ -315,6 +323,7 @@ ${window.location.href}`})
}
}
ViewSingleArticleComponent.propTypes = {
rateArticle: PropTypes.func.isRequired,
getArticle: PropTypes.func.isRequired,
article: PropTypes.shape().isRequired,
reason: PropTypes.string.isRequired,
Expand All @@ -323,8 +332,14 @@ ViewSingleArticleComponent.propTypes = {
match: PropTypes.shape().isRequired,
history: PropTypes.shape(),
url: PropTypes.string.isRequired,
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 a70da43

Please sign in to comment.