Merge pull request #376 from sharetribe/upload-avatar

Upload avatar
This commit is contained in:
Vesa Luusua 2017-09-08 12:28:01 +03:00 committed by GitHub
commit 41f4f42b07
32 changed files with 1189 additions and 264 deletions

View file

@ -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#0bc2a2e82be2c5eadbc22e511a750a2a91dc0fd5",
"source-map-support": "^0.4.15",
"url": "^0.11.0"
},

View file

@ -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,

View file

@ -7,123 +7,42 @@
* <input type="file" accept="images/*" onChange={handleChange} />
* </AddImages>
*/
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 => {
const { onClick } = props;
return (
<button className={css.removeImage} onClick={onClick}>
<svg
width="10px"
height="10px"
viewBox="0 0 10 10"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<g strokeWidth="1" fillRule="evenodd">
<g transform="translate(-821.000000, -311.000000)">
<g transform="translate(809.000000, 299.000000)">
<path
d="M21.5833333,16.5833333 L17.4166667,16.5833333 L17.4166667,12.4170833 C17.4166667,12.1866667 17.2391667,12 17.00875,12 C16.77875,12 16.5920833,12.18625 16.5920833,12.41625 L16.5883333,16.5833333 L12.4166667,16.5833333 C12.18625,16.5833333 12,16.7695833 12,17 C12,17.23 12.18625,17.4166667 12.4166667,17.4166667 L16.5875,17.4166667 L16.5833333,21.5829167 C16.5829167,21.8129167 16.7691667,21.9995833 16.9991667,22 L16.9995833,22 C17.2295833,22 17.41625,21.81375 17.4166667,21.58375 L17.4166667,17.4166667 L21.5833333,17.4166667 C21.8133333,17.4166667 22,17.23 22,17 C22,16.7695833 21.8133333,16.5833333 21.5833333,16.5833333"
transform="translate(17.000000, 17.000000) rotate(-45.000000) translate(-17.000000, -17.000000) "
/>
</g>
</g>
</g>
</svg>
</button>
);
};
const { any, 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
? <div className={css.thumbnailLoading}><FormattedMessage id="AddImages.upload" /></div>
: null;
const removeButton = imageId ? <RemoveImageButton onClick={handleRemoveClick} /> : null;
const classes = classNames(css.thumbnail, className);
return (
<Promised
key={id}
promise={this.state.promisedImage}
renderFulfilled={dataURL => {
return (
<div className={classes}>
<div className={css.aspectWrapper}>
<img src={dataURL} alt={file.name} className={css.rootForImage} />
</div>
{removeButton}
{uploadingOverlay}
</div>
);
}}
renderRejected={() => (
<div className={css.thumbnail}><FormattedMessage id="AddImages.couldNotReadFile" /></div>
)}
/>
);
}
}
Thumbnail.defaultProps = { className: null, imageId: null };
Thumbnail.propTypes = {
className: string,
file: any.isRequired,
id: string.isRequired,
imageId: uuid,
onRemoveImage: func.isRequired,
};
import RemoveImageButton from './RemoveImageButton';
const ThumbnailWrapper = props => {
const { className, image, savedImageAltText, onRemoveImage } = props;
const handleRemoveClick = e => {
e.stopPropagation();
onRemoveImage(image.id);
};
if (image.file) {
return <Thumbnail className={className} onRemoveImage={onRemoveImage} {...image} />;
// Add remove button only when the image has been uploaded and can be removed
const removeButton = image.imageId ? <RemoveImageButton onClick={handleRemoveClick} /> : null;
// While image is uploading we show overlay on top of thumbnail
const uploadingOverlay = !image.imageId
? <div className={css.thumbnailLoading}><SpinnerIcon /></div>
: null;
return (
<ImageFromFile
id={image.id}
className={className}
rootClassName={css.thumbnail}
file={image.file}
>
{removeButton}
{uploadingOverlay}
</ImageFromFile>
);
} else {
const handleRemoveClick = e => {
e.preventDefault();
onRemoveImage(image.id);
};
const classes = classNames(css.thumbnail, className);
return (
<div className={classes}>
@ -149,6 +68,8 @@ const ThumbnailWrapper = props => {
ThumbnailWrapper.defaultProps = { className: null };
const { array, func, node, string, object } = PropTypes;
ThumbnailWrapper.propTypes = {
className: string,
image: object.isRequired,

View file

@ -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 (
<button className={classes} onClick={onClick}>
<svg
width="10px"
height="10px"
viewBox="0 0 10 10"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<g strokeWidth="1" fillRule="evenodd">
<g transform="translate(-821.000000, -311.000000)">
<g transform="translate(809.000000, 299.000000)">
<path
d="M21.5833333,16.5833333 L17.4166667,16.5833333 L17.4166667,12.4170833 C17.4166667,12.1866667 17.2391667,12 17.00875,12 C16.77875,12 16.5920833,12.18625 16.5920833,12.41625 L16.5883333,16.5833333 L12.4166667,16.5833333 C12.18625,16.5833333 12,16.7695833 12,17 C12,17.23 12.18625,17.4166667 12.4166667,17.4166667 L16.5875,17.4166667 L16.5833333,21.5829167 C16.5829167,21.8129167 16.7691667,21.9995833 16.9991667,22 L16.9995833,22 C17.2295833,22 17.41625,21.81375 17.4166667,21.58375 L17.4166667,17.4166667 L21.5833333,17.4166667 C21.8133333,17.4166667 22,17.23 22,17 C22,16.7695833 21.8133333,16.5833333 21.5833333,16.5833333"
transform="translate(17.000000, 17.000000) rotate(-45.000000) translate(-17.000000, -17.000000) "
/>
</g>
</g>
</g>
</svg>
</button>
);
};
RemoveImageButton.defaultProps = { className: null, rootClassName: null };
const { func, string } = PropTypes;
RemoveImageButton.propTypes = {
className: string,
rootClassName: string,
onClick: func.isRequired,
};
export default RemoveImageButton;

View file

@ -63,6 +63,12 @@
color: var(--matterColorLight);
}
.avatarImage {
width: 100%;
height: 100%;
border-radius: inherit;
}
.initials {
padding-bottom: 4px;
}

View file

@ -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 = (
<div className={classes} title={displayName}>
<span className={css.initials}>{abbreviatedName}</span>
</div>
);
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 (
<div className={classes} title={displayName}>
<ResponsiveImage
rootClassName={css.avatarImage}
alt={displayName}
image={avatarUser.profileImage}
nameSet={[
{ name: 'square-xlarge', size: '1x' },
{ name: 'square-xlarge2x', size: '2x' },
]}
/>
</div>
);
} else {
// Placeholder avatar (initials)
return (
<div className={classes} title={displayName}>
<span className={css.initials}>{abbreviatedName}</span>
</div>
);
}
};
const { string, oneOfType } = PropTypes;

View file

@ -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);
}

View file

@ -0,0 +1,77 @@
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, aspectRatioClassName, file, id, children } = this.props;
const classes = classNames(rootClassName || css.root, className);
const aspectRatioClasses = aspectRatioClassName || css.aspectWrapper;
return (
<Promised
key={id}
promise={this.state.promisedImage}
renderFulfilled={dataURL => {
return (
<div className={classes}>
<div className={css.threeToTwoWrapper}>
<div className={aspectRatioClasses}>
<img src={dataURL} alt={file.name} className={css.rootForImage} />
</div>
</div>
{children}
</div>
);
}}
renderRejected={() => (
<div className={classes}><FormattedMessage id="ImageFromFile.couldNotReadFile" /></div>
)}
/>
);
}
}
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,
};
export default ImageFromFile;

View file

@ -237,6 +237,7 @@
transition: width var(--transitionStyleButton);
}
.profileSettingsLink,
.yourListingsLink {
@apply --marketplaceH4FontStyles;
position: relative;

View file

@ -68,7 +68,19 @@ const TopbarDesktop = props => {
<Avatar className={css.avatar} user={currentUser} />
</MenuLabel>
<MenuContent className={css.profileMenuContent}>
<MenuItem key="manageListings">
<MenuItem key="ProfileSettingsPage">
<NamedLink
className={classNames(
css.profileSettingsLink,
currentPageClass('ProfileSettingsPage')
)}
name="ProfileSettingsPage"
>
<span className={css.menuItemBorder} />
<FormattedMessage id="TopbarDesktop.profileSettingsLink" />
</NamedLink>
</MenuItem>
<MenuItem key="ManageListingsPage">
<NamedLink
className={classNames(css.yourListingsLink, currentPageClass('ManageListingsPage'))}
name="ManageListingsPage"

View file

@ -102,6 +102,22 @@ exports[`TopbarDesktop data matches snapshot 1`] = `
style={Object {}}>
<ul
className="">
<li
className=""
role="menuitem">
<a
className=""
href="/profile-settings"
onClick={[Function]}
style={Object {}}
title={null}>
<span
className={undefined} />
<span>
Profile settings
</span>
</a>
</li>
<li
className=""
role="menuitem">

View file

@ -93,6 +93,12 @@ const TopbarMobileMenu = props => {
>
<FormattedMessage id="TopbarMobileMenu.yourListingsLink" />
</NamedLink>
<NamedLink
className={classNames(css.navigationLink, currentPageClass('ProfileSettingsPage'))}
name="ProfileSettingsPage"
>
<FormattedMessage id="TopbarMobileMenu.profileSettingsLink" />
</NamedLink>
</div>
<div className={css.footer}>

View file

@ -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;
}

View file

@ -12,11 +12,10 @@ const UserNav = props => {
const tabs = [
{
text: <FormattedMessage id="ManageListingsPage.profileSettings" />,
selected: selectedPageName === 'EditProfilePage',
disabled: true,
selected: selectedPageName === 'ProfileSettingsPage',
disabled: false,
linkProps: {
name: 'EditProfilePage',
params: { displayName: 'testinglink' },
name: 'ProfileSettingsPage',
},
},
{

View file

@ -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,

View file

@ -1,106 +0,0 @@
import React, { PropTypes } from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
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';
export const EditProfilePageComponent = props => {
const {
authInfoError,
authInProgress,
currentUser,
currentUserHasListings,
history,
isAuthenticated,
location,
logoutError,
notificationCount,
onLogout,
onManageDisableScrolling,
params,
} = props;
return (
<PageLayout
authInfoError={authInfoError}
logoutError={logoutError}
title={`Edit profile page with display name: ${params.displayName}`}
>
<Topbar
authInProgress={authInProgress}
currentUser={currentUser}
currentUserHasListings={currentUserHasListings}
history={history}
isAuthenticated={isAuthenticated}
location={location}
notificationCount={notificationCount}
onLogout={onLogout}
onManageDisableScrolling={onManageDisableScrolling}
/>
</PageLayout>
);
};
EditProfilePageComponent.defaultProps = {
authInfoError: null,
currentUser: null,
logoutError: null,
notificationCount: 0,
};
const { bool, func, instanceOf, number, object, shape, string } = PropTypes;
EditProfilePageComponent.propTypes = {
authInfoError: instanceOf(Error),
authInProgress: bool.isRequired,
currentUser: propTypes.currentUser,
currentUserHasListings: bool.isRequired,
isAuthenticated: bool.isRequired,
logoutError: instanceOf(Error),
notificationCount: number,
onLogout: func.isRequired,
onManageDisableScrolling: func.isRequired,
params: shape({ displayName: string.isRequired }).isRequired,
// from withRouter
history: shape({
push: func.isRequired,
}).isRequired,
location: shape({ state: object }).isRequired,
};
const mapStateToProps = state => {
// PageLayout needs authInfoError and logoutError, Topbar needs isAuthenticated
const { authInfoError, isAuthenticated, logoutError } = state.Auth;
// Topbar needs user info.
const {
currentUser,
currentUserHasListings,
currentUserNotificationCount: notificationCount,
} = state.user;
return {
authInfoError,
authInProgress: authenticationInProgress(state),
currentUser,
currentUserHasListings,
currentUserNotificationCount: notificationCount,
isAuthenticated,
logoutError,
scrollingDisabled: isScrollingDisabled(state),
};
};
const mapDispatchToProps = dispatch => ({
onLogout: historyPush => dispatch(logout(historyPush)),
onManageDisableScrolling: (componentId, disableScrolling) =>
dispatch(manageDisableScrolling(componentId, disableScrolling)),
});
const EditProfilePage = compose(connect(mapStateToProps, mapDispatchToProps), withRouter)(
EditProfilePageComponent
);
export default EditProfilePage;

View file

@ -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 (
<PageLayout authInfoError={authInfoError} logoutError={logoutError} title="Manage listings">
<PageLayout
authInfoError={authInfoError}
logoutError={logoutError}
scrollingDisabled={scrollingDisabled}
title="Manage listings"
>
<Topbar
authInProgress={authInProgress}
currentUser={currentUser}
@ -115,6 +121,7 @@ export class ManageListingsPageComponent extends Component {
notificationCount={notificationCount}
onLogout={onLogout}
onManageDisableScrolling={onManageDisableScrolling}
scrollingDisabled={scrollingDisabled}
/>
<UserNav selectedPageName="ManageListingsPage" />
{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({

View file

@ -2,6 +2,7 @@ exports[`ContactDetailsPage matches snapshot 1`] = `
<withRouter(PageLayout)
authInfoError={null}
logoutError={null}
scrollingDisabled={false}
title="Manage listings">
<Topbar
authInProgress={false}
@ -21,7 +22,8 @@ exports[`ContactDetailsPage matches snapshot 1`] = `
}
notificationCount={0}
onLogout={[Function]}
onManageDisableScrolling={[Function]} />
onManageDisableScrolling={[Function]}
scrollingDisabled={false} />
<UserNav
className={null}
rootClassName={null}

View file

@ -0,0 +1,260 @@
@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) {
display: block;
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,<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g stroke="#4A4A4A" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"><path d="M5.307 11.155L1 13l1.846-4.308L10.54 1 13 3.46zM11 5L9 3M5 11L3 9"/></g></svg>');
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;
}

View file

@ -0,0 +1,227 @@
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
? <div className={css.error}>
<FormattedMessage id="ProfileSettingsForm.imageUploadFailed" />
</div>
: null;
return (
<div className={css.uploadAvatarWrapper}>
<label className={css.label} htmlFor={id}>{label}</label>
<input
accept={accept}
className={css.uploadAvatarInput}
disabled={disabled}
id={id}
name={name}
onChange={event => {
const file = event.target.files[0];
const tempId = `${file.name}_${Date.now()}`;
onChange({ id: tempId, file });
}}
type={type}
/>
{error}
</div>
);
};
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,
pristine,
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
? <div className={css.uploadingImageOverlay}><SpinnerIcon /></div>
: null;
const hasUploadError = !!uploadImageError && !uploadInProgress;
const errorClasses = classNames({ [css.avatarUploadError]: hasUploadError });
const avatarImage = uploadInProgress && profileImage.file
? <ImageFromFile
id={profileImage.id}
className={errorClasses}
rootClassName={css.uploadingImage}
aspectRatioClassName={css.squareAspectRatio}
file={profileImage.file}
>
{uploadingOverlay}
</ImageFromFile>
: <Avatar className={errorClasses} user={user} />;
const chooseAvatarLabel = profileImage.imageId || (uploadInProgress && profileImage.file)
? <div className={css.avatarContainer}>
{avatarImage}
<div className={css.changeAvatar}>
<FormattedMessage id="ProfileSettingsForm.changeAvatar" />
</div>
</div>
: <div className={css.avatarPlaceholder}>
<div className={css.avatarPlaceholderText}>
<FormattedMessage id="ProfileSettingsForm.addYourProfilePicture" />
</div>
<div className={css.avatarPlaceholderTextMobile}>
<FormattedMessage id="ProfileSettingsForm.addYourProfilePictureMobile" />
</div>
</div>;
const submitError = updateProfileError
? <div className={css.error}>
<FormattedMessage id="ProfileSettingsForm.updateProfileFailed" />
</div>
: null;
const classes = classNames(rootClassName || css.root, className);
const inProgress = uploadInProgress || updateInProgress;
const submitDisabled = invalid || submitting || inProgress || pristine;
return (
<form className={classes} onSubmit={handleSubmit}>
<div className={css.sectionContainer}>
<h3 className={css.sectionTitle}>
<FormattedMessage id="ProfileSettingsForm.yourProfilePicture" />
</h3>
<Field
accept={ACCEPT_IMAGES}
component={RenderAvatar}
disabled={uploadInProgress}
id="ProfileSettingsForm.changeAvatar"
label={chooseAvatarLabel}
name="profileImage"
onChange={onImageUpload}
type="file"
uploadImageError={uploadImageError}
/>
<div className={css.tip}><FormattedMessage id="ProfileSettingsForm.tip" /></div>
<div className={css.fileInfo}><FormattedMessage id="ProfileSettingsForm.fileInfo" /></div>
</div>
<div className={classNames(css.sectionContainer, css.lastSection)}>
<h3 className={css.sectionTitle}>
<FormattedMessage id="ProfileSettingsForm.yourName" />
</h3>
<div className={css.nameContainer}>
<TextInputField
className={css.firstName}
type="text"
name="firstName"
id={`${form}.firstName`}
label={firstNameLabel}
placeholder={firstNamePlaceholder}
validate={firstNameRequired}
/>
<TextInputField
className={css.lastName}
type="text"
name="lastName"
id={`${form}.lastName`}
label={lastNameLabel}
placeholder={lastNamePlaceholder}
validate={lastNameRequired}
/>
</div>
</div>
{submitError}
<Button className={css.submitButton} type="submit" disabled={submitDisabled}>
<FormattedMessage id="ProfileSettingsForm.saveChanges" />
</Button>
</form>
);
};
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;

View file

@ -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;
}
}

View file

@ -0,0 +1,143 @@
import { updatedEntities, denormalisedEntities } from '../../util/data';
import { currentUserShowSuccess } from '../../ducks/user.duck';
// ================ Action types ================ //
export const CLEAR_UPDATED_FORM = 'app/EditListingPage/CLEAR_UPDATED_FORM';
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 = {
image: null,
uploadImageError: null,
uploadInProgress: false,
updateInProgress: false,
updateProfileError: null,
};
export default function reducer(state = initialState, action = {}) {
const { type, payload } = action;
switch (type) {
case UPLOAD_IMAGE_REQUEST:
// payload.params: { id: 'tempId', file }
return {
...state,
image: { ...payload.params },
uploadInProgress: true,
uploadImageError: null,
};
case UPLOAD_IMAGE_SUCCESS: {
// payload: { id: 'tempId', imageId: 'some-real-id'}
const { id, imageId } = payload;
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, 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 CLEAR_UPDATED_FORM:
return { ...state, updateProfileError: null, uploadImageError: null };
default:
return state;
}
}
// ================ Selectors ================ //
// ================ Action creators ================ //
export const clearUpdatedForm = () => ({
type: CLEAR_UPDATED_FORM,
});
// SDK method: images.uploadListingImage
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 = 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 ================ //
// Images return imageId which we need to map with previously generated temporary id
export function uploadImage(actionPayload) {
return (dispatch, getState, sdk) => {
const id = actionPayload.id;
dispatch(uploadImageRequest(actionPayload));
return sdk.images
.uploadProfileImage({ image: actionPayload.file })
.then(resp => dispatch(uploadImageSuccess({ data: { id, imageId: resp.data.data.id } })))
.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)));
};
};

View file

@ -0,0 +1,198 @@
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 { clearUpdatedForm, updateProfile, uploadImage } from './ProfileSettingsPage.duck';
import { PageLayout, Topbar, UserNav } from '../../components';
import { ProfileSettingsForm } from '../../containers';
import css from './ProfileSettingsPage.css';
const onImageUploadHandler = (values, fn) => {
const { id, imageId, file } = values;
if (file) {
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 {
authInfoError,
authInProgress,
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
? <ProfileSettingsForm
className={css.form}
currentUser={currentUser}
initialValues={{ firstName, lastName, profileImage }}
profileImage={profileImage}
onImageUpload={e => onImageUploadHandler(e, onImageUpload)}
uploadInProgress={uploadInProgress}
updateInProgress={updateInProgress}
uploadImageError={uploadImageError}
updateProfileError={updateProfileError}
onSubmit={values => {
onSubmit({ ...values, profileImage }, onUpdateProfile);
}}
onChange={onChange}
/>
: null;
return (
<PageLayout
className={css.root}
authInfoError={authInfoError}
logoutError={logoutError}
title="Profile settings"
scrollingDisabled={scrollingDisabled}
>
<Topbar
authInProgress={authInProgress}
currentPage="ProfileSettingsPage"
currentUser={currentUser}
currentUserHasListings={currentUserHasListings}
history={history}
isAuthenticated={isAuthenticated}
location={location}
notificationCount={notificationCount}
onLogout={onLogout}
onManageDisableScrolling={onManageDisableScrolling}
/>
<UserNav selectedPageName="ProfileSettingsPage" />
<div className={css.content}>
<h1><FormattedMessage id="ProfileSettingsPage.title" /></h1>
{profileSettingsForm}
</div>
</PageLayout>
);
};
ProfileSettingsPageComponent.defaultProps = {
authInfoError: null,
currentUser: null,
logoutError: null,
notificationCount: 0,
uploadImageError: null,
updateProfileError: null,
image: null,
};
const { bool, func, instanceOf, number, object, shape, string } = PropTypes;
ProfileSettingsPageComponent.propTypes = {
authInfoError: instanceOf(Error),
authInProgress: bool.isRequired,
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({
push: func.isRequired,
}).isRequired,
location: shape({ state: object }).isRequired,
};
const mapStateToProps = state => {
// PageLayout needs authInfoError and logoutError, Topbar needs isAuthenticated
const { authInfoError, isAuthenticated, logoutError } = state.Auth;
// Topbar needs user info.
const {
currentUser,
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 => ({
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)(
ProfileSettingsPageComponent
);
export default ProfileSettingsPage;

View file

@ -1,22 +1,27 @@
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(
<EditProfilePageComponent
params={{ displayName: 'my-shop' }}
history={{ push: noop }}
location={{ search: '' }}
scrollingDisabled={false}
<ProfileSettingsPageComponent
authInProgress={false}
currentUserHasListings={false}
history={{ push: noop }}
isAuthenticated={false}
location={{ search: '' }}
onChange={noop}
onImageUpload={noop}
onLogout={noop}
onManageDisableScrolling={noop}
onUpdateProfile={noop}
params={{ displayName: 'my-shop' }}
scrollingDisabled={false}
updateInProgress={false}
uploadInProgress={false}
/>
);
expect(tree).toMatchSnapshot();

View file

@ -2,9 +2,11 @@ exports[`ContactDetailsPage matches snapshot 1`] = `
<withRouter(PageLayout)
authInfoError={null}
logoutError={null}
title="Edit profile page with display name: my-shop">
scrollingDisabled={false}
title="Profile settings">
<Topbar
authInProgress={false}
currentPage="ProfileSettingsPage"
currentUser={null}
currentUserHasListings={false}
history={
@ -21,5 +23,16 @@ exports[`ContactDetailsPage matches snapshot 1`] = `
notificationCount={0}
onLogout={[Function]}
onManageDisableScrolling={[Function]} />
<UserNav
className={null}
rootClassName={null}
selectedPageName="ProfileSettingsPage" />
<div>
<h1>
<FormattedMessage
id="ProfileSettingsPage.title"
values={Object {}} />
</h1>
</div>
</withRouter(PageLayout)>
`;

View file

@ -9,7 +9,6 @@ import EditListingLocationForm from './EditListingLocationForm/EditListingLocati
import EditListingPage from './EditListingPage/EditListingPage';
import EditListingPhotosForm from './EditListingPhotosForm/EditListingPhotosForm';
import EditListingPricingForm from './EditListingPricingForm/EditListingPricingForm';
import EditProfilePage from './EditProfilePage/EditProfilePage';
import EmailVerificationPage from './EmailVerificationPage/EmailVerificationPage';
import EmailVerificationForm from './EmailVerificationForm/EmailVerificationForm';
import InboxPage from './InboxPage/InboxPage';
@ -26,6 +25,8 @@ 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';
import SecurityPage from './SecurityPage/SecurityPage';
@ -46,7 +47,6 @@ export {
EditListingPage,
EditListingPhotosForm,
EditListingPricingForm,
EditProfilePage,
EmailVerificationPage,
EmailVerificationForm,
InboxPage,
@ -63,6 +63,8 @@ export {
PayoutDetailsForm,
PayoutPreferencesPage,
ProfilePage,
ProfileSettingsForm,
ProfileSettingsPage,
SalePage,
SearchPage,
SecurityPage,

View file

@ -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,
};

View file

@ -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());

View file

@ -5,7 +5,6 @@ import {
CheckoutPage,
ContactDetailsPage,
EditListingPage,
EditProfilePage,
InboxPage,
LandingPage,
ListingPage,
@ -16,6 +15,7 @@ import {
PasswordForgottenPage,
PayoutPreferencesPage,
ProfilePage,
ProfileSettingsPage,
SalePage,
SearchPage,
SecurityPage,
@ -122,18 +122,16 @@ const routesConfiguration = [
exact: true,
name: 'ProfilePage',
component: props => <ProfilePage {...props} />,
routes: [
{
path: '/u/:displayName/edit',
auth: true,
exact: true,
name: 'EditProfilePage',
component: props => <EditProfilePage {...props} />,
},
],
},
],
},
{
path: '/profile-settings',
auth: true,
exact: true,
name: 'ProfileSettingsPage',
component: props => <ProfileSettingsPage {...props} />,
},
{
path: '/login',
exact: true,

View file

@ -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.",
@ -252,6 +251,23 @@
"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",
"ProfileSettingsPage.title": "Profile settings",
"ResponsiveImage.noImage": "No image",
"SaleDetailsPanel.bookingBreakdownTitle": "Booking breakdown",
"SaleDetailsPanel.listingAcceptedTitle": "Woohoo! You accepted a request from {customerName} for {listingLink}",
@ -355,6 +371,7 @@
"TopbarDesktop.login": "Log in",
"TopbarDesktop.logo": "Logo",
"TopbarDesktop.logout": "Log out",
"TopbarDesktop.profileSettingsLink": "Profile settings",
"TopbarDesktop.signup": "Sign up",
"TopbarDesktop.yourListingsLink": "Your listings",
"TopbarMobileMenu.greeting": "Hello {displayName}",
@ -362,6 +379,7 @@
"TopbarMobileMenu.loginLink": "Log in",
"TopbarMobileMenu.logoutLink": "Log out",
"TopbarMobileMenu.newListingLink": "+ Add your sauna",
"TopbarMobileMenu.profileSettingsLink": "Profile settings",
"TopbarMobileMenu.signupLink": "Sign up",
"TopbarMobileMenu.signupOrLogin": "{signup} or {login}",
"TopbarMobileMenu.unauthorizedGreeting": "Hello there,{lineBreak}would you like to {signupOrLogin}?",

View file

@ -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 };
};

View file

@ -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#0bc2a2e82be2c5eadbc22e511a750a2a91dc0fd5":
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#0bc2a2e82be2c5eadbc22e511a750a2a91dc0fd5"
dependencies:
axios "^0.15.3"
js-cookie "^2.1.3"