ProfileSettingsForm

This commit is contained in:
Vesa Luusua 2017-09-08 01:22:41 +03:00
parent a069d7f0db
commit cec8ce3cb6
4 changed files with 502 additions and 0 deletions

View file

@ -0,0 +1,259 @@
@import '../../marketplace.css';
:root {
--avatarSize: 96px;
--avatarSizeDesktop: 240px;
}
.root {
margin-top: 23px;
@media (--viewportMedium) {
margin-top: 35px;
}
}
.sectionContainer {
padding: 0;
margin-bottom: 34px;
@media (--viewportMedium) {
padding: 0;
margin-bottom: 54px;
}
}
.sectionTitle {
/* Font */
color: var(--matterColorAnti);
margin-top: 0;
margin-bottom: 13px;
@media (--viewportMedium) {
margin-top: 0;
margin-bottom: 20px;
}
}
.lastSection {
margin-bottom: 66px;
@media (--viewportMedium) {
margin-bottom: 111px;
}
}
.uploadAvatarInput {
display: none;
}
.uploadAvatarWrapper {
margin-top: 19px;
margin-bottom: 18px;
@media (--viewportMedium) {
margin-top: 44px;
margin-bottom: 20px;
}
}
.label {
width: var(--avatarSize);
@media (--viewportMedium) {
width: var(--avatarSizeDesktop);
}
}
.avatarPlaceholder,
.avatarContainer {
/* Dimension */
position: relative;
width: var(--avatarSize);
height: var(--avatarSize);
/* Center content */
display: flex;
align-items: center;
justify-content: center;
/* Initial coloring */
background-color: var(--matterColorBright);
border-radius: calc(var(--avatarSize) / 2);
cursor: pointer;
@media (--viewportMedium) {
width: var(--avatarSizeDesktop);
height: var(--avatarSizeDesktop);
border-radius: calc(var(--avatarSizeDesktop) / 2);
}
}
.avatarPlaceholder {
/* Placeholder border */
border-style: dashed;
border-color: var(--matterColorNegative);
border-width: 2px;
&:hover {
border-color: var(--matterColorAnti);
}
}
.avatarPlaceholderTextMobile {
@media (--viewportMedium) {
display: none;
}
}
.avatarPlaceholderText {
display: none;
@media (--viewportMedium) {
max-width: 130px;
text-align: center;
}
}
.avatarUploadError {
/* Placeholder border */
border-style: dashed;
border-color: var(--failColor);
border-width: 2px;
}
.error {
/* Font */
@apply --marketplaceH4FontStyles;
color: var(--failColor);
margin-top: 18px;
margin-bottom: 0;
@media (--viewportMedium) {
margin-top: 22px;
margin-bottom: 2px;
}
}
.changeAvatar {
/* Font */
@apply --marketplaceH5FontStyles;
/* Positioning: right */
position: absolute;
bottom: 27px;
right: -129px;
/* Dimensions */
width: 105px;
height: 41px;
padding: 9px 10px 9px 35px;
/* Look and feel (buttonish) */
background-color: var(--matterColorLight);
background-image: url('data:image/svg+xml;utf8,<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,225 @@
import React, { PropTypes } from 'react';
import { compose } from 'redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form';
import classNames from 'classnames';
import { ensureCurrentUser } from '../../util/data';
import * as validators from '../../util/validators';
import { Avatar, Button, ImageFromFile, SpinnerIcon, TextInputField } from '../../components';
import css from './ProfileSettingsForm.css';
const ACCEPT_IMAGES = 'image/*';
const RenderAvatar = props => {
const { accept, id, input, label, type, disabled, uploadImageError } = props;
const { name, onChange } = input;
const error = uploadImageError
? <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,
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 submitDisabled = invalid || submitting || uploadInProgress || updateInProgress;
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

@ -25,6 +25,7 @@ import PasswordForgottenPage from './PasswordForgottenPage/PasswordForgottenPage
import PayoutDetailsForm from './PayoutDetailsForm/PayoutDetailsForm';
import PayoutPreferencesPage from './PayoutPreferencesPage/PayoutPreferencesPage';
import ProfilePage from './ProfilePage/ProfilePage';
import ProfileSettingsForm from './ProfileSettingsForm/ProfileSettingsForm';
import ProfileSettingsPage from './ProfileSettingsPage/ProfileSettingsPage';
import SalePage from './SalePage/SalePage';
import SearchPage from './SearchPage/SearchPage';
@ -62,6 +63,7 @@ export {
PayoutDetailsForm,
PayoutPreferencesPage,
ProfilePage,
ProfileSettingsForm,
ProfileSettingsPage,
SalePage,
SearchPage,

View file

@ -251,6 +251,22 @@
"PayoutDetailsForm.streetAddressRequired": "This field is required",
"PayoutDetailsForm.submitButtonText": "Save details & publish listing",
"PayoutDetailsForm.title": "One more thing: payout preferences",
"ProfileSettingsForm.addYourProfilePicture": "+ Add your profile picture…",
"ProfileSettingsForm.addYourProfilePictureMobile": "+ Add",
"ProfileSettingsForm.changeAvatar": "Change",
"ProfileSettingsForm.fileInfo": ".JPG, .GIF or .PNG max. 10 MB",
"ProfileSettingsForm.firstNameLabel": "First name",
"ProfileSettingsForm.firstNamePlaceholder": "John",
"ProfileSettingsForm.firstNameRequired": "This field is required",
"ProfileSettingsForm.imageUploadFailed": "Whoopsie, something went wrong - please try again.",
"ProfileSettingsForm.lastNameLabel": "Last name",
"ProfileSettingsForm.lastNamePlaceholder": "Doe",
"ProfileSettingsForm.lastNameRequired": "This field is required",
"ProfileSettingsForm.saveChanges": "Save changes",
"ProfileSettingsForm.tip": "Tip: Choose an image where your face is recognizable.",
"ProfileSettingsForm.updateProfileFailed": "Whoopsie, something went wrong - please try again.",
"ProfileSettingsForm.yourName": "Your name",
"ProfileSettingsForm.yourProfilePicture": "Your profile picture",
"ResponsiveImage.noImage": "No image",
"SaleDetailsPanel.bookingBreakdownTitle": "Booking breakdown",
"SaleDetailsPanel.listingAcceptedTitle": "Woohoo! You accepted a request from {customerName} for {listingLink}",