Skip to content

Commit

Permalink
feat(follow user): Users should be able to follow each other
Browse files Browse the repository at this point in the history
- Follow a specific user
- Unfollow a specific user
- View followers (users that follow you)
- View users you follow

[Starts #161348765]
  • Loading branch information
Bruce Allan Makaaru authored and Bruce Allan Makaaru committed Dec 20, 2018
1 parent d35cfd8 commit e27a80b
Show file tree
Hide file tree
Showing 24 changed files with 1,416 additions and 9 deletions.
5 changes: 5 additions & 0 deletions src/__snapshots__/index.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ exports[`Provider and App renders <Provider/> correctly 2`] = `
component={[Function]}
path="/createArticle"
/>
<Route
component={[Function]}
exact={true}
path="/profiles/:username"
/>
</Switch>
<_class
options={Object {}}
Expand Down
6 changes: 6 additions & 0 deletions src/actions/actionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ const ACTION_TYPE = {
GET_ARTICLE: 'GET_ARTICLE',
GET_LIKE_STATUS: 'GET_LIKE_STATUS',
USER_LOGIN: 'USER_LOGIN',
FOLLOW_USER: 'FOLLOW_USER',
UNFOLLOW_USER: 'UNFOLLOW_USER',
GET_FOLLOWERS: 'GET_FOLLOWERS',
GET_FOLLOWEES: 'GET_FOLLOWEES',
FOLLOW_ERROR: 'FOLLOW_ERROR',
SHOW_USER_PROFILE: 'SHOW_USER_PROFILE',
SHOW_ERROR: 'SHOW_ERROR',
SOCIAL_LOGIN: 'SOCIAL_LOGIN_SUCCESS',
LOGIN: 'LOGIN',
Expand Down
78 changes: 78 additions & 0 deletions src/actions/followActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import axios from 'axios';
import actionTypes from './actionTypes';
import APP_URL from '../utils/constants';

const showProfileAction = payload => ({
type: actionTypes.SHOW_USER_PROFILE,
payload,
});

const followAction = payload => ({
type: actionTypes.FOLLOW_USER,
payload,
});

const unfollowAction = payload => ({
type: actionTypes.UNFOLLOW_USER,
payload,
});

const getFollowersAction = payload => ({
type: actionTypes.GET_FOLLOWERS,
payload,
});

const getFolloweesAction = payload => ({ // followees = users you follow
type: actionTypes.GET_FOLLOWEES,
payload,
});

const showErrorAction = payload => ({
type: actionTypes.SHOW_ERROR,
payload,
});

export const getProfileThunk = ({ username, token }) => (dispatch) => {
const url = `${APP_URL}/profiles/${username}`;
return axios.get(url, { headers: { Authorization: `Token ${token}` } })
.then((response) => {
dispatch(showProfileAction(response.data.results));
})
.catch(() => {
dispatch(showErrorAction('Could not find profile'));
});
};

export const followThunk = ({ user, token }) => (dispatch) => {
const url = `${APP_URL}/users/${user}/follow`;
return axios.post(url, user, { headers: { Authorization: `Token ${token}` } })
.then((response) => {
dispatch(followAction(response.data.results));
})
.catch((error) => {
dispatch(showErrorAction(error.response.data.results.error));
});
};

export const unfollowThunk = ({ user, token }) => (dispatch) => {
const url = `${APP_URL}/users/${user}/follow`;
return axios.delete(url, { headers: { Authorization: `Token ${token}` } })
.then((response) => {
dispatch(unfollowAction(response.data.results));
})
.catch((error) => {
dispatch(showErrorAction(error.response.data.results.error));
});
};

const headers = { headers: { Authorization: `Token ${localStorage.getItem('token')}` } };
export const getFollowProfilesThunk = (url, getFollowers) => (dispatch) => {
if (getFollowers) {
return axios.get(url, headers).then((response) => {
dispatch(getFollowersAction(response.data.results));
}).catch(() => {});
}
return axios.get(url, headers).then((response) => {
dispatch(getFolloweesAction(response.data.results));
}).catch(() => {});
};
18 changes: 18 additions & 0 deletions src/actions/loginActions/loginActions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,22 @@ describe('Login Actions tests', () => {
));
});
});
test('Login action failure', () => {
moxios.stubRequest(`${APP_URL}/users/login`, {
status: 400,
responseText: {
error: { response: { data: { results: { error: ['login error'] } } } },
},
});
const expectedtActions = {
type: ACTION_TYPE.USER_LOGIN_FAILURE,
errorMessage: 'login error',
};
const store = mockStore({});
store.dispatch(loginThunk())
.then(() => {
expect(store.getActions()).toEqual(expect.objectContaining(expectedtActions));
})
.catch(() => {});
});
});
171 changes: 171 additions & 0 deletions src/actions/tests/followActions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import moxios from 'moxios';
import actionTypes from '../actionTypes';
import {
getProfileThunk,
followThunk, unfollowThunk,
getFollowProfilesThunk,
} from '../followActions';
import APP_URL from '../../utils/constants';

const mockStore = configureMockStore([thunk]);

describe('Folow, Unfollow Actions', () => {
let store;
let profileUrl;
let usersUrl;
let janedoe;
let profilesList;
let error;

beforeEach(() => {
moxios.install();
store = mockStore({});
profileUrl = `${APP_URL}/profiles/janedoe`;
usersUrl = `${APP_URL}/users/`;
janedoe = {
username: 'janedoe',
bio: 'This is who I am',
first_name: 'Jane',
last_name: 'Doe',
image: 'https://picsum.photos/600',
created_at: '10-11-2011',
updated_at: '11-11-2018',
isFollowee: false,
};
profilesList = [
{
username: 'jack',
bio: '',
image: '',
first_name: 'Jack',
last_name: 'Katto',
created_at: '10-11-2018',
updated_at: '12-11-2018',
},
{
username: 'ivy',
bio: '',
image: '',
first_name: 'Ivy',
last_name: 'Jones',
created_at: '10-11-2018',
updated_at: '15-11-2018',
},
];
error = {
response: { data: { results: { error: 'Error message' } } },
};
});

afterEach(() => {
moxios.uninstall();
});

test('Get profile of user to follow or unfollow', () => {
moxios.stubRequest(profileUrl, {
status: 200,
responseText: janedoe,
});
const expectedActions = [{ type: actionTypes.SHOW_USER_PROFILE }];
store.dispatch(getProfileThunk({ username: 'janedoe', token: 'abcabc' }))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
})
.catch(() => {});
});

test('Profile of user one is trying to follow is not found', () => {
moxios.stubRequest(profileUrl, {
status: 404,
responseText: 'Could not find profile',
});
const expectedActions = [{ type: actionTypes.SHOW_ERROR }];
store.dispatch(getProfileThunk({ username: 'janedoe', token: 'abcabc' }))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
})
.catch(() => {});
});

test('Follow a user', () => {
moxios.stubRequest(`${usersUrl}janedoe/follow`, {
status: 200,
responseText: janedoe,
});
const expectedActions = [{ type: actionTypes.FOLLOW_USER }];
store.dispatch(followThunk({ user: 'janedoe', token: 'abcabc' }))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
})
.catch(() => {});
});

test('Tying to Follow a user that does not exist', () => {
moxios.stubRequest(`${usersUrl}janedoe/follow`, {
status: 404,
responseText: error,
});
const expectedActions = [{ type: actionTypes.SHOW_ERROR }];
store.dispatch(followThunk({ user: 'janedoe', token: 'abcabc' }))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
})
.catch(() => {});
});

test('Unfollow a user', () => {
moxios.stubRequest(`${usersUrl}janedoe/follow`, {
status: 200,
responseText: { message: 'You have Unfollowed the User' },
});
const expectedActions = [{ type: actionTypes.UNFOLLOW_USER }];
store.dispatch(unfollowThunk({ user: 'janedoe', token: 'abcabc' }))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
})
.catch(() => {});
});

test('Tyring to Unfollow a user that does not exist', () => {
moxios.stubRequest(`${usersUrl}janedoe/follow`, {
status: 404,
responseText: error,
});
const expectedActions = [{ type: actionTypes.SHOW_ERROR }];
store.dispatch(unfollowThunk({ user: 'janedoe', token: 'abcabc' }))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
})
.catch(() => {});
});

test('View followers of a specific user', () => {
const url = `${usersUrl}janedoe/followers`;
moxios.stubRequest(url, {
status: 200,
responseText: profilesList,
});
const expectedActions = [{ type: actionTypes.GET_FOLLOWERS }];
store.dispatch(getFollowProfilesThunk(url, true))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
})
.catch(() => {});
});

test('View users that a specific user follows ("followees")', () => {
const url = `${usersUrl}janedoe/following`;
moxios.stubRequest(url, {
status: 200,
responseText: profilesList,
});
const expectedActions = [{ type: actionTypes.GET_FOLLOWEES }];
store.dispatch(getFollowProfilesThunk(url, false))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
})
.catch(() => {});
});
});
22 changes: 22 additions & 0 deletions src/commons/initialStates.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ const initialState = {
},
},
ratingReducer: {},
followUnfollowReducer: {
currentProfile: {
username: '',
bio: '',
first_name: '',
last_name: '',
image: '',
created_at: '',
updated_at: '',
isFollowee: false,
},
followersList: [],
followeesList: [],
},
};

export const sampleArticle = {
Expand Down Expand Up @@ -92,4 +106,12 @@ export const sampleListOfArticles = [
{ ...sampleArticle, id: 7 }, { ...sampleArticle, id: 8 }, { ...sampleArticle, id: 9 },
];

export const tempProfile = {
username: localStorage.getItem('username'),
first_name: 'You!',
last_name: '',
image: 'https://iviidev.info/downloads/images/you.jpg',
isFollowee: false,
};

export default initialState;
2 changes: 2 additions & 0 deletions src/components/App/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import EditProfilePageConnected from '../../containers/profiles/editProfile';
import PasswordResetPage from '../../containers/PasswordResetPage';
import NewPasswordPage from '../../containers/PasswordResetPage/newpasswordPage';
import CreateArticlePage from '../../containers/CreateArticlePage';
import FollowUnfollow from '../../containers/FollowUnfollow';

library.add(faSearch);
const App = () => (
Expand All @@ -32,6 +33,7 @@ const App = () => (
<Route path="/passwordreset" component={PasswordResetPage} />
<Route path="/newpassword" component={NewPasswordPage} exact />
<Route path="/createArticle" component={CreateArticlePage} />
<Route exact path="/profiles/:username" component={FollowUnfollow} />
</Switch>
<Notification />
<FooterConnected />
Expand Down
12 changes: 12 additions & 0 deletions src/components/Follow/__snapshots__/index.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<Follow /> component renders the Follow component 1`] = `
<button
className="btn btn-outline-danger btn-sm m-2"
id="bt-follow"
onClick={[Function]}
type="button"
>
Unfollow
</button>
`;
20 changes: 20 additions & 0 deletions src/components/Follow/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';

const Follow = ({ onClick, option }) => (
<button
type="button"
id="bt-follow"
className={option ? 'btn btn-outline-danger btn-sm m-2' : 'btn btn-outline-primary btn-sm m-2'}
onClick={() => { onClick(option); }}
>
{option ? 'Unfollow' : 'Follow'}
</button>
);

Follow.propTypes = {
onClick: PropTypes.func.isRequired,
option: PropTypes.bool.isRequired,
};

export default Follow;
10 changes: 10 additions & 0 deletions src/components/Follow/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';
import { shallow } from 'enzyme';
import Follow from '.';

describe('<Follow /> component', () => {
it('renders the Follow component', () => {
const wrapper = shallow(<Follow onClick={jest.fn()} option />);
expect(wrapper).toMatchSnapshot();
});
});
Loading

0 comments on commit e27a80b

Please sign in to comment.