Skip to content

Commit

Permalink
feat(like-dislike):users can like/dislike articles (#20)
Browse files Browse the repository at this point in the history
- creat like and dislike action dispensers
- add like and dislike button images to styles folder
- add like and dislike buttons to the article view page
- format getArticlesreducer to also get likes and dislikes of an article
- write tests for the like/dislike functionality

[Finishes #164798260]
  • Loading branch information
Genza999 authored and MuhanguziDavid committed May 23, 2019
1 parent cc0a5ad commit 51cadd7
Show file tree
Hide file tree
Showing 11 changed files with 439 additions and 19 deletions.
65 changes: 65 additions & 0 deletions src/actions/LikeDislikeActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import axios from "axios";
import {
LIKE_ARTICLE_FAILURE,
LIKE_ARTICLE_SUCCESS,
DISLIKE_ARTICLE_FAILURE,
DISLIKE_ARTICLE_SUCCESS
} from "./types";

export const likeArticle = slug => {
return async dispatch => {
const body = {};
const headers = {
Authorization: `Bearer ${sessionStorage.getItem("token")}`,
"Content-type": "application/json"
};
return await axios
.post(
`https://ah-backend-prime-staging.herokuapp.com/api/v1/articles/${slug}/like/`,
body,
{ headers: headers }
)
.then(data => {
if (data.data.author) {
dispatch({
type: LIKE_ARTICLE_SUCCESS,
payload: data.data
});
} else {
dispatch({
type: LIKE_ARTICLE_FAILURE,
payload: "Failed to like article"
});
}
});
};
};

export const dislikeArticle = slug => {
return async dispatch => {
const body = {};
const headers = {
Authorization: `Bearer ${sessionStorage.getItem("token")}`,
"Content-type": "application/json"
};
return await axios
.post(
`https://ah-backend-prime-staging.herokuapp.com/api/v1/articles/${slug}/dislike/`,
body,
{ headers: headers }
)
.then(data => {
if (data.data.author) {
dispatch({
type: DISLIKE_ARTICLE_SUCCESS,
payload: data.data
});
} else {
dispatch({
type: DISLIKE_ARTICLE_FAILURE,
payload: "Failed to dislike article"
});
}
});
};
};
4 changes: 4 additions & 0 deletions src/actions/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ export const FOLLOWERS_LIST = "FOLLOWERS_LIST";
export const FOLLOWING_LIST = "FOLLOWING_LIST";
export const FOLLOWERS_LIST_FAILED = "FOLLOWERS_LIST_FAILED";
export const FOLLOWING_LIST_FAILED = "FOLLOWING_LIST_FAILED";
export const LIKE_ARTICLE_SUCCESS = "LIKE_ARTICLE_SUCCESS";
export const LIKE_ARTICLE_FAILURE = "LIKE_ARTICLE_FAILURE";
export const DISLIKE_ARTICLE_FAILURE = "DISLIKE_ARTICLE_FAILURE";
export const DISLIKE_ARTICLE_SUCCESS = "DISLIKE_ARTICLE_SUCCESS";
60 changes: 53 additions & 7 deletions src/components/articles/singleArticle.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,50 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import moment from "moment";
import { Link } from "react-router-dom";
import { Link, withRouter } from "react-router-dom";
import FollowsButton from "../FollowsButton";
import { followUser, unfollowUser } from "../../actions/profileActions";
import { likeArticle, dislikeArticle } from "../../actions/LikeDislikeActions";
import like from "../../styles/images/like.png";
import dislike from "../../styles/images/dislike.png";

import "../../styles/singleArticle.scss";
import { getArticleAction } from "../../actions/getArticle";

export class SingleArticleComponent extends Component {
state = {
likes: 0,
dislikes: 0
};
componentDidMount() {
this.props.getArticleAction(this.props.match.params.slug);
}
componentWillReceiveProps(newProps) {
const { likes, dislikes } = newProps;
this.setState({
likes: likes,
dislikes: dislikes
});
}

handlefollowUser = () => {
const { followUser } = this.props;
const username = sessionStorage.getItem("userview_name");
followUser(username);
};

handlelike = () => {
const { likeArticle } = this.props;
const slug = this.props.match.params.slug;
likeArticle(slug);
};

handledislike = () => {
const { dislikeArticle } = this.props;
const slug = this.props.match.params.slug;
dislikeArticle(slug);
};

handleunfollowUser = () => {
const { unfollowUser } = this.props;
const username = sessionStorage.getItem("userview_name");
Expand All @@ -29,7 +55,8 @@ export class SingleArticleComponent extends Component {
username === sessionStorage.getItem("username") ? true : false;

render() {
const { article, isfollowing } = this.props;
const { isfollowing, article } = this.props;
const { likes, dislikes } = this.state;
const oneArticle = article ? article.article : null;
const singleArticle = oneArticle ? (
<div className="container">
Expand Down Expand Up @@ -104,6 +131,21 @@ export class SingleArticleComponent extends Component {
<div className="article-description-body">
<p style={{ fontWeight: "light" }}>{oneArticle.body}</p>
</div>
<div>
<img
onClick={this.handlelike}
src={like}
style={{ width: "30px", height: "30px" }}
/>
{likes}
<div className="divider" />
<img
onClick={this.handledislike}
src={dislike}
style={{ width: "30px", height: "30px" }}
/>
{dislikes}
</div>
</div>
</div>
) : (
Expand All @@ -115,10 +157,14 @@ export class SingleArticleComponent extends Component {

export const mapStateToProps = state => ({
article: state.getArticleReducer.article,
isfollowing: state.profileReducer.isfollowing
isfollowing: state.profileReducer.isfollowing,
likes: state.getArticleReducer.likes,
dislikes: state.getArticleReducer.dislikes
});

export default connect(
mapStateToProps,
{ getArticleAction, followUser, unfollowUser }
)(SingleArticleComponent);
export default withRouter(
connect(
mapStateToProps,
{ getArticleAction, followUser, unfollowUser, likeArticle, dislikeArticle }
)(SingleArticleComponent)
);
28 changes: 25 additions & 3 deletions src/reducers/getArticleReducer.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,37 @@
import { FETCH_ARTICLE_SUCCESS } from "../actions/types";
import {
FETCH_ARTICLE_SUCCESS,
LIKE_ARTICLE_FAILURE,
LIKE_ARTICLE_SUCCESS,
DISLIKE_ARTICLE_FAILURE,
DISLIKE_ARTICLE_SUCCESS
} from "../actions/types";

const initialState = {
article: {}
article: {},
likes: 0,
dislikes: 0
};

const getArticleReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_ARTICLE_SUCCESS:
return {
...state,
article: action.payload
article: action.payload,
likes: action.payload.article.likes,
dislikes: action.payload.article.dislikes
};
case LIKE_ARTICLE_SUCCESS:
return {
...state,
likes: action.payload.likes,
dislikes: action.payload.dislikes
};
case DISLIKE_ARTICLE_SUCCESS:
return {
...state,
likes: action.payload.likes,
dislikes: action.payload.dislikes
};
default:
return state;
Expand Down
Binary file added src/styles/images/dislike.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/styles/images/like.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/styles/profile.scss
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ button.stats {
margin-bottom: 5px;
}

.divider {
width: 15px;
height: auto;
display: inline-block;
}

#following {
border-bottom: 1px solid #ccc;
margin-bottom: 5px;
Expand Down
156 changes: 156 additions & 0 deletions src/tests/actions/LikeDislikeActions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import moxios from "moxios";
import { likeArticle, dislikeArticle } from "../../actions/LikeDislikeActions";

import {
LIKE_ARTICLE_SUCCESS,
DISLIKE_ARTICLE_SUCCESS,
LIKE_ARTICLE_FAILURE,
DISLIKE_ARTICLE_FAILURE
} from "../../actions/types";

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

describe("LikeDislike action", () => {
beforeEach(function() {
moxios.install();
});
afterEach(function() {
moxios.uninstall();
});
it("should call the like action", () => {
const store = mockStore({});
moxios.wait(() => {
const requestM = moxios.requests.mostRecent();
requestM.respondWith({
status: 200,
response: {
id: 64,
author: {
username: "patritsfitz",
bio:
"This is my favorite song, I just dunno the words, but I still mess with you, you just ain't never heard. It goes like drop that thing. The day you don't drop that thing"
},
title: "kuhcf",
average_rating: 0,
slug: "kuhcf-0x3f",
likes: 1,
dislikes: 0
}
});
});
const expectedActions = [
{
type: LIKE_ARTICLE_SUCCESS,
payload: {
id: 64,
author: {
username: "patritsfitz",
bio:
"This is my favorite song, I just dunno the words, but I still mess with you, you just ain't never heard. It goes like drop that thing. The day you don't drop that thing"
},
title: "kuhcf",
average_rating: 0,
slug: "kuhcf-0x3f",
likes: 1,
dislikes: 0
}
}
];
const slug = "kuhcf-0x3f";
return store.dispatch(likeArticle(slug)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});

it("should call the dislike action", () => {
const store = mockStore({});
moxios.wait(() => {
const requestM = moxios.requests.mostRecent();
requestM.respondWith({
status: 200,
response: {
id: 64,
author: {
username: "patritsfitz",
bio:
"This is my favorite song, I just dunno the words, but I still mess with you, you just ain't never heard. It goes like drop that thing. The day you don't drop that thing"
},
title: "kuhcf",
average_rating: 0,
slug: "kuhcf-0x3f",
likes: 1,
dislikes: 0
}
});
});
const expectedActions = [
{
type: DISLIKE_ARTICLE_SUCCESS,
payload: {
id: 64,
author: {
username: "patritsfitz",
bio:
"This is my favorite song, I just dunno the words, but I still mess with you, you just ain't never heard. It goes like drop that thing. The day you don't drop that thing"
},
title: "kuhcf",
average_rating: 0,
slug: "kuhcf-0x3f",
likes: 1,
dislikes: 0
}
}
];
const slug = "kuhcf-0x3f";
return store.dispatch(dislikeArticle(slug)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});

it("should fail to call the dislike action", () => {
const store = mockStore({});
moxios.wait(() => {
const requestM = moxios.requests.mostRecent();
requestM.respondWith({
response: {
error: "Failed to dislike article"
}
});
});
const expectedActions = [
{
type: DISLIKE_ARTICLE_FAILURE,
payload: "Failed to dislike article"
}
];
const slug = "kuhcf-0x3f";
return store.dispatch(dislikeArticle(slug)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});

it("should fail to call the like action", () => {
const store = mockStore({});
moxios.wait(() => {
const requestM = moxios.requests.mostRecent();
requestM.respondWith({
response: {
error: "Failed to like article"
}
});
});
const expectedActions = [
{
type: LIKE_ARTICLE_FAILURE,
payload: "Failed to like article"
}
];
const slug = "kuhcf-0x3f";
return store.dispatch(likeArticle(slug)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
});
3 changes: 3 additions & 0 deletions src/tests/components/__snapshots__/singleArticle.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Component for getting single Articles should receive props 1`] = `ShallowWrapper {}`;
Loading

0 comments on commit 51cadd7

Please sign in to comment.