Skip to content

Commit

Permalink
feat(edit profile): users can edit their profiles
Browse files Browse the repository at this point in the history
- enables users to edit their profiles
[Delivers #161820823]
  • Loading branch information
kakaemma committed Nov 23, 2018
1 parent b9401f5 commit 42d71bf
Show file tree
Hide file tree
Showing 12 changed files with 371 additions and 2 deletions.
8 changes: 8 additions & 0 deletions src/actions/actionCreators.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
SEND_RESET_LINK_ERROR,
RESET_PASSWORD_SUCCESS,
RESET_PASSWORD_ERROR,
UPDATE_PROFILE_INITIATED,
UPDATE_PROFILE_SUCCESS,
} from './types';

export const socialLoginInitiated = () => ({
Expand Down Expand Up @@ -132,3 +134,9 @@ export const ResetPasswordError = payload => ({
type: RESET_PASSWORD_ERROR,
payload,
});
export const updateProfileInitiated = () => ({
type: UPDATE_PROFILE_INITIATED,
});
export const updateProfileSuccess = () => ({
type: UPDATE_PROFILE_SUCCESS,
});
2 changes: 2 additions & 0 deletions src/actions/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export const SOCIAL_LOGIN_SUCCESS = 'SOCIAL_LOGIN_SUCCESS';
export const GET_PROFILE_PAYLOAD = 'GET_PROFILE_PAYLOAD';
export const GET_PROFILE_INITIATED = 'GET_PROFILE_INITIATED';
export const LOGOUT_USER = 'LOGOUT_USER';
export const UPDATE_PROFILE_INITIATED = 'UPDATE_PROFILE_INITIATED';
export const UPDATE_PROFILE_SUCCESS = 'UPDATE_PROFILE_SUCCESS';

export const CREATE_ARTICLE_SUCCESS = 'CREATE_ARTICLE_SUCCESS';
export const CREATE_ARTICLE_INITIATED = 'CREATE_ARTICLE_INITIATED';
Expand Down
16 changes: 16 additions & 0 deletions src/actions/userActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
sendResetLinkError,
ResetPasswordSuccess,
ResetPasswordError,
updateProfileInitiated,
updateProfileSuccess,
} from './actionCreators';

import axiosInstance from '../config/axiosInstance';
Expand Down Expand Up @@ -173,3 +175,17 @@ export const resetPassword = (passwordDetails) => dispatch => {
return toast.error('Connection Error', { autoClose: 3500, hideProgressBar: true });
});
};

export const updateProfile = profileData => dispatch => {
dispatch(updateProfileInitiated());
return axiosInstance
.put('api/profiles/update/', profileData)
.then((response) => {
dispatch(updateProfileSuccess());
dispatch({ type: GET_PROFILE_PAYLOAD, payload: response.data.profile });
localStorage.setItem('username', response.data.profile.username);
toast.success('Profile update successful', { autoClose: 3500, hideProgressBar: true });
}).catch(() => {
toast.error('Profile update failed', { autoClose: 3500, hideProgressBar: true });
});
};
69 changes: 69 additions & 0 deletions src/components/profiles/EditProfile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import EditProfileForm from './EditProfileForm';
import { updateProfile } from '../../actions/userActions';

class EditProfile extends Component {
constructor(props) {
super(props);
const { username, bio } = this.props;
this.state = {
username,
bio,
};
}

handleSubmit = event => {
event.preventDefault();
const { avatar } = this.props;
const { username, bio } = this.state;
const profileData = {
profile: {
username,
bio,
avatar,
},
};
const { updateProfile } = this.props;
updateProfile(profileData);
};

handleChange = event => {
const { name, value } = event.target;
this.setState({ [name]: value });
}

render() {
const { username, bio } = this.state;
return (
<div>
<EditProfileForm
handleSubmit={this.handleSubmit}
handleChange={this.handleChange}
username={username}
bio={bio}
/>
</div>
);
}
}

EditProfile.propTypes = {
username: PropTypes.string,
bio: PropTypes.string,
avatar: PropTypes.string,
updateProfile: PropTypes.func.isRequired,
};
EditProfile.defaultProps = {
username: '',
bio: '',
avatar: '',
};

const mapStateToProps = state => ({
profileUpdateSuccessful: state.user.profileUpdateSuccessful,
});

export { EditProfile as EditProfileTest };
export default connect(mapStateToProps, { updateProfile })(EditProfile);
60 changes: 60 additions & 0 deletions src/components/profiles/EditProfileForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import PropTypes from 'prop-types';

const EditProfileForm = (props) => {
const {
username, bio, handleChange, handleSubmit,
} = props;
return (
<div className="container">
<div className="row">
<div className="modal fade" id="editProfileModal" role="dialog" aria-hidden="true" style={{ marginTop: 100 }}>
<div className="modal-dialog" role="document">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title" id="editProfileModal">
Personal information
</h5>
<button type="button" className="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div className="modal-body">
<form onSubmit={handleSubmit} id="edit-profile-form">
<div className="form-group">
<label className="control-label" htmlFor="username">Username</label>
<div className="input-group mb-2">
<div className="input-group-prepend">
<div className="input-group-text"><i className="fa fa-user" /></div>
</div>
<input type="text" name="username" className="form-control" id="username" value={username} onChange={handleChange} />
</div>
</div>
<div className="form-group">
<label className="control-label" htmlFor="bio">Bio</label>
<div className="input-group mb-2">
<textarea name="bio" className="form-control" id="bio" value={bio} onChange={handleChange} />
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-danger" data-dismiss="modal">Cancel</button>
<button type="submit" className="btn btn-outline-primary">Save changes</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
);
};

EditProfileForm.propTypes = {
username: PropTypes.string.isRequired,
bio: PropTypes.string.isRequired,
handleChange: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired,
};

export default EditProfileForm;
10 changes: 8 additions & 2 deletions src/components/profiles/Profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import profileImage from '../../assets/images/profileImage.png';
import { getProfile } from '../../actions/userActions';
import { fetchUserArticles } from '../../actions/articleActions';
import ArticlesList from '../Articles/ArticlesList';
import EditProfile from './EditProfile';

export class Profile extends Component {
componentDidMount() {
Expand All @@ -25,7 +26,7 @@ export class Profile extends Component {
render() {
const {
loading, profilePayload: {
followers, following, username, bio,
followers, following, username, bio, avatar,
}, userArticlesPayload,
} = this.props;

Expand Down Expand Up @@ -71,7 +72,12 @@ export class Profile extends Component {
</div>
<div className="row">
<div className="col-12">
<button type="button" className="btn ah-btn">Edit</button>
<button type="button" className="btn-edit" data-toggle="modal" data-target="#editProfileModal">Edit</button>
<EditProfile
username={username}
bio={bio}
avatar={avatar}
/>
</div>
</div>
</div>
Expand Down
14 changes: 14 additions & 0 deletions src/reducers/userReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
SOCIAL_LOGIN_SUCCESS,
GET_PROFILE_PAYLOAD,
GET_PROFILE_INITIATED,
UPDATE_PROFILE_INITIATED,
UPDATE_PROFILE_SUCCESS,
LOGOUT_USER,
SEND_RESET_LINK_INITIATED,
SEND_RESET_LINK_SUCCESS,
Expand All @@ -27,6 +29,7 @@ const initialState = {
sendLinkError: {},
resetPasswordSuccess: false,
resetPasswordError: {},
profileUpdateSuccessful: false,
};


Expand Down Expand Up @@ -76,6 +79,17 @@ const userReducer = (state = initialState, action) => {
...state,
loading: action.payload,
};
case UPDATE_PROFILE_INITIATED:
return {
...state,
loading: true,
};
case UPDATE_PROFILE_SUCCESS:
return {
...state,
loading: false,
profileUpdateSuccessful: true,
};
case LOGOUT_USER:
return {
...state,
Expand Down
58 changes: 58 additions & 0 deletions src/tests/actions/userActions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
facebookLoginUser,
getProfile,
updateLoginStatus,
updateProfile,
} from '../../actions/userActions';
import {
REGISTER_USER_SUCCESS,
Expand All @@ -19,6 +20,8 @@ import {
SOCIAL_LOGIN_SUCCESS,
GET_PROFILE_PAYLOAD,
GET_PROFILE_INITIATED,
UPDATE_PROFILE_INITIATED,
UPDATE_PROFILE_SUCCESS,
} from '../../actions/types';
import axiosInstance from '../../config/axiosInstance';

Expand Down Expand Up @@ -255,4 +258,59 @@ describe('userAction', () => {
],
);
});
it('should update the profile', () => {
const profileData = {
profile: {
username: 'userTwo',
bio: 'updated bio',
avatar: 'https://pixabay.com/en/user-person-people-profile-account-1633249/',
following: ['userOne'],
followers: ['userOne'],
},
};
const expectedActions = [
{ type: UPDATE_PROFILE_INITIATED },
{ type: UPDATE_PROFILE_SUCCESS },
{ type: GET_PROFILE_PAYLOAD },
];
const response = {
profile: {
username: 'user1',
bio: 'My bio',
avatar: 'https://pixabay.com/en/user-person-people-profile-account-1633249/',
},
};
mock.onPut('https://ah-backend-targaryen-staging.herokuapp.com/api/profiles/update/', profileData)
.reply(200, response);
store.dispatch(updateProfile(profileData))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
it('should should not update profile if there is an error', () => {
const profileData = {
profile: {
username: 'user1',
bio: 'My bio',
avatar: 'https://pixabay.com/en/user-person-people-profile-account-1633249/',
following: ['user2'],
followers: ['user2'],
},
};
const expectedActions = [
{ type: UPDATE_PROFILE_INITIATED },
];
const response = {
profile: {
username: 'user1',
bio: 'My bio',
},
};
mock.onPut('https://ah-backend-targaryen-staging.herokuapp.com/api/profiles/update/', profileData)
.reply(400, response);
store.dispatch(updateProfile(profileData))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
});
43 changes: 43 additions & 0 deletions src/tests/components/EditProfile.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react';
import { shallow } from 'enzyme';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { EditProfileTest } from '../../components/profiles/EditProfile';

const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('EditProfile component', () => {
let wrapper;
const props = {
username: 'testuser',
bio: 'bio',
avatar: 'avatar',
updateProfile: jest.fn(),
handleChange: jest.fn(),
};

const getEvent = (name = '', value = '') => ({
preventDefault: jest.fn(),
target: {
name,
value,
},
});

beforeEach(() => {
const store = mockStore({ intitialState: {} });
wrapper = shallow(<EditProfileTest {...props} store={store} />);
});

it('should render correctly', () => {
expect(wrapper).toMatchSnapshot();
});
it('should call updateProfile when handleSubmit is called', () => {
wrapper.instance().handleSubmit(getEvent());
expect(props.updateProfile).toBeCalled();
});
it('should set state when handleChange event is called', () => {
wrapper.instance().handleChange(getEvent('username', 'testuser'));
expect(wrapper.state().username).toEqual('testuser');
});
});
Loading

0 comments on commit 42d71bf

Please sign in to comment.