From 0ca26678f8ca6ba0194677bbc6610685d4e3d04e Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Mon, 4 Sep 2017 15:33:47 +0200 Subject: [PATCH 01/18] EditListingPage renamed to ProfileSettingsPage --- src/app.test.js | 2 +- .../ProfileSettingsPage.js} | 18 ++++++++---------- .../ProfileSettingsPage.test.js} | 4 ++-- .../ProfileSettingsPage.test.js.snap} | 2 +- src/containers/index.js | 4 ++-- src/routesConfiguration.js | 18 ++++++++---------- 6 files changed, 22 insertions(+), 26 deletions(-) rename src/containers/{EditProfilePage/EditProfilePage.js => ProfileSettingsPage/ProfileSettingsPage.js} (83%) rename src/containers/{EditProfilePage/EditProfilePage.test.js => ProfileSettingsPage/ProfileSettingsPage.test.js} (84%) rename src/containers/{EditProfilePage/__snapshots__/EditProfilePage.test.js.snap => ProfileSettingsPage/__snapshots__/ProfileSettingsPage.test.js.snap} (90%) diff --git a/src/app.test.js b/src/app.test.js index ee59940a..d86efbe9 100644 --- a/src/app.test.js +++ b/src/app.test.js @@ -67,7 +67,7 @@ describe('Application', () => { '/l/new': defaultAuthPath, '/l/listing-title-slug/1234/new/description': defaultAuthPath, '/l/listing-title-slug/1234/checkout': defaultAuthPath, - '/u/1234/edit': defaultAuthPath, + '/profile-settings': defaultAuthPath, '/inbox': defaultAuthPath, '/inbox/orders': defaultAuthPath, '/inbox/sales': defaultAuthPath, diff --git a/src/containers/EditProfilePage/EditProfilePage.js b/src/containers/ProfileSettingsPage/ProfileSettingsPage.js similarity index 83% rename from src/containers/EditProfilePage/EditProfilePage.js rename to src/containers/ProfileSettingsPage/ProfileSettingsPage.js index a44c6e6b..f736e7d8 100644 --- a/src/containers/EditProfilePage/EditProfilePage.js +++ b/src/containers/ProfileSettingsPage/ProfileSettingsPage.js @@ -7,7 +7,7 @@ import { logout, authenticationInProgress } from '../../ducks/Auth.duck'; import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck'; import { PageLayout, Topbar } from '../../components'; -export const EditProfilePageComponent = props => { +export const ProfileSettingsPageComponent = props => { const { authInfoError, authInProgress, @@ -20,14 +20,13 @@ export const EditProfilePageComponent = props => { notificationCount, onLogout, onManageDisableScrolling, - params, } = props; return ( { ); }; -EditProfilePageComponent.defaultProps = { +ProfileSettingsPageComponent.defaultProps = { authInfoError: null, currentUser: null, logoutError: null, notificationCount: 0, }; -const { bool, func, instanceOf, number, object, shape, string } = PropTypes; +const { bool, func, instanceOf, number, object, shape } = PropTypes; -EditProfilePageComponent.propTypes = { +ProfileSettingsPageComponent.propTypes = { authInfoError: instanceOf(Error), authInProgress: bool.isRequired, currentUser: propTypes.currentUser, @@ -63,7 +62,6 @@ EditProfilePageComponent.propTypes = { notificationCount: number, onLogout: func.isRequired, onManageDisableScrolling: func.isRequired, - params: shape({ displayName: string.isRequired }).isRequired, // from withRouter history: shape({ @@ -99,8 +97,8 @@ const mapDispatchToProps = dispatch => ({ dispatch(manageDisableScrolling(componentId, disableScrolling)), }); -const EditProfilePage = compose(connect(mapStateToProps, mapDispatchToProps), withRouter)( - EditProfilePageComponent +const ProfileSettingsPage = compose(connect(mapStateToProps, mapDispatchToProps), withRouter)( + ProfileSettingsPageComponent ); -export default EditProfilePage; +export default ProfileSettingsPage; diff --git a/src/containers/EditProfilePage/EditProfilePage.test.js b/src/containers/ProfileSettingsPage/ProfileSettingsPage.test.js similarity index 84% rename from src/containers/EditProfilePage/EditProfilePage.test.js rename to src/containers/ProfileSettingsPage/ProfileSettingsPage.test.js index cca70454..d9ef3ce0 100644 --- a/src/containers/EditProfilePage/EditProfilePage.test.js +++ b/src/containers/ProfileSettingsPage/ProfileSettingsPage.test.js @@ -1,13 +1,13 @@ import React from 'react'; import { renderShallow } from '../../util/test-helpers'; -import { EditProfilePageComponent } from './EditProfilePage'; +import { ProfileSettingsPageComponent } from './ProfileSettingsPage'; const noop = () => null; describe('ContactDetailsPage', () => { it('matches snapshot', () => { const tree = renderShallow( - + title="Profile settings"> , - routes: [ - { - path: '/u/:displayName/edit', - auth: true, - exact: true, - name: 'EditProfilePage', - component: props => , - }, - ], }, ], }, + { + path: '/profile-settings', + auth: true, + exact: true, + name: 'ProfileSettingsPage', + component: props => , + }, { path: '/login', exact: true, From d8ac958e37315a6a285f3db959701bc68d40ce49 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Mon, 4 Sep 2017 15:34:24 +0200 Subject: [PATCH 02/18] UserNav added to ProfileSettingsPage --- src/components/UserNav/UserNav.js | 7 +++---- src/containers/ProfileSettingsPage/ProfileSettingsPage.js | 3 ++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/UserNav/UserNav.js b/src/components/UserNav/UserNav.js index 9a85be15..8a86bd8f 100644 --- a/src/components/UserNav/UserNav.js +++ b/src/components/UserNav/UserNav.js @@ -12,11 +12,10 @@ const UserNav = props => { const tabs = [ { text: , - selected: selectedPageName === 'EditProfilePage', - disabled: true, + selected: selectedPageName === 'ProfileSettingsPage', + disabled: false, linkProps: { - name: 'EditProfilePage', - params: { displayName: 'testinglink' }, + name: 'ProfileSettingsPage', }, }, { diff --git a/src/containers/ProfileSettingsPage/ProfileSettingsPage.js b/src/containers/ProfileSettingsPage/ProfileSettingsPage.js index f736e7d8..4db64e70 100644 --- a/src/containers/ProfileSettingsPage/ProfileSettingsPage.js +++ b/src/containers/ProfileSettingsPage/ProfileSettingsPage.js @@ -5,7 +5,7 @@ import { withRouter } from 'react-router-dom'; import * as propTypes from '../../util/propTypes'; import { logout, authenticationInProgress } from '../../ducks/Auth.duck'; import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck'; -import { PageLayout, Topbar } from '../../components'; +import { PageLayout, Topbar, UserNav } from '../../components'; export const ProfileSettingsPageComponent = props => { const { @@ -39,6 +39,7 @@ export const ProfileSettingsPageComponent = props => { onLogout={onLogout} onManageDisableScrolling={onManageDisableScrolling} /> + ); }; From d11d53866225fbde99900379e15550c541f1ef72 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Mon, 4 Sep 2017 15:55:08 +0200 Subject: [PATCH 03/18] Profile settings link to TopbarDesktop --- src/components/TopbarDesktop/TopbarDesktop.css | 1 + src/components/TopbarDesktop/TopbarDesktop.js | 11 ++++++++++- .../ProfileSettingsPage/ProfileSettingsPage.js | 1 + src/translations/en.json | 1 + 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/TopbarDesktop/TopbarDesktop.css b/src/components/TopbarDesktop/TopbarDesktop.css index 1fcdb986..63bf711e 100644 --- a/src/components/TopbarDesktop/TopbarDesktop.css +++ b/src/components/TopbarDesktop/TopbarDesktop.css @@ -237,6 +237,7 @@ transition: width var(--transitionStyleButton); } +.profileSettingsLink, .yourListingsLink { @apply --marketplaceH4FontStyles; position: relative; diff --git a/src/components/TopbarDesktop/TopbarDesktop.js b/src/components/TopbarDesktop/TopbarDesktop.js index 04f7cd01..c6bf6845 100644 --- a/src/components/TopbarDesktop/TopbarDesktop.js +++ b/src/components/TopbarDesktop/TopbarDesktop.js @@ -68,7 +68,16 @@ const TopbarDesktop = props => { - + + + + + + + { > Date: Mon, 4 Sep 2017 18:00:03 +0200 Subject: [PATCH 04/18] Profile settings link to TopbarMobileMenu + adding missing status to PageLayout on those pages --- src/components/TopbarDesktop/TopbarDesktop.js | 5 ++++- .../__snapshots__/TopbarDesktop.test.js.snap | 16 ++++++++++++++ .../TopbarMobileMenu/TopbarMobileMenu.js | 6 +++++ .../ManageListingsPage/ManageListingsPage.js | 22 +++++++++++++------ .../ManageListingsPage.test.js.snap | 4 +++- .../ProfileSettingsPage.js | 3 +++ src/translations/en.json | 1 + 7 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/components/TopbarDesktop/TopbarDesktop.js b/src/components/TopbarDesktop/TopbarDesktop.js index c6bf6845..4f8f2c19 100644 --- a/src/components/TopbarDesktop/TopbarDesktop.js +++ b/src/components/TopbarDesktop/TopbarDesktop.js @@ -70,7 +70,10 @@ const TopbarDesktop = props => { diff --git a/src/components/TopbarDesktop/__snapshots__/TopbarDesktop.test.js.snap b/src/components/TopbarDesktop/__snapshots__/TopbarDesktop.test.js.snap index ac36ad45..974488a2 100644 --- a/src/components/TopbarDesktop/__snapshots__/TopbarDesktop.test.js.snap +++ b/src/components/TopbarDesktop/__snapshots__/TopbarDesktop.test.js.snap @@ -102,6 +102,22 @@ exports[`TopbarDesktop data matches snapshot 1`] = ` style={Object {}}>
    +
  • + + + + Profile settings + + +
  • diff --git a/src/components/TopbarMobileMenu/TopbarMobileMenu.js b/src/components/TopbarMobileMenu/TopbarMobileMenu.js index 229dacee..798de732 100644 --- a/src/components/TopbarMobileMenu/TopbarMobileMenu.js +++ b/src/components/TopbarMobileMenu/TopbarMobileMenu.js @@ -93,6 +93,12 @@ const TopbarMobileMenu = props => { > + + +
    diff --git a/src/containers/ManageListingsPage/ManageListingsPage.js b/src/containers/ManageListingsPage/ManageListingsPage.js index b5ae9faf..7d640dac 100644 --- a/src/containers/ManageListingsPage/ManageListingsPage.js +++ b/src/containers/ManageListingsPage/ManageListingsPage.js @@ -38,6 +38,7 @@ export class ManageListingsPageComponent extends Component { const { authInfoError, authInProgress, + closingListing, currentUser, currentUserHasListings, history, @@ -47,15 +48,15 @@ export class ManageListingsPageComponent extends Component { logoutError, notificationCount, onCloseListing, - onOpenListing, onLogout, onManageDisableScrolling, + onOpenListing, + openingListing, pagination, queryInProgress, queryListingsError, queryParams, - openingListing, - closingListing, + scrollingDisabled, } = this.props; // TODO Handle openingListingError, closingListingError, @@ -103,7 +104,12 @@ export class ManageListingsPageComponent extends Component { const listingMenuOpen = this.state.listingMenuOpen; return ( - + {queryInProgress ? loadingResults : null} @@ -161,6 +168,7 @@ const { arrayOf, bool, func, instanceOf, number, object, shape, string } = PropT ManageListingsPageComponent.propTypes = { authInfoError: instanceOf(Error), authInProgress: bool.isRequired, + closingListing: shape({ uuid: string.isRequired }), currentUser: propTypes.currentUser, currentUserHasListings: bool.isRequired, isAuthenticated: bool.isRequired, @@ -168,15 +176,15 @@ ManageListingsPageComponent.propTypes = { logoutError: instanceOf(Error), notificationCount: number, onCloseListing: func.isRequired, - onOpenListing: func.isRequired, onLogout: func.isRequired, onManageDisableScrolling: func.isRequired, + onOpenListing: func.isRequired, + openingListing: shape({ uuid: string.isRequired }), pagination: propTypes.pagination, queryInProgress: bool.isRequired, queryListingsError: instanceOf(Error), queryParams: object, - closingListing: shape({ uuid: string.isRequired }), - openingListing: shape({ uuid: string.isRequired }), + scrollingDisabled: bool.isRequired, // from withRouter history: shape({ diff --git a/src/containers/ManageListingsPage/__snapshots__/ManageListingsPage.test.js.snap b/src/containers/ManageListingsPage/__snapshots__/ManageListingsPage.test.js.snap index f547bb3a..2d9d1d1b 100644 --- a/src/containers/ManageListingsPage/__snapshots__/ManageListingsPage.test.js.snap +++ b/src/containers/ManageListingsPage/__snapshots__/ManageListingsPage.test.js.snap @@ -2,6 +2,7 @@ exports[`ContactDetailsPage matches snapshot 1`] = ` + onManageDisableScrolling={[Function]} + scrollingDisabled={false} /> { notificationCount, onLogout, onManageDisableScrolling, + scrollingDisabled, } = props; return ( @@ -27,6 +28,7 @@ export const ProfileSettingsPageComponent = props => { authInfoError={authInfoError} logoutError={logoutError} title="Profile settings" + scrollingDisabled={scrollingDisabled} > Date: Tue, 5 Sep 2017 15:13:07 +0300 Subject: [PATCH 05/18] ProfileSettingsPage: added duck file --- .../ProfileSettingsPage.duck.js | 78 +++++++++++++++++++ .../ProfileSettingsPage.js | 29 +++++++ 2 files changed, 107 insertions(+) create mode 100644 src/containers/ProfileSettingsPage/ProfileSettingsPage.duck.js diff --git a/src/containers/ProfileSettingsPage/ProfileSettingsPage.duck.js b/src/containers/ProfileSettingsPage/ProfileSettingsPage.duck.js new file mode 100644 index 00000000..b4e92d26 --- /dev/null +++ b/src/containers/ProfileSettingsPage/ProfileSettingsPage.duck.js @@ -0,0 +1,78 @@ +const requestAction = actionType => params => ({ type: actionType, payload: { params } }); +const successAction = actionType => result => ({ type: actionType, payload: result.data }); +const errorAction = actionType => error => ({ type: actionType, payload: error, error: true }); + +// ================ Action types ================ // + +export const UPLOAD_IMAGE_REQUEST = 'app/EditListingPage/UPLOAD_IMAGE_REQUEST'; +export const UPLOAD_IMAGE_SUCCESS = 'app/EditListingPage/UPLOAD_IMAGE_SUCCESS'; +export const UPLOAD_IMAGE_ERROR = 'app/EditListingPage/UPLOAD_IMAGE_ERROR'; + +export const REMOVE_LISTING_IMAGE = 'app/EditListingPage/REMOVE_LISTING_IMAGE'; + +// ================ Reducer ================ // + +const initialState = { + // Error instance placeholders for each endpoint + uploadImageError: null, + image: {}, + updateInProgress: false, +}; + +export default function reducer(state = initialState, action = {}) { + const { type, payload } = action; + switch (type) { + + case UPLOAD_IMAGE_REQUEST: { + // payload.params: { id: 'tempId', file } + const image = { + [payload.params.id]: { ...payload.params }, + }; + + return { + ...state, + image, + updateInProgress: true, + uploadImageError: null, + }; + } + case UPLOAD_IMAGE_SUCCESS: { + // payload.params: { id: 'tempId', imageId: 'some-real-id'} + const { id, imageId } = payload; + const file = state.image[id].file; + const image = { [id]: { id, imageId, file } }; + return { ...state, image, updateInProgress: false }; + } + case UPLOAD_IMAGE_ERROR: { + // eslint-disable-next-line no-console + return { ...state, image: null, updateInProgress: false , uploadImageError: payload.error }; + } + + default: + return state; + } +} + +// ================ Selectors ================ // + +// ================ Action creators ================ // + +// SDK method: listings.uploadImage +export const uploadImage = requestAction(UPLOAD_IMAGE_REQUEST); +export const uploadImageSuccess = successAction(UPLOAD_IMAGE_SUCCESS); +export const uploadImageError = errorAction(UPLOAD_IMAGE_ERROR); + +// ================ Thunk ================ // + +// Images return imageId which we need to map with previously generated temporary id +export function requestImageUpload(actionPayload) { + return (dispatch, getState, sdk) => { + const id = actionPayload.id; + dispatch(uploadImage(actionPayload)); + + return sdk.images + .uploadListingImage({ image: actionPayload.file }) + .then(resp => dispatch(uploadImageSuccess({ data: { id, imageId: resp.data.data.id } }))) + .catch(e => dispatch(uploadImageError({ id, error: e }))); + }; +} diff --git a/src/containers/ProfileSettingsPage/ProfileSettingsPage.js b/src/containers/ProfileSettingsPage/ProfileSettingsPage.js index b4b54b9c..1d5b62c1 100644 --- a/src/containers/ProfileSettingsPage/ProfileSettingsPage.js +++ b/src/containers/ProfileSettingsPage/ProfileSettingsPage.js @@ -5,8 +5,24 @@ import { withRouter } from 'react-router-dom'; import * as propTypes from '../../util/propTypes'; import { logout, authenticationInProgress } from '../../ducks/Auth.duck'; import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck'; +import { requestImageUpload } from './ProfileSettingsPage.duck'; import { PageLayout, Topbar, UserNav } from '../../components'; +const ACCEPT_IMAGES = 'image/*'; + +const onImageUploadHandler = (event, fn) => { + const file = event.target.files[0]; + if (file) { + fn({ id: `${file.name}_${Date.now()}`, file }) + .then(response => { + console.log('Response:', response); + }) + .catch(error => { + console.log('Error:', error); + }); + } +} + export const ProfileSettingsPageComponent = props => { const { authInfoError, @@ -18,6 +34,7 @@ export const ProfileSettingsPageComponent = props => { location, logoutError, notificationCount, + onImageUpload, onLogout, onManageDisableScrolling, scrollingDisabled, @@ -43,6 +60,16 @@ export const ProfileSettingsPageComponent = props => { onManageDisableScrolling={onManageDisableScrolling} /> + + onImageUploadHandler(e, onImageUpload)} + type="file" + disabled={false} + + /> ); }; @@ -64,6 +91,7 @@ ProfileSettingsPageComponent.propTypes = { isAuthenticated: bool.isRequired, logoutError: instanceOf(Error), notificationCount: number, + onImageUpload: func.isRequired, onLogout: func.isRequired, onManageDisableScrolling: func.isRequired, scrollingDisabled: bool.isRequired, @@ -97,6 +125,7 @@ const mapStateToProps = state => { }; const mapDispatchToProps = dispatch => ({ + onImageUpload: data => dispatch(requestImageUpload(data)), onLogout: historyPush => dispatch(logout(historyPush)), onManageDisableScrolling: (componentId, disableScrolling) => dispatch(manageDisableScrolling(componentId, disableScrolling)), From 0208100a86c292fdcc64f39c835224e8df3cb6ab Mon Sep 17 00:00:00 2001 From: Hannu Lyytikainen Date: Tue, 5 Sep 2017 16:04:24 +0300 Subject: [PATCH 06/18] Add ProfileSettingsPage reducer --- src/containers/reducers.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/containers/reducers.js b/src/containers/reducers.js index c54cc573..c0d47bc4 100644 --- a/src/containers/reducers.js +++ b/src/containers/reducers.js @@ -11,6 +11,7 @@ import OrderPage from './OrderPage/OrderPage.duck'; import SalePage from './SalePage/SalePage.duck'; import SearchPage from './SearchPage/SearchPage.duck'; import ManageListingsPage from './ManageListingsPage/ManageListingsPage.duck'; +import ProfileSettingsPage from './ProfileSettingsPage/ProfileSettingsPage.duck'; export { CheckoutPage, @@ -19,6 +20,7 @@ export { ListingPage, ManageListingsPage, OrderPage, + ProfileSettingsPage, SalePage, SearchPage, }; From 01f189528fb0221c4200450a55ade7d062eb2b4d Mon Sep 17 00:00:00 2001 From: Hannu Lyytikainen Date: Tue, 5 Sep 2017 16:06:04 +0300 Subject: [PATCH 07/18] Invoke profile image upload in API --- package.json | 2 +- .../ProfileSettingsPage/ProfileSettingsPage.duck.js | 2 +- yarn.lock | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 469b2be9..156500da 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "redux-thunk": "^2.2.0", "sanitize.css": "^5.0.0", "sharetribe-scripts": "0.9.2", - "sharetribe-sdk": "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#917869bbbfc791a38da193c956645779864c049d", + "sharetribe-sdk": "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#0ae201201c03c6031bf9c1ab281470dca9015621", "source-map-support": "^0.4.15", "url": "^0.11.0" }, diff --git a/src/containers/ProfileSettingsPage/ProfileSettingsPage.duck.js b/src/containers/ProfileSettingsPage/ProfileSettingsPage.duck.js index b4e92d26..ce49561d 100644 --- a/src/containers/ProfileSettingsPage/ProfileSettingsPage.duck.js +++ b/src/containers/ProfileSettingsPage/ProfileSettingsPage.duck.js @@ -71,7 +71,7 @@ export function requestImageUpload(actionPayload) { dispatch(uploadImage(actionPayload)); return sdk.images - .uploadListingImage({ image: actionPayload.file }) + .uploadProfileImage({ image: actionPayload.file }) .then(resp => dispatch(uploadImageSuccess({ data: { id, imageId: resp.data.data.id } }))) .catch(e => dispatch(uploadImageError({ id, error: e }))); }; diff --git a/yarn.lock b/yarn.lock index 98c75638..f02a1d9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6242,9 +6242,9 @@ sharetribe-scripts@0.9.2: optionalDependencies: fsevents "1.0.17" -"sharetribe-sdk@git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#917869bbbfc791a38da193c956645779864c049d": +"sharetribe-sdk@git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#0ae201201c03c6031bf9c1ab281470dca9015621": version "0.0.1" - resolved "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#917869bbbfc791a38da193c956645779864c049d" + resolved "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#0ae201201c03c6031bf9c1ab281470dca9015621" dependencies: axios "^0.15.3" js-cookie "^2.1.3" From d5b905ae2de02dc66cd367b89d5a6a90c446c862 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Wed, 6 Sep 2017 13:50:18 +0300 Subject: [PATCH 08/18] package.json to point sdk that has updateProfile --- package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 156500da..502c2a96 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "redux-thunk": "^2.2.0", "sanitize.css": "^5.0.0", "sharetribe-scripts": "0.9.2", - "sharetribe-sdk": "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#0ae201201c03c6031bf9c1ab281470dca9015621", + "sharetribe-sdk": "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#0bc2a2e82be2c5eadbc22e511a750a2a91dc0fd5", "source-map-support": "^0.4.15", "url": "^0.11.0" }, diff --git a/yarn.lock b/yarn.lock index f02a1d9e..9ee6d345 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6242,9 +6242,9 @@ sharetribe-scripts@0.9.2: optionalDependencies: fsevents "1.0.17" -"sharetribe-sdk@git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#0ae201201c03c6031bf9c1ab281470dca9015621": +"sharetribe-sdk@git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#0bc2a2e82be2c5eadbc22e511a750a2a91dc0fd5": version "0.0.1" - resolved "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#0ae201201c03c6031bf9c1ab281470dca9015621" + resolved "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#0bc2a2e82be2c5eadbc22e511a750a2a91dc0fd5" dependencies: axios "^0.15.3" js-cookie "^2.1.3" From d4310d4a0fac102464c48fd751cd2fc2e22f2edb Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Thu, 7 Sep 2017 16:19:48 +0300 Subject: [PATCH 09/18] Include profileImage and denormalize it --- src/ducks/user.duck.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/ducks/user.duck.js b/src/ducks/user.duck.js index 8fb3f62a..dc7eb3ca 100644 --- a/src/ducks/user.duck.js +++ b/src/ducks/user.duck.js @@ -1,3 +1,4 @@ +import { updatedEntities, denormalisedEntities } from '../util/data'; import { TX_STATE_PREAUTHORIZED } from '../util/propTypes'; // ================ Action types ================ // @@ -217,9 +218,15 @@ export const fetchCurrentUser = () => } return sdk.currentUser - .show() + .show({ include: ['profileImage'] }) .then(response => { - dispatch(currentUserShowSuccess(response.data.data)); + // Temporary denormalization for profileImage + // Profile image will be included to users + const currentUserId = response.data.data.id; + const entities = updatedEntities({}, response.data); + const denormalised = denormalisedEntities(entities, 'current-user', [currentUserId]); + const currentUser = denormalised[0]; + dispatch(currentUserShowSuccess(currentUser)); }) .then(() => { dispatch(fetchCurrentUserHasListings()); From 02da6a5de9b1d8d3c6ca897e12f46c9c33e5b9ae Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Thu, 7 Sep 2017 17:18:00 +0300 Subject: [PATCH 10/18] Extract ImageFromFile component away from AddImages --- src/components/AddImages/AddImages.js | 107 +++++------------- .../ImageFromFile/ImageFromFile.css | 31 +++++ src/components/ImageFromFile/ImageFromFile.js | 70 ++++++++++++ src/components/index.js | 2 + src/translations/en.json | 3 +- 5 files changed, 131 insertions(+), 82 deletions(-) create mode 100644 src/components/ImageFromFile/ImageFromFile.css create mode 100644 src/components/ImageFromFile/ImageFromFile.js diff --git a/src/components/AddImages/AddImages.js b/src/components/AddImages/AddImages.js index adf420b3..bf2ee7f9 100644 --- a/src/components/AddImages/AddImages.js +++ b/src/components/AddImages/AddImages.js @@ -7,12 +7,10 @@ * * */ -import React, { Component, PropTypes } from 'react'; -import { FormattedMessage } from 'react-intl'; +import React, { PropTypes } from 'react'; import { SortableContainer } from 'react-sortable-hoc'; import classNames from 'classnames'; -import { Promised, ResponsiveImage } from '../../components'; -import { uuid } from '../../util/propTypes'; +import { ImageFromFile, ResponsiveImage, SpinnerIcon } from '../../components'; import css from './AddImages.css'; const RemoveImageButton = props => { @@ -41,89 +39,38 @@ const RemoveImageButton = props => { ); }; -const { any, array, func, node, string, object } = PropTypes; +const { array, func, node, string, object } = PropTypes; RemoveImageButton.propTypes = { onClick: func.isRequired }; -// readImage returns a promise which is resolved -// when FileReader has loaded given file as dataURL -const readImage = file => - new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = e => resolve(e.target.result); - reader.onerror = e => { - // eslint-disable-next-line - console.error('Error (', e, `) happened while reading ${file.name}: ${e.target.result}`); - reject(new Error(`Error reading ${file.name}: ${e.target.result}`)); - }; - reader.readAsDataURL(file); - }); - -// Create sortable elments out of given thumbnail file -class Thumbnail extends Component { - constructor(props) { - super(props); - this.state = { - promisedImage: readImage(this.props.file), - }; - } - - render() { - const { className, file, id, imageId, onRemoveImage } = this.props; - - const handleRemoveClick = e => { - e.preventDefault(); - onRemoveImage(id); - }; - - // While image is uploading we show overlay on top of thumbnail - const uploadingOverlay = !imageId - ?
    - : null; - const removeButton = imageId ? : null; - const classes = classNames(css.thumbnail, className); - return ( - { - return ( -
    -
    - {file.name} -
    - {removeButton} - {uploadingOverlay} -
    - ); - }} - renderRejected={() => ( -
    - )} - /> - ); - } -} - -Thumbnail.defaultProps = { className: null, imageId: null }; - -Thumbnail.propTypes = { - className: string, - file: any.isRequired, - id: string.isRequired, - imageId: uuid, - onRemoveImage: func.isRequired, -}; - const ThumbnailWrapper = props => { const { className, image, savedImageAltText, onRemoveImage } = props; + const handleRemoveClick = e => { + e.preventDefault(); + onRemoveImage(image.id); + }; + if (image.file) { - return ; + // Remove this image if file has been uploaded + const removeButton = image.imageId ? : null; + + // While image is uploading we show overlay on top of thumbnail + const uploadingOverlay = !image.imageId + ?
    + : null; + + return ( + + {removeButton} + {uploadingOverlay} + + ); } else { - const handleRemoveClick = e => { - e.preventDefault(); - onRemoveImage(image.id); - }; const classes = classNames(css.thumbnail, className); return (
    diff --git a/src/components/ImageFromFile/ImageFromFile.css b/src/components/ImageFromFile/ImageFromFile.css new file mode 100644 index 00000000..d59d29ab --- /dev/null +++ b/src/components/ImageFromFile/ImageFromFile.css @@ -0,0 +1,31 @@ +@import '../../marketplace.css'; + +.root { + display: block; + position: relative; + width: 100%; + margin: 0; + overflow: hidden; + background-color: var(--matterColorNegative); /* Loading BG color */ +} + +.threeToTwoWrapper { + position: relative; +} + +.aspectWrapper { + padding-bottom: calc(100% * (2 / 3)); +} + +.rootForImage { + /* Layout - image will take space defined by aspect ratio wrapper */ + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + width: 100%; + height: 100%; + object-fit: cover; + border-radius: var(--borderRadius); +} diff --git a/src/components/ImageFromFile/ImageFromFile.js b/src/components/ImageFromFile/ImageFromFile.js new file mode 100644 index 00000000..bbbfd0b1 --- /dev/null +++ b/src/components/ImageFromFile/ImageFromFile.js @@ -0,0 +1,70 @@ +import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; +import { Promised } from '../../components'; + +import css from './ImageFromFile.css'; + +// readImage returns a promise which is resolved +// when FileReader has loaded given file as dataURL +const readImage = file => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = e => resolve(e.target.result); + reader.onerror = e => { + // eslint-disable-next-line + console.error('Error (', e, `) happened while reading ${file.name}: ${e.target.result}`); + reject(new Error(`Error reading ${file.name}: ${e.target.result}`)); + }; + reader.readAsDataURL(file); + }); + +// Create elements out of given thumbnail file +class ImageFromFile extends Component { + constructor(props) { + super(props); + this.state = { + promisedImage: readImage(this.props.file), + }; + } + + render() { + const { className, rootClassName, file, id, children } = this.props; + const classes = classNames(rootClassName || css.root, className); + return ( + { + return ( +
    +
    +
    + {file.name} +
    +
    + {children} +
    + ); + }} + renderRejected={() => ( +
    + )} + /> + ); + } +} + +ImageFromFile.defaultProps = { className: null, children: null, rootClassName: null }; + +const { any, node, string } = PropTypes; + +ImageFromFile.propTypes = { + className: string, + rootClassName: string, + file: any.isRequired, + id: string.isRequired, + children: node, +}; + +export default ImageFromFile; diff --git a/src/components/index.js b/src/components/index.js index b547b49a..7c381a91 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -18,6 +18,7 @@ import ExpandingTextarea from './ExpandingTextarea/ExpandingTextarea'; import FilterPanel from './FilterPanel/FilterPanel'; import HeroSection from './HeroSection/HeroSection'; import ImageCarousel from './ImageCarousel/ImageCarousel'; +import ImageFromFile from './ImageFromFile/ImageFromFile'; import ListingCard from './ListingCard/ListingCard'; import LocationAutocompleteInput, { LocationAutocompleteInputField, @@ -85,6 +86,7 @@ export { FilterPanel, HeroSection, ImageCarousel, + ImageFromFile, InlineTextButton, ListingCard, LocationAutocompleteInput, diff --git a/src/translations/en.json b/src/translations/en.json index d62461bc..1d2430bb 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1,6 +1,4 @@ { - "AddImages.couldNotReadFile": "Could not read file", - "AddImages.upload": "Uploading", "AuthenticationPage.emailAlreadyInUse": "An account already exists with this email address. Try logging in instead.", "AuthenticationPage.loginFailed": "The email and password you entered did not match our records. Please double-check and try again.", "AuthenticationPage.loginLinkText": "Login", @@ -125,6 +123,7 @@ "HeroSection.subTitle": "The largest online community to rent saunas in Finland.", "HeroSection.title": "Book saunas everywhere.", "ImageCarousel.imageAltText": "Image {index}/{count}", + "ImageFromFile.couldNotReadFile": "Could not read file", "InboxPage.fetchFailed": "Could not load all messages. Please try again.", "InboxPage.noOrdersFound": "You haven't made any bookings.", "InboxPage.noSalesFound": "Nobody has booked anything from you yet.", From 6b8cb45b1599b23db4726e7d32f6bb20f3f192cd Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Fri, 8 Sep 2017 01:18:51 +0300 Subject: [PATCH 11/18] Correct aspect ratio can be passed to ImageFromFile --- src/components/ImageFromFile/ImageFromFile.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/ImageFromFile/ImageFromFile.js b/src/components/ImageFromFile/ImageFromFile.js index bbbfd0b1..101cdc7f 100644 --- a/src/components/ImageFromFile/ImageFromFile.js +++ b/src/components/ImageFromFile/ImageFromFile.js @@ -29,8 +29,9 @@ class ImageFromFile extends Component { } render() { - const { className, rootClassName, file, id, children } = this.props; + const { className, rootClassName, aspectRatioClassName, file, id, children } = this.props; const classes = classNames(rootClassName || css.root, className); + const aspectRatioClasses = aspectRatioClassName || css.aspectWrapper; return (
    -
    +
    {file.name}
    @@ -55,13 +56,19 @@ class ImageFromFile extends Component { } } -ImageFromFile.defaultProps = { className: null, children: null, rootClassName: null }; +ImageFromFile.defaultProps = { + className: null, + children: null, + rootClassName: null, + aspectRatioClassName: null, +}; const { any, node, string } = PropTypes; ImageFromFile.propTypes = { className: string, rootClassName: string, + aspectRatioClassName: string, file: any.isRequired, id: string.isRequired, children: node, From a069d7f0db0561775bce29a16cee7d21062c9673 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Fri, 8 Sep 2017 01:21:29 +0300 Subject: [PATCH 12/18] UserNav was too short for baseline --- src/components/UserNav/UserNav.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/UserNav/UserNav.css b/src/components/UserNav/UserNav.css index 9c70df53..5022161a 100644 --- a/src/components/UserNav/UserNav.css +++ b/src/components/UserNav/UserNav.css @@ -6,7 +6,7 @@ @media (--viewportMedium) { display: flex; justify-content: flex-end; - height: 55px; + height: 57px; align-items: flex-end; padding: 13px 24px 0 24px; } From cec8ce3cb676b7152003326d9e24cb1ee6cde546 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Fri, 8 Sep 2017 01:22:41 +0300 Subject: [PATCH 13/18] ProfileSettingsForm --- .../ProfileSettingsForm.css | 259 ++++++++++++++++++ .../ProfileSettingsForm.js | 225 +++++++++++++++ src/containers/index.js | 2 + src/translations/en.json | 16 ++ 4 files changed, 502 insertions(+) create mode 100644 src/containers/ProfileSettingsForm/ProfileSettingsForm.css create mode 100644 src/containers/ProfileSettingsForm/ProfileSettingsForm.js diff --git a/src/containers/ProfileSettingsForm/ProfileSettingsForm.css b/src/containers/ProfileSettingsForm/ProfileSettingsForm.css new file mode 100644 index 00000000..3649566d --- /dev/null +++ b/src/containers/ProfileSettingsForm/ProfileSettingsForm.css @@ -0,0 +1,259 @@ +@import '../../marketplace.css'; + +:root { + --avatarSize: 96px; + --avatarSizeDesktop: 240px; +} + +.root { + margin-top: 23px; + + @media (--viewportMedium) { + margin-top: 35px; + } +} + +.sectionContainer { + padding: 0; + margin-bottom: 34px; + + @media (--viewportMedium) { + padding: 0; + margin-bottom: 54px; + } +} + +.sectionTitle { + /* Font */ + color: var(--matterColorAnti); + + margin-top: 0; + margin-bottom: 13px; + @media (--viewportMedium) { + margin-top: 0; + margin-bottom: 20px; + } +} + +.lastSection { + margin-bottom: 66px; + @media (--viewportMedium) { + margin-bottom: 111px; + } +} + +.uploadAvatarInput { + display: none; +} + +.uploadAvatarWrapper { + margin-top: 19px; + margin-bottom: 18px; + + @media (--viewportMedium) { + margin-top: 44px; + margin-bottom: 20px; + } +} + +.label { + width: var(--avatarSize); + @media (--viewportMedium) { + width: var(--avatarSizeDesktop); + } +} + +.avatarPlaceholder, +.avatarContainer { + /* Dimension */ + position: relative; + width: var(--avatarSize); + height: var(--avatarSize); + + /* Center content */ + display: flex; + align-items: center; + justify-content: center; + + /* Initial coloring */ + background-color: var(--matterColorBright); + border-radius: calc(var(--avatarSize) / 2); + cursor: pointer; + + @media (--viewportMedium) { + width: var(--avatarSizeDesktop); + height: var(--avatarSizeDesktop); + border-radius: calc(var(--avatarSizeDesktop) / 2); + } + +} + +.avatarPlaceholder { + /* Placeholder border */ + border-style: dashed; + border-color: var(--matterColorNegative); + border-width: 2px; + + &:hover { + border-color: var(--matterColorAnti); + } +} + +.avatarPlaceholderTextMobile { + @media (--viewportMedium) { + display: none; + } +} +.avatarPlaceholderText { + display: none; + + @media (--viewportMedium) { + max-width: 130px; + text-align: center; + } +} + +.avatarUploadError { + /* Placeholder border */ + border-style: dashed; + border-color: var(--failColor); + border-width: 2px; +} + +.error { + /* Font */ + @apply --marketplaceH4FontStyles; + color: var(--failColor); + margin-top: 18px; + margin-bottom: 0; + + @media (--viewportMedium) { + margin-top: 22px; + margin-bottom: 2px; + } +} + +.changeAvatar { + /* Font */ + @apply --marketplaceH5FontStyles; + + /* Positioning: right */ + position: absolute; + bottom: 27px; + right: -129px; + /* Dimensions */ + width: 105px; + height: 41px; + padding: 9px 10px 9px 35px; + + /* Look and feel (buttonish) */ + background-color: var(--matterColorLight); + background-image: url('data:image/svg+xml;utf8,'); + background-repeat: no-repeat; + background-position: 15px center; + border: solid 1px var(--matterColorNegative); + + margin-top: 0; + margin-bottom: 0; + + @media (--viewportMedium) { + /* Position: under */ + bottom: -8px; + right: auto; + margin-top: 0; + margin-bottom: 0; + } +} + +.uploadingImage { + /* Dimensions */ + width: var(--avatarSize); + height: var(--avatarSize); + + /* Image fitted to container */ + object-fit: cover; + background-color: var(--matterColorNegative); /* Loading BG color */ + border-radius: calc(var(--avatarSize) / 2); + overflow: hidden; + + display: block; + position: relative; + margin: 0; + + @media (--viewportMedium) { + width: var(--avatarSizeDesktop); + height: var(--avatarSizeDesktop); + border-radius: calc(var(--avatarSizeDesktop) / 2); + } +} + +.uploadingImageOverlay { + /* Cover everything (overlay) */ + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + + /* Overlay style */ + background-color: var(--matterColorLight); + opacity: 0.8; + + /* Center content */ + display: flex; + justify-content: center; + align-items: center; +} + +/* Avatar has square aspect ratio */ +/* Default is 3:2 */ +.squareAspectRatio { + padding-bottom: 100%; +} + +.tip { + @apply --marketplaceDefaultFontStyles; + color: var(--matterColorAnti); + margin-top: 0; + margin-bottom: 13px; + + @media (--viewportMedium) { + margin-top: 0; + margin-bottom: 10px; + } + +} + +.fileInfo { + @apply --marketplaceH4FontStyles; + color: var(--matterColorAnti); + margin-top: 0; + margin-bottom: 0; + + @media (--viewportMedium) { + margin-top: 0; + margin-bottom: 0; + } +} + +.nameContainer { + display: flex; + justify-content: space-between; + margin-top: 14px; + + @media (--viewportMedium) { + margin-top: 26px; + } +} + +.firstName { + width: calc(34% - 9px); +} + +.lastName { + width: calc(66% - 9px); +} + +.submitButton { + margin-top: 24px; +} diff --git a/src/containers/ProfileSettingsForm/ProfileSettingsForm.js b/src/containers/ProfileSettingsForm/ProfileSettingsForm.js new file mode 100644 index 00000000..a1fa4f37 --- /dev/null +++ b/src/containers/ProfileSettingsForm/ProfileSettingsForm.js @@ -0,0 +1,225 @@ +import React, { PropTypes } from 'react'; +import { compose } from 'redux'; +import { FormattedMessage, injectIntl, intlShape } from 'react-intl'; +import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form'; +import classNames from 'classnames'; +import { ensureCurrentUser } from '../../util/data'; +import * as validators from '../../util/validators'; +import { Avatar, Button, ImageFromFile, SpinnerIcon, TextInputField } from '../../components'; + +import css from './ProfileSettingsForm.css'; + +const ACCEPT_IMAGES = 'image/*'; + +const RenderAvatar = props => { + const { accept, id, input, label, type, disabled, uploadImageError } = props; + const { name, onChange } = input; + const error = uploadImageError + ?
    + +
    + : null; + + return ( +
    + + { + const file = event.target.files[0]; + const tempId = `${file.name}_${Date.now()}`; + onChange({ id: tempId, file }); + }} + type={type} + /> + {error} +
    + ); +}; + +RenderAvatar.defaultProps = { uploadImageError: null }; +const { bool, func, instanceOf, node, object, shape, string } = PropTypes; + +RenderAvatar.propTypes = { + accept: string.isRequired, + disabled: bool.isRequired, + id: string.isRequired, + input: shape({ + value: object, + onChange: func.isRequired, + name: string.isRequired, + }).isRequired, + label: node.isRequired, + type: string.isRequired, + uploadImageError: instanceOf(Error), +}; + +const ProfileSettingsFormComponent = props => { + const { + className, + currentUser, + form, + handleSubmit, + intl, + invalid, + onImageUpload, + profileImage, + rootClassName, + submitting, + updateInProgress, + updateProfileError, + uploadImageError, + uploadInProgress, + } = props; + + const user = ensureCurrentUser(currentUser); + + // First name + const firstNameLabel = intl.formatMessage({ + id: 'ProfileSettingsForm.firstNameLabel', + }); + const firstNamePlaceholder = intl.formatMessage({ + id: 'ProfileSettingsForm.firstNamePlaceholder', + }); + const firstNameRequiredMessage = intl.formatMessage({ + id: 'ProfileSettingsForm.firstNameRequired', + }); + const firstNameRequired = validators.required(firstNameRequiredMessage); + + // Last name + const lastNameLabel = intl.formatMessage({ + id: 'ProfileSettingsForm.lastNameLabel', + }); + const lastNamePlaceholder = intl.formatMessage({ + id: 'ProfileSettingsForm.lastNamePlaceholder', + }); + const lastNameRequiredMessage = intl.formatMessage({ + id: 'ProfileSettingsForm.lastNameRequired', + }); + const lastNameRequired = validators.required(lastNameRequiredMessage); + + const uploadingOverlay = uploadInProgress + ?
    + : null; + + const hasUploadError = !!uploadImageError && !uploadInProgress; + const errorClasses = classNames({ [css.avatarUploadError]: hasUploadError }); + const avatarImage = uploadInProgress && profileImage.file + ? + {uploadingOverlay} + + : ; + + const chooseAvatarLabel = profileImage.imageId || (uploadInProgress && profileImage.file) + ?
    + {avatarImage} +
    + +
    +
    + :
    +
    + +
    +
    + +
    +
    ; + + const submitError = updateProfileError + ?
    + +
    + : null; + + const classes = classNames(rootClassName || css.root, className); + const submitDisabled = invalid || submitting || uploadInProgress || updateInProgress; + + return ( +
    +
    +

    + +

    + +
    +
    +
    +
    +

    + +

    +
    + + +
    +
    + {submitError} + +
    + ); +}; + +ProfileSettingsFormComponent.defaultProps = { + rootClassName: null, + className: null, + uploadImageError: null, + updateProfileError: null, +}; + +ProfileSettingsFormComponent.propTypes = { + ...formPropTypes, + rootClassName: string, + className: string, + uploadImageError: instanceOf(Error), + uploadInProgress: bool.isRequired, + updateInProgress: bool.isRequired, + updateProfileError: instanceOf(Error), + intl: intlShape.isRequired, +}; + +const defaultFormName = 'ProfileSettingsForm'; + +const ProfileSettingsForm = compose(reduxForm({ form: defaultFormName }), injectIntl)( + ProfileSettingsFormComponent +); + +export default ProfileSettingsForm; diff --git a/src/containers/index.js b/src/containers/index.js index 57504d88..7bef4382 100644 --- a/src/containers/index.js +++ b/src/containers/index.js @@ -25,6 +25,7 @@ import PasswordForgottenPage from './PasswordForgottenPage/PasswordForgottenPage import PayoutDetailsForm from './PayoutDetailsForm/PayoutDetailsForm'; import PayoutPreferencesPage from './PayoutPreferencesPage/PayoutPreferencesPage'; import ProfilePage from './ProfilePage/ProfilePage'; +import ProfileSettingsForm from './ProfileSettingsForm/ProfileSettingsForm'; import ProfileSettingsPage from './ProfileSettingsPage/ProfileSettingsPage'; import SalePage from './SalePage/SalePage'; import SearchPage from './SearchPage/SearchPage'; @@ -62,6 +63,7 @@ export { PayoutDetailsForm, PayoutPreferencesPage, ProfilePage, + ProfileSettingsForm, ProfileSettingsPage, SalePage, SearchPage, diff --git a/src/translations/en.json b/src/translations/en.json index 1d2430bb..c0ff7c4f 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -251,6 +251,22 @@ "PayoutDetailsForm.streetAddressRequired": "This field is required", "PayoutDetailsForm.submitButtonText": "Save details & publish listing", "PayoutDetailsForm.title": "One more thing: payout preferences", + "ProfileSettingsForm.addYourProfilePicture": "+ Add your profile picture…", + "ProfileSettingsForm.addYourProfilePictureMobile": "+ Add", + "ProfileSettingsForm.changeAvatar": "Change", + "ProfileSettingsForm.fileInfo": ".JPG, .GIF or .PNG max. 10 MB", + "ProfileSettingsForm.firstNameLabel": "First name", + "ProfileSettingsForm.firstNamePlaceholder": "John", + "ProfileSettingsForm.firstNameRequired": "This field is required", + "ProfileSettingsForm.imageUploadFailed": "Whoopsie, something went wrong - please try again.", + "ProfileSettingsForm.lastNameLabel": "Last name", + "ProfileSettingsForm.lastNamePlaceholder": "Doe", + "ProfileSettingsForm.lastNameRequired": "This field is required", + "ProfileSettingsForm.saveChanges": "Save changes", + "ProfileSettingsForm.tip": "Tip: Choose an image where your face is recognizable.", + "ProfileSettingsForm.updateProfileFailed": "Whoopsie, something went wrong - please try again.", + "ProfileSettingsForm.yourName": "Your name", + "ProfileSettingsForm.yourProfilePicture": "Your profile picture", "ResponsiveImage.noImage": "No image", "SaleDetailsPanel.bookingBreakdownTitle": "Booking breakdown", "SaleDetailsPanel.listingAcceptedTitle": "Woohoo! You accepted a request from {customerName} for {listingLink}", From 76cb0ac551ce177ffb4a62731d57c8081ab1475c Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Fri, 8 Sep 2017 01:23:51 +0300 Subject: [PATCH 14/18] ProfileSettingsPage.duck: update profile call --- .../ProfileSettingsPage.duck.js | 112 ++++++++++++++---- 1 file changed, 88 insertions(+), 24 deletions(-) diff --git a/src/containers/ProfileSettingsPage/ProfileSettingsPage.duck.js b/src/containers/ProfileSettingsPage/ProfileSettingsPage.duck.js index ce49561d..361e82e3 100644 --- a/src/containers/ProfileSettingsPage/ProfileSettingsPage.duck.js +++ b/src/containers/ProfileSettingsPage/ProfileSettingsPage.duck.js @@ -1,53 +1,80 @@ +import { updatedEntities, denormalisedEntities } from '../../util/data'; +import { currentUserShowSuccess } from '../../ducks/user.duck'; + const requestAction = actionType => params => ({ type: actionType, payload: { params } }); const successAction = actionType => result => ({ type: actionType, payload: result.data }); const errorAction = actionType => error => ({ type: actionType, payload: error, error: true }); // ================ Action types ================ // -export const UPLOAD_IMAGE_REQUEST = 'app/EditListingPage/UPLOAD_IMAGE_REQUEST'; -export const UPLOAD_IMAGE_SUCCESS = 'app/EditListingPage/UPLOAD_IMAGE_SUCCESS'; -export const UPLOAD_IMAGE_ERROR = 'app/EditListingPage/UPLOAD_IMAGE_ERROR'; +export const MARK_FORM_UPDATED = 'app/EditListingPage/MARK_FORM_UPDATED'; +export const CLEAR_UPDATED_FORM = 'app/EditListingPage/CLEAR_UPDATED_FORM'; -export const REMOVE_LISTING_IMAGE = 'app/EditListingPage/REMOVE_LISTING_IMAGE'; +export const UPLOAD_IMAGE_REQUEST = 'app/ProfileSettingsPage/UPLOAD_IMAGE_REQUEST'; +export const UPLOAD_IMAGE_SUCCESS = 'app/ProfileSettingsPage/UPLOAD_IMAGE_SUCCESS'; +export const UPLOAD_IMAGE_ERROR = 'app/ProfileSettingsPage/UPLOAD_IMAGE_ERROR'; + +export const UPDATE_PROFILE_REQUEST = 'app/ProfileSettingsPage/UPDATE_PROFILE_REQUEST'; +export const UPDATE_PROFILE_SUCCESS = 'app/ProfileSettingsPage/UPDATE_PROFILE_SUCCESS'; +export const UPDATE_PROFILE_ERROR = 'app/ProfileSettingsPage/UPDATE_PROFILE_ERROR'; // ================ Reducer ================ // const initialState = { - // Error instance placeholders for each endpoint + formUpdated: false, + image: null, uploadImageError: null, - image: {}, + uploadInProgress: false, updateInProgress: false, + updateProfileError: null, }; export default function reducer(state = initialState, action = {}) { const { type, payload } = action; switch (type) { - - case UPLOAD_IMAGE_REQUEST: { + case UPLOAD_IMAGE_REQUEST: // payload.params: { id: 'tempId', file } - const image = { - [payload.params.id]: { ...payload.params }, - }; - return { ...state, - image, - updateInProgress: true, + image: { ...payload.params }, + uploadInProgress: true, uploadImageError: null, }; - } case UPLOAD_IMAGE_SUCCESS: { - // payload.params: { id: 'tempId', imageId: 'some-real-id'} + // payload: { id: 'tempId', imageId: 'some-real-id'} const { id, imageId } = payload; - const file = state.image[id].file; - const image = { [id]: { id, imageId, file } }; - return { ...state, image, updateInProgress: false }; + const { file } = state.image || {}; + const image = { id, imageId, file }; + return { ...state, image, uploadInProgress: false }; } case UPLOAD_IMAGE_ERROR: { // eslint-disable-next-line no-console - return { ...state, image: null, updateInProgress: false , uploadImageError: payload.error }; + return { ...state, image: null, uploadInProgress: false, uploadImageError: payload.error }; } + case UPDATE_PROFILE_REQUEST: + return { + ...state, + updateInProgress: true, + updateProfileError: null, + }; + case UPDATE_PROFILE_SUCCESS: + return { + ...state, + updateInProgress: false, + }; + case UPDATE_PROFILE_ERROR: + return { + ...state, + updateInProgress: false, + updateProfileError: payload, + }; + + case MARK_FORM_UPDATED: + return { ...state, formUpdated: payload }; + case CLEAR_UPDATED_FORM: + return { ...state, formUpdated: null, updateProfileError: null, uploadImageError: null }; + default: return state; } @@ -57,18 +84,32 @@ export default function reducer(state = initialState, action = {}) { // ================ Action creators ================ // -// SDK method: listings.uploadImage -export const uploadImage = requestAction(UPLOAD_IMAGE_REQUEST); +export const markFormUpdated = tab => ({ + type: MARK_FORM_UPDATED, + payload: tab, +}); + +export const clearUpdatedForm = () => ({ + type: CLEAR_UPDATED_FORM, +}); + +// SDK method: images.uploadListingImage +export const uploadImageRequest = requestAction(UPLOAD_IMAGE_REQUEST); export const uploadImageSuccess = successAction(UPLOAD_IMAGE_SUCCESS); export const uploadImageError = errorAction(UPLOAD_IMAGE_ERROR); +// SDK method: sdk.currentUser.updateProfile +export const updateProfileRequest = requestAction(UPDATE_PROFILE_REQUEST); +export const updateProfileSuccess = successAction(UPDATE_PROFILE_SUCCESS); +export const updateProfileError = errorAction(UPDATE_PROFILE_ERROR); + // ================ Thunk ================ // // Images return imageId which we need to map with previously generated temporary id -export function requestImageUpload(actionPayload) { +export function uploadImage(actionPayload) { return (dispatch, getState, sdk) => { const id = actionPayload.id; - dispatch(uploadImage(actionPayload)); + dispatch(uploadImageRequest(actionPayload)); return sdk.images .uploadProfileImage({ image: actionPayload.file }) @@ -76,3 +117,26 @@ export function requestImageUpload(actionPayload) { .catch(e => dispatch(uploadImageError({ id, error: e }))); }; } + +export const updateProfile = actionPayload => { + return (dispatch, getState, sdk) => { + dispatch(updateProfileRequest()); + + return sdk.currentUser + .updateProfile(actionPayload, { expand: true, include: ['profileImage'] }) + .then(response => { + dispatch(updateProfileSuccess(response)); + + // Temporary denormalization for profileImage + // Profile image will be included to users + const currentUserId = response.data.data.id; + const entities = updatedEntities({}, response.data); + const denormalised = denormalisedEntities(entities, 'current-user', [currentUserId]); + const currentUser = denormalised[0]; + + // Update current user in state.user.currentUser through user.duck.js + dispatch(currentUserShowSuccess(currentUser)); + }) + .catch(e => dispatch(updateProfileError(e))); + }; +}; From d9e7dbe2c9284ffbaf5090996739f3675984b753 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Fri, 8 Sep 2017 01:24:30 +0300 Subject: [PATCH 15/18] ProfileSettingsPage with form --- .../ProfileSettingsPage.css | 14 +++ .../ProfileSettingsPage.js | 106 ++++++++++++++---- .../ProfileSettingsPage.test.js | 13 ++- .../ProfileSettingsPage.test.js.snap | 13 +++ src/translations/en.json | 1 + src/util/data.js | 2 +- 6 files changed, 121 insertions(+), 28 deletions(-) create mode 100644 src/containers/ProfileSettingsPage/ProfileSettingsPage.css diff --git a/src/containers/ProfileSettingsPage/ProfileSettingsPage.css b/src/containers/ProfileSettingsPage/ProfileSettingsPage.css new file mode 100644 index 00000000..cab8c99e --- /dev/null +++ b/src/containers/ProfileSettingsPage/ProfileSettingsPage.css @@ -0,0 +1,14 @@ +@import '../../marketplace.css'; + +.root { + background-color: var(--matterColorLight); +} +.content { + width: calc(100% - 48px); + margin: 12px 24px; + + @media (--viewportMedium) { + max-width: 565px; + margin: 89px auto; + } +} diff --git a/src/containers/ProfileSettingsPage/ProfileSettingsPage.js b/src/containers/ProfileSettingsPage/ProfileSettingsPage.js index 1d5b62c1..a08499ea 100644 --- a/src/containers/ProfileSettingsPage/ProfileSettingsPage.js +++ b/src/containers/ProfileSettingsPage/ProfileSettingsPage.js @@ -2,26 +2,33 @@ import React, { PropTypes } from 'react'; import { compose } from 'redux'; import { connect } from 'react-redux'; import { withRouter } from 'react-router-dom'; +import { FormattedMessage } from 'react-intl'; import * as propTypes from '../../util/propTypes'; +import { ensureCurrentUser } from '../../util/data'; import { logout, authenticationInProgress } from '../../ducks/Auth.duck'; import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck'; -import { requestImageUpload } from './ProfileSettingsPage.duck'; +import { clearUpdatedForm, updateProfile, uploadImage } from './ProfileSettingsPage.duck'; import { PageLayout, Topbar, UserNav } from '../../components'; +import { ProfileSettingsForm } from '../../containers'; -const ACCEPT_IMAGES = 'image/*'; +import css from './ProfileSettingsPage.css'; -const onImageUploadHandler = (event, fn) => { - const file = event.target.files[0]; +const onImageUploadHandler = (values, fn) => { + const { id, imageId, file } = values; if (file) { - fn({ id: `${file.name}_${Date.now()}`, file }) - .then(response => { - console.log('Response:', response); - }) - .catch(error => { - console.log('Error:', error); - }); + fn({ id, imageId, file }); } -} +}; + +const onSubmit = (values, fn) => { + const { firstName, lastName, profileImage } = values; + const name = { firstName, lastName }; + const updatedValues = profileImage.imageId + ? { ...name, profileImageId: profileImage.imageId } + : name; + + fn(updatedValues); +}; export const ProfileSettingsPageComponent = props => { const { @@ -30,18 +37,48 @@ export const ProfileSettingsPageComponent = props => { currentUser, currentUserHasListings, history, + image, isAuthenticated, location, logoutError, notificationCount, + onChange, onImageUpload, onLogout, onManageDisableScrolling, + onUpdateProfile, scrollingDisabled, + updateInProgress, + updateProfileError, + uploadImageError, + uploadInProgress, } = props; + const user = ensureCurrentUser(currentUser); + const { firstName, lastName } = user.attributes.profile; + const profileImage = image || { imageId: user.profileImage.id }; + + const profileSettingsForm = user.id + ? onImageUploadHandler(e, onImageUpload)} + uploadInProgress={uploadInProgress} + updateInProgress={updateInProgress} + uploadImageError={uploadImageError} + updateProfileError={updateProfileError} + onSubmit={values => { + onSubmit({ ...values, profileImage }, onUpdateProfile); + }} + onChange={onChange} + /> + : null; + return ( { onManageDisableScrolling={onManageDisableScrolling} /> - - onImageUploadHandler(e, onImageUpload)} - type="file" - disabled={false} - /> +
    +

    + {profileSettingsForm} +
    ); }; @@ -79,9 +111,12 @@ ProfileSettingsPageComponent.defaultProps = { currentUser: null, logoutError: null, notificationCount: 0, + uploadImageError: null, + updateProfileError: null, + image: null, }; -const { bool, func, instanceOf, number, object, shape } = PropTypes; +const { bool, func, instanceOf, number, object, shape, string } = PropTypes; ProfileSettingsPageComponent.propTypes = { authInfoError: instanceOf(Error), @@ -89,12 +124,23 @@ ProfileSettingsPageComponent.propTypes = { currentUser: propTypes.currentUser, currentUserHasListings: bool.isRequired, isAuthenticated: bool.isRequired, + image: shape({ + id: string, + imageId: propTypes.uuid, + file: instanceOf(File), + }), logoutError: instanceOf(Error), notificationCount: number, + onChange: func.isRequired, onImageUpload: func.isRequired, onLogout: func.isRequired, onManageDisableScrolling: func.isRequired, + onUpdateProfile: func.isRequired, scrollingDisabled: bool.isRequired, + updateInProgress: bool.isRequired, + updateProfileError: instanceOf(Error), + uploadImageError: instanceOf(Error), + uploadInProgress: bool.isRequired, // from withRouter history: shape({ @@ -112,23 +158,37 @@ const mapStateToProps = state => { currentUserHasListings, currentUserNotificationCount: notificationCount, } = state.user; + const { + image, + uploadImageError, + uploadInProgress, + updateInProgress, + updateProfileError, + } = state.ProfileSettingsPage; return { authInfoError, authInProgress: authenticationInProgress(state), currentUser, currentUserHasListings, currentUserNotificationCount: notificationCount, + image, isAuthenticated, logoutError, scrollingDisabled: isScrollingDisabled(state), + updateInProgress, + updateProfileError, + uploadImageError, + uploadInProgress, }; }; const mapDispatchToProps = dispatch => ({ - onImageUpload: data => dispatch(requestImageUpload(data)), + onChange: () => dispatch(clearUpdatedForm()), + onImageUpload: data => dispatch(uploadImage(data)), onLogout: historyPush => dispatch(logout(historyPush)), onManageDisableScrolling: (componentId, disableScrolling) => dispatch(manageDisableScrolling(componentId, disableScrolling)), + onUpdateProfile: data => dispatch(updateProfile(data)), }); const ProfileSettingsPage = compose(connect(mapStateToProps, mapDispatchToProps), withRouter)( diff --git a/src/containers/ProfileSettingsPage/ProfileSettingsPage.test.js b/src/containers/ProfileSettingsPage/ProfileSettingsPage.test.js index d9ef3ce0..aa627db6 100644 --- a/src/containers/ProfileSettingsPage/ProfileSettingsPage.test.js +++ b/src/containers/ProfileSettingsPage/ProfileSettingsPage.test.js @@ -8,15 +8,20 @@ describe('ContactDetailsPage', () => { it('matches snapshot', () => { const tree = renderShallow( ); expect(tree).toMatchSnapshot(); diff --git a/src/containers/ProfileSettingsPage/__snapshots__/ProfileSettingsPage.test.js.snap b/src/containers/ProfileSettingsPage/__snapshots__/ProfileSettingsPage.test.js.snap index 83b1148a..4d96355a 100644 --- a/src/containers/ProfileSettingsPage/__snapshots__/ProfileSettingsPage.test.js.snap +++ b/src/containers/ProfileSettingsPage/__snapshots__/ProfileSettingsPage.test.js.snap @@ -2,9 +2,11 @@ exports[`ContactDetailsPage matches snapshot 1`] = ` + +
    +

    + +

    +
    `; diff --git a/src/translations/en.json b/src/translations/en.json index c0ff7c4f..61ccd39c 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -267,6 +267,7 @@ "ProfileSettingsForm.updateProfileFailed": "Whoopsie, something went wrong - please try again.", "ProfileSettingsForm.yourName": "Your name", "ProfileSettingsForm.yourProfilePicture": "Your profile picture", + "ProfileSettingsPage.title": "Profile settings", "ResponsiveImage.noImage": "No image", "SaleDetailsPanel.bookingBreakdownTitle": "Booking breakdown", "SaleDetailsPanel.listingAcceptedTitle": "Woohoo! You accepted a request from {customerName} for {listingLink}", diff --git a/src/util/data.js b/src/util/data.js index 58a942f6..36b03042 100644 --- a/src/util/data.js +++ b/src/util/data.js @@ -160,6 +160,6 @@ export const ensureUser = user => { * @param {Object} current user entity object, which is to be ensured agains null values */ export const ensureCurrentUser = user => { - const empty = { id: null, type: 'current-user', attributes: { profile: {} } }; + const empty = { id: null, type: 'current-user', attributes: { profile: {} }, profileImage: {} }; return { ...empty, ...user }; }; From 47c15ab6364e41b7ec46372c0d7a3e531bb4413e Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Fri, 8 Sep 2017 01:32:10 +0300 Subject: [PATCH 16/18] Missing display block to desktop --- src/containers/ProfileSettingsForm/ProfileSettingsForm.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/containers/ProfileSettingsForm/ProfileSettingsForm.css b/src/containers/ProfileSettingsForm/ProfileSettingsForm.css index 3649566d..8ae91e80 100644 --- a/src/containers/ProfileSettingsForm/ProfileSettingsForm.css +++ b/src/containers/ProfileSettingsForm/ProfileSettingsForm.css @@ -108,6 +108,7 @@ display: none; @media (--viewportMedium) { + display: block; max-width: 130px; text-align: center; } From a47c4dea014b0f460c4dad708a1922eb49c0fa7a Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Fri, 8 Sep 2017 11:54:21 +0300 Subject: [PATCH 17/18] Refactoring and fixing form disabled state to use pristine --- src/components/AddImages/AddImages.js | 38 +++-------------- src/components/AddImages/RemoveImageButton.js | 42 +++++++++++++++++++ .../ProfileSettingsForm.js | 4 +- .../ProfileSettingsPage.duck.js | 41 +++++++++--------- 4 files changed, 72 insertions(+), 53 deletions(-) create mode 100644 src/components/AddImages/RemoveImageButton.js diff --git a/src/components/AddImages/AddImages.js b/src/components/AddImages/AddImages.js index bf2ee7f9..9b5f0ba9 100644 --- a/src/components/AddImages/AddImages.js +++ b/src/components/AddImages/AddImages.js @@ -11,47 +11,19 @@ import React, { PropTypes } from 'react'; import { SortableContainer } from 'react-sortable-hoc'; import classNames from 'classnames'; import { ImageFromFile, ResponsiveImage, SpinnerIcon } from '../../components'; + import css from './AddImages.css'; - -const RemoveImageButton = props => { - const { onClick } = props; - return ( - - ); -}; - -const { array, func, node, string, object } = PropTypes; - -RemoveImageButton.propTypes = { onClick: func.isRequired }; +import RemoveImageButton from './RemoveImageButton'; const ThumbnailWrapper = props => { const { className, image, savedImageAltText, onRemoveImage } = props; const handleRemoveClick = e => { - e.preventDefault(); + e.stopPropagation(); onRemoveImage(image.id); }; if (image.file) { - // Remove this image if file has been uploaded + // Add remove button only when the image has been uploaded and can be removed const removeButton = image.imageId ? : null; // While image is uploading we show overlay on top of thumbnail @@ -96,6 +68,8 @@ const ThumbnailWrapper = props => { ThumbnailWrapper.defaultProps = { className: null }; +const { array, func, node, string, object } = PropTypes; + ThumbnailWrapper.propTypes = { className: string, image: object.isRequired, diff --git a/src/components/AddImages/RemoveImageButton.js b/src/components/AddImages/RemoveImageButton.js new file mode 100644 index 00000000..11c70754 --- /dev/null +++ b/src/components/AddImages/RemoveImageButton.js @@ -0,0 +1,42 @@ +import React, { PropTypes } from 'react'; +import classNames from 'classnames'; +import css from './AddImages.css'; + +const RemoveImageButton = props => { + const { className, rootClassName, onClick } = props; + const classes = classNames(rootClassName || css.removeImage, className); + return ( + + ); +}; + +RemoveImageButton.defaultProps = { className: null, rootClassName: null }; + +const { func, string } = PropTypes; + +RemoveImageButton.propTypes = { + className: string, + rootClassName: string, + onClick: func.isRequired, +}; + +export default RemoveImageButton; diff --git a/src/containers/ProfileSettingsForm/ProfileSettingsForm.js b/src/containers/ProfileSettingsForm/ProfileSettingsForm.js index a1fa4f37..c733870d 100644 --- a/src/containers/ProfileSettingsForm/ProfileSettingsForm.js +++ b/src/containers/ProfileSettingsForm/ProfileSettingsForm.js @@ -67,6 +67,7 @@ const ProfileSettingsFormComponent = props => { intl, invalid, onImageUpload, + pristine, profileImage, rootClassName, submitting, @@ -143,7 +144,8 @@ const ProfileSettingsFormComponent = props => { : null; const classes = classNames(rootClassName || css.root, className); - const submitDisabled = invalid || submitting || uploadInProgress || updateInProgress; + const inProgress = uploadInProgress || updateInProgress; + const submitDisabled = invalid || submitting || inProgress || pristine; return (
    diff --git a/src/containers/ProfileSettingsPage/ProfileSettingsPage.duck.js b/src/containers/ProfileSettingsPage/ProfileSettingsPage.duck.js index 361e82e3..751cc856 100644 --- a/src/containers/ProfileSettingsPage/ProfileSettingsPage.duck.js +++ b/src/containers/ProfileSettingsPage/ProfileSettingsPage.duck.js @@ -1,13 +1,8 @@ import { updatedEntities, denormalisedEntities } from '../../util/data'; import { currentUserShowSuccess } from '../../ducks/user.duck'; -const requestAction = actionType => params => ({ type: actionType, payload: { params } }); -const successAction = actionType => result => ({ type: actionType, payload: result.data }); -const errorAction = actionType => error => ({ type: actionType, payload: error, error: true }); - // ================ Action types ================ // -export const MARK_FORM_UPDATED = 'app/EditListingPage/MARK_FORM_UPDATED'; export const CLEAR_UPDATED_FORM = 'app/EditListingPage/CLEAR_UPDATED_FORM'; export const UPLOAD_IMAGE_REQUEST = 'app/ProfileSettingsPage/UPLOAD_IMAGE_REQUEST'; @@ -21,7 +16,6 @@ export const UPDATE_PROFILE_ERROR = 'app/ProfileSettingsPage/UPDATE_PROFILE_ERRO // ================ Reducer ================ // const initialState = { - formUpdated: false, image: null, uploadImageError: null, uploadInProgress: false, @@ -70,10 +64,8 @@ export default function reducer(state = initialState, action = {}) { updateProfileError: payload, }; - case MARK_FORM_UPDATED: - return { ...state, formUpdated: payload }; case CLEAR_UPDATED_FORM: - return { ...state, formUpdated: null, updateProfileError: null, uploadImageError: null }; + return { ...state, updateProfileError: null, uploadImageError: null }; default: return state; @@ -84,24 +76,33 @@ export default function reducer(state = initialState, action = {}) { // ================ Action creators ================ // -export const markFormUpdated = tab => ({ - type: MARK_FORM_UPDATED, - payload: tab, -}); - export const clearUpdatedForm = () => ({ type: CLEAR_UPDATED_FORM, }); // SDK method: images.uploadListingImage -export const uploadImageRequest = requestAction(UPLOAD_IMAGE_REQUEST); -export const uploadImageSuccess = successAction(UPLOAD_IMAGE_SUCCESS); -export const uploadImageError = errorAction(UPLOAD_IMAGE_ERROR); +export const uploadImageRequest = params => ({ type: UPLOAD_IMAGE_REQUEST, payload: { params } }); +export const uploadImageSuccess = result => ({ type: UPLOAD_IMAGE_SUCCESS, payload: result.data }); +export const uploadImageError = error => ({ + type: UPLOAD_IMAGE_ERROR, + payload: error, + error: true, +}); // SDK method: sdk.currentUser.updateProfile -export const updateProfileRequest = requestAction(UPDATE_PROFILE_REQUEST); -export const updateProfileSuccess = successAction(UPDATE_PROFILE_SUCCESS); -export const updateProfileError = errorAction(UPDATE_PROFILE_ERROR); +export const updateProfileRequest = params => ({ + type: UPDATE_PROFILE_REQUEST, + payload: { params }, +}); +export const updateProfileSuccess = result => ({ + type: UPDATE_PROFILE_SUCCESS, + payload: result.data, +}); +export const updateProfileError = error => ({ + type: UPDATE_PROFILE_ERROR, + payload: error, + error: true, +}); // ================ Thunk ================ // From a81120c07882701b3023225667c3b5a8734fa8bd Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Fri, 8 Sep 2017 12:17:19 +0300 Subject: [PATCH 18/18] Avatar for current user (temporary fix) --- src/components/Avatar/Avatar.css | 6 ++++++ src/components/Avatar/Avatar.js | 35 +++++++++++++++++++++++++------- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/components/Avatar/Avatar.css b/src/components/Avatar/Avatar.css index 9712c103..7b9bcb17 100644 --- a/src/components/Avatar/Avatar.css +++ b/src/components/Avatar/Avatar.css @@ -63,6 +63,12 @@ color: var(--matterColorLight); } +.avatarImage { + width: 100%; + height: 100%; + border-radius: inherit; +} + .initials { padding-bottom: 4px; } diff --git a/src/components/Avatar/Avatar.js b/src/components/Avatar/Avatar.js index 03304e62..762bba71 100644 --- a/src/components/Avatar/Avatar.js +++ b/src/components/Avatar/Avatar.js @@ -2,20 +2,41 @@ import React, { PropTypes } from 'react'; import classNames from 'classnames'; import * as propTypes from '../../util/propTypes'; import { ensureUser } from '../../util/data'; +import { ResponsiveImage } from '../../components/'; import css from './Avatar.css'; const Avatar = props => { const { rootClassName, className, user } = props; const classes = classNames(rootClassName || css.root, className); - const { displayName, abbreviatedName } = ensureUser(user).attributes.profile; - const placeHolderAvatar = ( -
    - {abbreviatedName} -
    - ); + const avatarUser = ensureUser(user); + const { displayName, abbreviatedName } = avatarUser.attributes.profile; - return placeHolderAvatar; + // TODO this is a temporary avatar fix for currentUser's profile data. + // Avatar images should be included to all user's attributes in the future. + if (avatarUser.profileImage) { + return ( +
    + +
    + ); + + } else { + // Placeholder avatar (initials) + return ( +
    + {abbreviatedName} +
    + ); + } }; const { string, oneOfType } = PropTypes;