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 7, 2019
1 parent 27e725d commit 949e65b
Show file tree
Hide file tree
Showing 18 changed files with 752 additions and 365 deletions.
661 changes: 324 additions & 337 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"firebase": "^5.8.1",
"jest-mock": "^23.2.0",
"js-cookie": "^2.2.0",
"js-cookies": "^1.0.4",
"jsdom": "^13.2.0",
"moment": "^2.24.0",
"moxios": "^0.4.0",
Expand All @@ -30,8 +31,9 @@
"react-router-dom": "^4.3.1",
"react-scripts": "2.1.3",
"react-semantic-toasts": "^0.4.1",
"react-simplemde-editor": "^3.6.22",
"react-share": "^2.4.0",
"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
95 changes: 95 additions & 0 deletions src/actions/__tests__/ratingAction.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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 },
{ type: types.RATE_ARTICLE_RESET },
];
const store = mockStore(message);
return store.dispatch(actions.rateArticle(ratingData)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
});
5 changes: 5 additions & 0 deletions src/actions/actionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ 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 CREATE_ARTICLE_RESET = 'CREATE_ARTICLE_RESET';
export const UPDATE_ACTION = 'UPDATE_ACTION';
export const UPDATE_SUCCESSFUL = 'UPDATE_SUCCESSFUL';
export const UPDATE_REJECTED = 'UPDATE_REJECTED';
Expand Down Expand Up @@ -54,3 +55,7 @@ export const FETCH_PROFILE_FOLLOWERS_FAILED = 'FETCH PROFILE FOLLOWERS FAILED';
export const PROFILE_FOLLOWING = 'FETCH PROFILE FOLLOWING DATA';
export const FETCH_PROFILE_FOLLOWING_FAILED = 'FETCH PROFILE FOLLOWERS FAILED';
export const PROFILES_LOADING = 'PROFILES LOADING STATUS';
export const RATE_ARTICLE = 'RATE_ARTICLE';
export const RATE_ARTICLE_SUCCESS = 'RATE_ARTICLE_SUCCESS';
export const RATE_ARTICLE_FAIL = 'RATE_ARTICLE_FAIL';
export const RATE_ARTICLE_RESET = 'RATE_ARTICLE_RESET';
5 changes: 5 additions & 0 deletions src/actions/articleActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ export const createSuccess = response => ({
response,
});

export const createReset = response => ({
type: types.CREATE_ARTICLE_RESET,
response,
});

export function fetchArticles() {
return (dispatch) => {
dispatch(fetchAllArticles());
Expand Down
44 changes: 44 additions & 0 deletions src/actions/ratingActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import axios from 'axios';
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,
}
);
export const ratingReset = () => (
{
type: types.RATE_ARTICLE_RESET,
}
);

export const rateArticle = (payload, token) => (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));
dispatch(ratingReset());
});
};
67 changes: 46 additions & 21 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 @@ -13,26 +12,53 @@ import {
WhatsappShareButton,
EmailShareButton, FacebookIcon, WhatsappIcon, TwitterIcon, GooglePlusIcon, EmailIcon,
} from 'react-share';
import { connect } from 'react-redux';
import Cookies from 'js-cookie';
import SideBarMenu from '../Menu/Menu';
import Header from '../Header/Header';
import * as likingActions from '../../actions/likeActions';
import * as dislikeActions from '../../actions/dislikeActions';
import Footer from '../Footer/Footer';
import renderActionButtons from './LikeDislikeContainer';
import { rateArticle } from '../../actions/ratingActions';
import rateArticleComponent from './ratingArticleComponent';

class ViewSingleArticleComponent extends Component {
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 },
};
const { rateArticle } = this.props;
const token = Cookies.get('access_token');
rateArticle(payload, token);
}

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

renderActionButton = (buttonClass, id, content, iconClass, onClick) => (
<button type="button" className={buttonClass} id={id} onClick={onClick}>
<i className={iconClass} />
<span>
{' '}
{content}
</span>
</button>
Expand Down Expand Up @@ -89,7 +115,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 @@ -115,14 +140,12 @@ class ViewSingleArticleComponent extends Component {
</div>
<div className="middle aligned content">
Written by:
{' '}
{this.renderLink('',
article.article.author.username,
`/profiles/${article.article.author.username}`)}
</div>
</div>
</div>

</div>
{renderActionButtons(article,
this.likeArticle, this.dislikeArticle, this.renderActionButton, this.renderLink)}
Expand Down Expand Up @@ -219,19 +242,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 @@ -240,8 +255,7 @@ ${window.location.href}`,
<p className="small">
<strong><i className="clock icon" /></strong>
{article.article.read_time}
{' '}
min
min
</p>
);

Expand All @@ -253,21 +267,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 @@ -308,6 +324,8 @@ ${window.location.href}`,
}
}
ViewSingleArticleComponent.propTypes = {
rateArticle: PropTypes.func.isRequired,
ratingReset: PropTypes.func.isRequired,
getArticle: PropTypes.func.isRequired,
article: PropTypes.shape().isRequired,
dislike: PropTypes.shape().isRequired,
Expand All @@ -318,7 +336,9 @@ ViewSingleArticleComponent.propTypes = {
match: PropTypes.shape().isRequired,
history: PropTypes.shape(),
url: PropTypes.string.isRequired,
failing: PropTypes.bool.isRequired,
};

ViewSingleArticleComponent.defaultProps = {
history: null,
};
Expand All @@ -331,13 +351,18 @@ const matchDispatchToProps = dispatch => (
dislike: slug => (
dispatch(dislikeActions.dislikeArticle(slug))
),

rateArticle: (articlesData, token) => (
dispatch(rateArticle(articlesData, token))
),
}
);

function mapStateToProps(state) {
return {
likeReducer: state.likeReducer,
dislikeReducer: state.dislikeReducer,
failing: state.ratingReducer.failing,

};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('ViewSingleArticle tests', () => {
loading: false,
successfulMessage: null,
},
ratingReducer: {},
});
const article = {
message: 'Article found.',
Expand Down
Loading

0 comments on commit 949e65b

Please sign in to comment.