Skip to content

Commit

Permalink
Upload profile picture
Browse files Browse the repository at this point in the history
add image input field
send request to server to save image
update profile picture with uploaded image
use spinner component to show loading upload
write test for
  • Loading branch information
Tyak99 committed Jul 18, 2019
1 parent a2fd917 commit b3c33f8
Show file tree
Hide file tree
Showing 13 changed files with 418 additions and 177 deletions.
373 changes: 212 additions & 161 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
"@fortawesome/react-fontawesome": "^0.1.4",
"axios": "^0.19.0",
"axios-mock-adapter": "^1.17.0",
"babel-eslint": "^10.0.2",
"bootstrap": "^4.3.1",
"classnames": "^2.2.6",
"clean-webpack-plugin": "^3.0.0",
Expand All @@ -57,7 +56,6 @@
"reactstrap": "^8.0.0",
"redux": "^4.0.1",
"redux-thunk": "^2.3.0",
"regenerator-runtime": "^0.13.2",
"uglifyjs-webpack-plugin": "^2.1.3",
"url-loader": "^2.0.1",
"webpack": "^4.35.2",
Expand Down
55 changes: 54 additions & 1 deletion src/test/actions/__snapshots__/profileAction.spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Array [
"DOB": undefined,
"bio": "Help me",
"email": undefined,
"image": "https://cdn.umnbootcamp.com/wp-content/uploads/sites/93/2018/03/placeholder-person.png",
"image": undefined,
"industry": undefined,
"name": "John Grisham",
"phoneNumber": 7689567890,
Expand Down Expand Up @@ -74,19 +74,72 @@ exports[`profile reducer should test the initial state 1`] = `
Object {
"data": Object {},
"error": null,
"isLoading": false,
}
`;

exports[`profile reducer should test the state for update when get profile fails 1`] = `
Object {
"data": Object {},
"error": undefined,
"isLoading": false,
}
`;

exports[`profile reducer should test the state for update when get profile success 1`] = `
Object {
"data": undefined,
"error": null,
"isLoading": false,
}
`;

exports[`profile reducer should test the state for update when upload image failed 1`] = `
Object {
"data": Object {},
"error": undefined,
"isLoading": false,
}
`;

exports[`profile reducer should test the state for update when upload image failed 2`] = `
Object {
"data": Object {},
"error": null,
"isLoading": true,
}
`;

exports[`profile reducer should test the state for update when upload image success 1`] = `
Object {
"data": Object {
"image": undefined,
},
"error": null,
"isLoading": false,
}
`;

exports[`upload actions dispatch UPLOAD_FAILED with error object 1`] = `
Array [
Object {
"imageUrl": "http//",
"type": "UPLOAD_SUCCESS",
},
Object {
"error": Object {
"error": "unable to upload image",
},
"type": "UPLOAD_FAILED",
},
]
`;

exports[`upload actions dispatch UPLOAD_SUCCESS with image url 1`] = `
Array [
Object {
"imageUrl": "http//",
"type": "UPLOAD_SUCCESS",
},
]
`;
1 change: 1 addition & 0 deletions src/test/actions/authActions/changePassword.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe('test methods', () => {
loading: true,
passwordchangeSuccess: 'please check you email',
passwordChangeError: 'invalid email',
location: { search: 'ddd' },
};

const enzymeWrapper = shallow(<ChangePassword {...props} />);
Expand Down
28 changes: 27 additions & 1 deletion src/test/actions/profileAction.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import nock from 'nock';
import { getProfile, getProfileSuccess } from '../../../store/actions/profile';
import { getProfile, getProfileSuccess, uploadSuccess, uploadFailed } from '../../../store/actions/profile';
import profileReducer, { initialState } from '../../../store/reducers/profile';

const token = 'token';
Expand Down Expand Up @@ -72,6 +72,20 @@ describe('async actions', () => {
});
});

describe('upload actions', () => {
const store = mockStore({});
it('dispatch UPLOAD_SUCCESS with image url', (done) => {
store.dispatch(uploadSuccess('http//'));
expect(store.getActions()).toMatchSnapshot();
done();
});
it('dispatch UPLOAD_FAILED with error object', (done) => {
store.dispatch(uploadFailed({ error: 'unable to upload image' }));
expect(store.getActions()).toMatchSnapshot();
done();
});
});

describe('profile reducer', () => {
it('should test the initial state', () => {
expect(profileReducer(undefined, {})).toMatchSnapshot();
Expand All @@ -82,4 +96,16 @@ describe('profile reducer', () => {
it('should test the state for update when get profile success', () => {
expect(profileReducer(initialState, { type: 'GET_PROFILE_SUCCESS' })).toMatchSnapshot();
});
it('should test the state for update when upload image success', (done) => {
expect(profileReducer(initialState, { type: 'UPLOAD_SUCCESS' })).toMatchSnapshot();
done();
});
it('should test the state for update when upload image failed', (done) => {
expect(profileReducer(initialState, { type: 'UPLOAD_FAILED' })).toMatchSnapshot();
done();
});
it('should test the state for update when upload image failed', (done) => {
expect(profileReducer(initialState, { type: 'UPLOAD_START' })).toMatchSnapshot();
done();
});
});
8 changes: 8 additions & 0 deletions src/test/components/ProfileDetails.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,17 @@ const props = {
name: 'Tyak',
},
};
const wrapper = shallow(<ProfileDetails {...props} />);
describe('component: ProfileDetails', () => {
it('renders correctly', (done) => {
shallow(<ProfileDetails {...props} />);
done();
});
it('tests to find the upload Icon and the input type file', (done) => {
const icon = wrapper.find('#uploadIcon');
const input = wrapper.find('input').first();
expect(icon.length).toBe(1);
expect(input.length).toBe(1);
done();
});
});
25 changes: 22 additions & 3 deletions src/views/Profile/Profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,41 @@ import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Row, Col } from 'reactstrap';
import { Link } from 'react-router-dom';
import Header from '../../components/Header/Header';
import Sidebar from '../../components/Sidebar/Sidebar';
import UserDetails from '../../components/UserDetails/UserDetails';
import ProfileDetails from './ProfileDetails';
import './profile.scss';
import * as actions from '../../../store/actions/profile';
import Button from '../../components/Button';
import Footer from '../../components/Footer/Footer';

export class Profile extends React.Component {
constructor(props) {
super(props);
this.imageFile = React.createRef();
}

componentDidMount() {
const { getProfile, userId } = this.props;
getProfile(userId);
}

render() {
const { profile } = this.props;
const { profile, isLoading, uploadImage } = this.props;
return (
<div>
<Header />
<Row>
<Col md="3" lg="2" sm="3">
<Sidebar>
<ProfileDetails profile={profile} />
<ProfileDetails
profile={profile}
loading={isLoading}
uploadImage={uploadImage}
imageFile={this.imageFile}
/>
</Sidebar>
</Col>
<Col md="9" lg="10" sm="9">
Expand All @@ -35,13 +47,16 @@ export class Profile extends React.Component {
<div className="memberCard">
<h5>
You have not enrolled in any course
<Button type="button" text="Start Journey" />
<Link to="/dashboard">
<Button type="button" text="Start Journey" />
</Link>
</h5>
</div>
<UserDetails profile={profile} />
</div>
</Col>
</Row>
<Footer />
</div>
);
}
Expand All @@ -55,13 +70,17 @@ Profile.propTypes = {
image: PropTypes.string,
}),
userId: PropTypes.number.isRequired,
isLoading: PropTypes.bool,
uploadImage: PropTypes.func,
};
const mapStateToProps = state => ({
profile: state.profile.data,
userId: state.auth.user.userId,
isLoading: state.profile.isLoading,
});

const mapDispatchToProps = dispatch => ({
getProfile: userId => dispatch(actions.getProfile(userId)),
uploadImage: image => dispatch(actions.uploadImage(image)),
});
export default connect(mapStateToProps, mapDispatchToProps)(Profile);
29 changes: 24 additions & 5 deletions src/views/Profile/ProfileDetails.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import PropTypes from 'prop-types';
import './profileDetails.scss';
import Spinner from '../../components/Spinner/Spinner';
import defaultImage from '../../utils/config';
import './profileDetails.scss';


const ProfileDetails = ({ profile }) => (
const ProfileDetails = ({ profile, loading, imageFile, uploadImage }) => (
<div className="profile">
<div className="profileImage">
<img
src={profile.image || defaultImage}
alt="profile"
style={{ filter: loading ? 'blur(4px)' : '' }}
/>
{loading ? (
<div className="spinner-position">
<Spinner />
</div>
) : ''}
<input
type="file"
name="mediaFile"
id="image"
style={{ display: 'none' }}
ref={imageFile}
accept="image/*"
onChange={() => uploadImage(imageFile.current.files[0])}
/>
<FontAwesomeIcon icon="camera" />
<FontAwesomeIcon icon="camera" onClick={() => imageFile.current.click()} id="uploadIcon" />
</div>
<div className="profileDetails">
<h4>
Expand Down Expand Up @@ -44,8 +60,6 @@ const ProfileDetails = ({ profile }) => (
</div>
)
}


</div>
</div>

Expand All @@ -59,5 +73,10 @@ ProfileDetails.propTypes = {
bio: PropTypes.string,
socialMedia: PropTypes.array,
}),
uploadImage: PropTypes.func,
imageFile: PropTypes.shape({
current: PropTypes.object,
}),
loading: PropTypes.bool,
};
export default ProfileDetails;
10 changes: 9 additions & 1 deletion src/views/Profile/profileDetails.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
font-size: 25px;
color: #666666;
}

.spinner-position {
position: absolute;
left: 88px;
top: 80px;
height: 64px;
}
}

.profileDetails {
Expand Down Expand Up @@ -57,4 +64,5 @@
color: cornflowerblue;
}
}
}
}

2 changes: 1 addition & 1 deletion store/actions/authActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const registerUser = (newUser, history) => async dispatch => {
// Set current user
dispatch(setCurrentUser(decoded));

history.push('/modules');
history.push('/dashboard');
}
} catch (err) {
const { error } = err.response.data;
Expand Down
38 changes: 36 additions & 2 deletions store/actions/profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import axios from 'axios';
import * as actionCreators from '../constants/profileContants';

const url = 'https://freyja-ah-backend.herokuapp.com/api/profiles';
const token = localStorage.getItem('jwtToken');
const token = localStorage.getItem('token');
export const getProfileSuccess = profile => ({
type: actionCreators.GET_PROFILE_SUCCESS,
data: profile,
Expand All @@ -27,7 +27,7 @@ export const getProfile = userId => async (dispatch) => {
name: `${user.firstName} ${user.lastName}`,
email: user.email,
bio: user.profile.bio,
image: user.profile.image || 'https://cdn.umnbootcamp.com/wp-content/uploads/sites/93/2018/03/placeholder-person.png',
image: user.profile.image,
username: user.userName,
phoneNumber: user.profile.phoneNumber,
socialMedia: [
Expand All @@ -45,3 +45,37 @@ export const getProfile = userId => async (dispatch) => {
dispatch(getProfileFailed(error));
}
};

export const uploadStart = () => ({
type: actionCreators.UPLOAD_START,
});

export const uploadSuccess = imageUrl => ({
type: actionCreators.UPLOAD_SUCCESS,
imageUrl,
});

export const uploadFailed = error => ({
type: actionCreators.UPLOAD_FAILED,
error,
});

export const uploadImage = image => async (dispatch) => {
dispatch(uploadStart());
const config = {
headers: {
'Content-type': 'multipart/form-data',
Authorization: token,
},
};
try {
const data = new FormData();
data.append('image', image);

const upload = await axios.post('https://freyja-ah-backend.herokuapp.com/api/image', data, config);
const imageUrl = upload.data.data;
dispatch(uploadSuccess(imageUrl));
} catch (error) {
dispatch(uploadFailed(error.response.data));
}
};
Loading

0 comments on commit b3c33f8

Please sign in to comment.