Skip to content

Commit 65448ae

Browse files
mattwills8sexta13
authored andcommitted
Kevin and Mary can upload a picture (#153)
* nightwatch config and write basics of kevin smoke test * update readme and Kevin logged in successfully step * rebase master * minor change to the way we get the selenium server path * chore(git): remove unneeded .gitignore lines * add profile image upload form to profile page * show profile picture first, on hover give option to upload new, on click show upload new form * add profilePicture to User model * missed a comma in the schema definition * add placeholder profile image, display user's profile picture in header * fix some minor linting errors - passes all tests here * merge master * merge master * show profile picture first, on hover give option to upload new, on click show upload new form * add profilePicture to User model * missed a comma in the schema definition * add placeholder profile image, display user's profile picture in header * fix some minor linting errors - passes all tests here * remove redux form in favour of material ui * yarn lock after merge * re-add profile picture avatar after merging with master * run yarn * fix upload dropzone improts * add an h3 to main.scss * style profile page * add upload profile picture functionality to both front end and back end * update upload dropzone to use our scss theme colours * import profile image at top * remove arrow functions from onclick in profule picture form * fix profile picture import * add confirmation stage to profile picture upload flow
1 parent 07315a1 commit 65448ae

File tree

22 files changed

+1168
-376
lines changed

22 files changed

+1168
-376
lines changed

client/actions/user/index.js

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1-
import { UPDATE_USER } from './types'
1+
import * as userTypes from './types'
22

33
import apiOptionsFromState from '../../api/lib/apiOptionsFromState'
44
import usersApiClient from '../../api/users'
55
import ApplyForProjectException from '../../exceptions/ApplyForProjectException'
66

7-
export const updateUser = { type: UPDATE_USER }
7+
export const updateUser = { type: userTypes.UPDATE_USER }
8+
9+
export const uploadingProfilePicture = { type: userTypes.UPLOADING_PROFILE_PICTURE }
10+
export const failedUploadingProfilePicture = { type: userTypes.FAILED_UPLOADING_PROFILE_PICTURE }
11+
export const successUploadingProfilePicture = { type: userTypes.SUCCESS_UPLOADING_PROFILE_PICTURE }
12+
13+
export const updatingProfilePicture = { type: userTypes.UPDATE_PROFILE_PICTURE }
14+
export const failedUpdatingProfilePicture = { type: userTypes.FAILED_UPDATING_PROFILE_PICTURE }
815

916
export default class UserActionCreator {
1017
static applyForProject (project, user) {
@@ -25,4 +32,37 @@ export default class UserActionCreator {
2532
}
2633
}
2734
}
35+
36+
static updateProfilePicture (file) {
37+
return async (dispatch, getState) => {
38+
const filename = file.name.replace(/(\.[\w\d_-]+)$/i, '_profilePicture_' + Date.now() + '$1')
39+
const state = getState()
40+
const apiOptions = apiOptionsFromState(state)
41+
42+
// try uploading image to server
43+
dispatch(uploadingProfilePicture)
44+
try {
45+
const url = await usersApiClient.getPresignedUrlForUserProfilePicture(apiOptions, filename)
46+
await usersApiClient.uploadImage(apiOptions, url, file)
47+
} catch (e) {
48+
console.log(e)
49+
dispatch(failedUploadingProfilePicture)
50+
}
51+
dispatch(successUploadingProfilePicture)
52+
53+
// try updating user profile picture in db
54+
dispatch(updatingProfilePicture)
55+
try {
56+
const updatedUser = await usersApiClient.updateUserProfilePictureFilename(apiOptions, filename)
57+
58+
dispatch({
59+
...updateUser,
60+
payload: updatedUser
61+
})
62+
} catch (e) {
63+
console.log(e)
64+
dispatch(failedUpdatingProfilePicture)
65+
}
66+
}
67+
}
2868
}

client/actions/user/types.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,9 @@
11
export const UPDATE_USER = 'UPDATE_USER'
2+
3+
export const UPLOADING_PROFILE_PICTURE = 'UPLOADING_PROFILE_PICTURE'
4+
export const FAILED_UPLOADING_PROFILE_PICTURE = 'FAILED_UPLOADING_PROFILE_PICTURE'
5+
export const SUCCESS_UPLOADING_PROFILE_PICTURE = 'SUCCESS_UPLOADING_PROFILE_PICTURE'
6+
7+
export const UPDATE_PROFILE_PICTURE = 'UPDATE_PROFILE_PICTURE_IN_DB'
8+
export const FAILED_UPDATING_PROFILE_PICTURE = 'FAILED_UPDATING_PROFILE_PICTURE_IN_DB'
9+
export const SUCCESS_UPDATING_PROFILE_PICTURE = 'SUCCESS_UPDATING_PROFILE_PICTURE_IN_DB'

client/api/users.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,23 @@ const usersApiClient = {
3030
return apiRequest.post('/users', apiOptions, body)
3131
},
3232

33+
async getPresignedUrlForUserProfilePicture (apiOptions, imageName) {
34+
const query = { imageName }
35+
return apiRequest.get('/users/presignedUrl', apiOptions, query)
36+
},
37+
38+
async uploadImage (apiOptions, url, file) {
39+
return fetch(url, {
40+
method: 'PUT',
41+
body: file
42+
})
43+
},
44+
45+
async updateUserProfilePictureFilename (apiOptions, filename) {
46+
const body = { profilePicture: filename }
47+
return apiRequest.put(`/users/profilePicture`, apiOptions, body)
48+
},
49+
3350
applyForProject (apiOptions, projectId) {
3451
return apiRequest.post(`/users/apply/${projectId}`, apiOptions)
3552
},

client/components/Header/Header.js

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import PropTypes from 'prop-types'
55
import AppBar from 'material-ui/AppBar'
66
import Toolbar from 'material-ui/Toolbar'
77
import Typography from 'material-ui/Typography'
8+
import Avatar from 'material-ui/Avatar'
89
import Badge from 'material-ui/Badge'
910
import Button from 'material-ui/Button'
10-
11+
import defaultProfileImage from '../../images/profile-image.jpg'
1112
import AuthActionCreator from '../../actions/auth'
1213
import styles from './Header.scss'
1314
import ApplicationActionCreator from '../../actions/application'
@@ -35,8 +36,7 @@ class Header extends Component {
3536
<Link
3637
key="show-applications"
3738
to="/show-applications"
38-
className={this.getLinkStyles('show-project')}
39-
>
39+
className={this.getLinkStyles('show-project')}>
4040
{applications.notSeenCounter > 0 ? (
4141
<Badge badgeContent={applications.notSeenCounter} color="primary">
4242
<span className={styles.applicationBadgeText}>APPLICATIONS</span>
@@ -56,16 +56,18 @@ class Header extends Component {
5656
key="logout"
5757
to="/"
5858
onClick={this.props.logout}
59-
className={this.getLinkStyles()}
60-
>
59+
className={this.getLinkStyles()}>
6160
LOG OUT
6261
</Link>,
6362
<Link
6463
key="profile"
6564
to="/profile"
66-
className={this.getLinkStyles('profile')}
67-
>
68-
PROFILE
65+
className={this.getLinkStyles('profile')}>
66+
<Avatar
67+
src={
68+
this.props.user.profilePicture || defaultProfileImage
69+
}
70+
/>
6971
</Link>
7072
]
7173
if (this.props.user.usertype === 'contact') {
@@ -74,8 +76,7 @@ class Header extends Component {
7476
<Link
7577
key="create-project"
7678
to="/create-project"
77-
className={this.getLinkStyles('create-project')}
78-
>
79+
className={this.getLinkStyles('create-project')}>
7980
CREATE PROJECT
8081
</Link>,
8182
...authButtons
@@ -88,8 +89,7 @@ class Header extends Component {
8889
<Link
8990
key="projects"
9091
to="/projects"
91-
className={this.getLinkStyles('projects')}
92-
>
92+
className={this.getLinkStyles('projects')}>
9393
PROJECTS
9494
</Link>,
9595
<Link key="login" to="/login" className={this.getLinkStyles('login')}>
@@ -99,8 +99,7 @@ class Header extends Component {
9999
key="signup"
100100
to="/signup"
101101
component={Link}
102-
className={styles.navButton}
103-
>
102+
className={styles.navButton}>
104103
SIGN UP
105104
</Button>
106105
]

client/components/ListOfProjects/ListOfProjects.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,18 @@ class ListOfProjects extends Component {
2626
<Project
2727
project={project}
2828
authenticated={this.props.authenticated}
29-
applyForProject={this.applyToProject} />
29+
applyForProject={this.applyToProject}
30+
/>
3031
</div>
3132
)
3233
})
3334
}
3435

3536
render () {
3637
return (
37-
<section className={styles.projectListSection}>
38+
<section className={`${styles.projectListSection} project-list-section`}>
3839
<div className={styles.listContainer}>
39-
<div className={styles.list}>
40-
{ this.renderListOfProjects() }
41-
</div>
40+
<div className={styles.list}>{this.renderListOfProjects()}</div>
4241
</div>
4342
</section>
4443
)

client/components/Profile/Profile.js

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,35 @@ import { connect } from 'react-redux'
33
import PropTypes from 'prop-types'
44

55
import ProjectActionCreator from '../../actions/project'
6+
import UserActionCreator from '../../actions/user'
7+
import ProfilePictureForm from './ProfilePictureForm/ProfilePictureForm'
68
import ListOfProjects from '../ListOfProjects/ListOfProjects'
9+
import styles from './Profile.scss'
710

811
class Profile extends Component {
912
componentDidMount () {
1013
this.props.onLoad(this.props.user)
1114
}
1215

1316
render () {
14-
const title = this.props.user.usertype === 'contact'
15-
? 'Your Organization\'s Projects'
16-
: 'Projects Applied For'
17+
const title =
18+
this.props.user.usertype === 'contact'
19+
? "Your Organization's Projects"
20+
: 'Projects Applied For'
1721

1822
return (
19-
<ListOfProjects
20-
title={title}
21-
projects={this.props.projects} />
23+
<div className={styles.profilePage}>
24+
<div className={styles.profilePictureForm}>
25+
<ProfilePictureForm
26+
user={this.props.user}
27+
updateProfilePicture={this.props.updateProfilePicture} />
28+
</div>
29+
30+
<div className={styles.listOfProjects}>
31+
<h3 className={styles.youProjects}>Your Projects</h3>
32+
<ListOfProjects title={title} projects={this.props.projects} />
33+
</div>
34+
</div>
2235
)
2336
}
2437
}
@@ -31,11 +44,13 @@ function mapStateToProps (state) {
3144
}
3245

3346
const mapDispatchToProps = {
34-
onLoad: ProjectActionCreator.fetchProfileProjects
47+
onLoad: ProjectActionCreator.fetchProfileProjects,
48+
updateProfilePicture: UserActionCreator.updateProfilePicture
3549
}
3650

3751
Profile.propTypes = {
3852
onLoad: PropTypes.func,
53+
updateProfilePicture: PropTypes.func,
3954
projects: PropTypes.array,
4055
user: PropTypes.object
4156
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
.profilePage {
2+
display: grid;
3+
grid-template-columns: 30% repeat(2, 1fr);
4+
grid-template-rows: auto;
5+
grid-row-gap: 15px;
6+
padding: 0;
7+
8+
h3 {
9+
margin-top: 20px;
10+
margin-bottom: 20px;
11+
text-align: center;
12+
}
13+
14+
.profilePictureForm {
15+
grid-column: 1 / 2;
16+
grid-row: 1 / 2;
17+
text-align: center;
18+
}
19+
20+
.listOfProjects {
21+
grid-column: 2 / 4;
22+
grid-row: 1 / 4;
23+
padding-top: 20px;
24+
25+
h3 {
26+
margin-top: 0;
27+
width: 93%;
28+
}
29+
30+
:global(.project-list-section) {
31+
padding: 0;
32+
}
33+
}
34+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import React, { Component } from 'react'
2+
import PropTypes from 'prop-types'
3+
import defaultProfileImage from '../../../images/profile-image.jpg'
4+
import VerticalAlignHelper from '../../shared/VerticalAlignHelper/VerticalAlignHelper'
5+
import UploadDropzone from '../../UploadDropzone/UploadDropzone'
6+
import styles from './ProfilePictureForm.scss'
7+
8+
class ProfilePictureForm extends Component {
9+
constructor (props) {
10+
super(props)
11+
12+
this.state = {
13+
showForm: false,
14+
showConfirmation: false
15+
}
16+
}
17+
18+
handleFormSubmit (event) {
19+
event.preventDefault()
20+
}
21+
22+
handleUploadClick (event) {
23+
this.setState({ showForm: true, showConfirmation: false })
24+
}
25+
26+
saveFile (file) {
27+
this.file = file
28+
this.setState({ showConfirmation: true })
29+
}
30+
31+
async handleConfirmationClick (event) {
32+
event.preventDefault()
33+
await this.uploadImage(this.file)
34+
this.setState({ showForm: false, showConfirmation: false })
35+
}
36+
37+
async uploadImage (file) {
38+
if (!file) {
39+
return this.setState({ showForm: true, showConfirmation: false })
40+
}
41+
await this.props.updateProfilePicture(file)
42+
}
43+
44+
renderImageUpload (field) {
45+
return (
46+
<UploadDropzone
47+
className={styles.inputImageUpload}
48+
saveFile={this.saveFile.bind(this)}
49+
/>
50+
)
51+
}
52+
53+
renderForm () {
54+
return (
55+
<form
56+
id="uploadProfilePictureForm"
57+
className={styles.form}
58+
onSubmit={this.handleFormSubmit.bind(this)} >
59+
{this.renderImageUpload()}
60+
</form>
61+
)
62+
}
63+
64+
renderProfilePicture () {
65+
const { user } = this.props
66+
return (
67+
<div
68+
className={styles.profilePicture}
69+
onClick={this.handleUploadClick.bind(this)}>
70+
<div className={styles.profilePictureInner}>
71+
<img
72+
className={styles.image}
73+
src={
74+
user.profilePicture || defaultProfileImage
75+
}
76+
/>
77+
78+
<div className={styles.imageHoverCover} />
79+
80+
<div className={styles.uploadNewButton}>
81+
<VerticalAlignHelper />
82+
<button>Upload New</button>
83+
</div>
84+
</div>
85+
</div>
86+
)
87+
}
88+
89+
render () {
90+
const render = this.state.showForm
91+
? this.renderForm()
92+
: this.renderProfilePicture()
93+
94+
return (
95+
<div className={styles.profilePictureWrapper}>
96+
<h3 className={styles.formHeading}>You</h3>
97+
{render}
98+
{this.state.showConfirmation && (
99+
<button
100+
className={styles.confirmation}
101+
onClick={this.handleConfirmationClick.bind(this)}>
102+
Confirm
103+
</button>
104+
)}
105+
</div>
106+
)
107+
}
108+
}
109+
110+
ProfilePictureForm.propTypes = {
111+
user: PropTypes.object,
112+
updateProfilePicture: PropTypes.func.isRequired
113+
}
114+
115+
export default ProfilePictureForm

0 commit comments

Comments
 (0)