mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-30 18:16:48 +10:00
commit
41f4f42b07
32 changed files with 1189 additions and 264 deletions
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
42
src/components/AddImages/RemoveImageButton.js
Normal file
42
src/components/AddImages/RemoveImageButton.js
Normal 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;
|
||||
|
|
@ -63,6 +63,12 @@
|
|||
color: var(--matterColorLight);
|
||||
}
|
||||
|
||||
.avatarImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.initials {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
31
src/components/ImageFromFile/ImageFromFile.css
Normal file
31
src/components/ImageFromFile/ImageFromFile.css
Normal 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);
|
||||
}
|
||||
77
src/components/ImageFromFile/ImageFromFile.js
Normal file
77
src/components/ImageFromFile/ImageFromFile.js
Normal 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;
|
||||
|
|
@ -237,6 +237,7 @@
|
|||
transition: width var(--transitionStyleButton);
|
||||
}
|
||||
|
||||
.profileSettingsLink,
|
||||
.yourListingsLink {
|
||||
@apply --marketplaceH4FontStyles;
|
||||
position: relative;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
260
src/containers/ProfileSettingsForm/ProfileSettingsForm.css
Normal file
260
src/containers/ProfileSettingsForm/ProfileSettingsForm.css
Normal 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;
|
||||
}
|
||||
227
src/containers/ProfileSettingsForm/ProfileSettingsForm.js
Normal file
227
src/containers/ProfileSettingsForm/ProfileSettingsForm.js
Normal 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;
|
||||
14
src/containers/ProfileSettingsPage/ProfileSettingsPage.css
Normal file
14
src/containers/ProfileSettingsPage/ProfileSettingsPage.css
Normal 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;
|
||||
}
|
||||
}
|
||||
143
src/containers/ProfileSettingsPage/ProfileSettingsPage.duck.js
Normal file
143
src/containers/ProfileSettingsPage/ProfileSettingsPage.duck.js
Normal 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)));
|
||||
};
|
||||
};
|
||||
198
src/containers/ProfileSettingsPage/ProfileSettingsPage.js
Normal file
198
src/containers/ProfileSettingsPage/ProfileSettingsPage.js
Normal 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;
|
||||
|
|
@ -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();
|
||||
|
|
@ -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)>
|
||||
`;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}?",
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue