From cec8ce3cb676b7152003326d9e24cb1ee6cde546 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Fri, 8 Sep 2017 01:22:41 +0300 Subject: [PATCH] ProfileSettingsForm --- .../ProfileSettingsForm.css | 259 ++++++++++++++++++ .../ProfileSettingsForm.js | 225 +++++++++++++++ src/containers/index.js | 2 + src/translations/en.json | 16 ++ 4 files changed, 502 insertions(+) create mode 100644 src/containers/ProfileSettingsForm/ProfileSettingsForm.css create mode 100644 src/containers/ProfileSettingsForm/ProfileSettingsForm.js diff --git a/src/containers/ProfileSettingsForm/ProfileSettingsForm.css b/src/containers/ProfileSettingsForm/ProfileSettingsForm.css new file mode 100644 index 00000000..3649566d --- /dev/null +++ b/src/containers/ProfileSettingsForm/ProfileSettingsForm.css @@ -0,0 +1,259 @@ +@import '../../marketplace.css'; + +:root { + --avatarSize: 96px; + --avatarSizeDesktop: 240px; +} + +.root { + margin-top: 23px; + + @media (--viewportMedium) { + margin-top: 35px; + } +} + +.sectionContainer { + padding: 0; + margin-bottom: 34px; + + @media (--viewportMedium) { + padding: 0; + margin-bottom: 54px; + } +} + +.sectionTitle { + /* Font */ + color: var(--matterColorAnti); + + margin-top: 0; + margin-bottom: 13px; + @media (--viewportMedium) { + margin-top: 0; + margin-bottom: 20px; + } +} + +.lastSection { + margin-bottom: 66px; + @media (--viewportMedium) { + margin-bottom: 111px; + } +} + +.uploadAvatarInput { + display: none; +} + +.uploadAvatarWrapper { + margin-top: 19px; + margin-bottom: 18px; + + @media (--viewportMedium) { + margin-top: 44px; + margin-bottom: 20px; + } +} + +.label { + width: var(--avatarSize); + @media (--viewportMedium) { + width: var(--avatarSizeDesktop); + } +} + +.avatarPlaceholder, +.avatarContainer { + /* Dimension */ + position: relative; + width: var(--avatarSize); + height: var(--avatarSize); + + /* Center content */ + display: flex; + align-items: center; + justify-content: center; + + /* Initial coloring */ + background-color: var(--matterColorBright); + border-radius: calc(var(--avatarSize) / 2); + cursor: pointer; + + @media (--viewportMedium) { + width: var(--avatarSizeDesktop); + height: var(--avatarSizeDesktop); + border-radius: calc(var(--avatarSizeDesktop) / 2); + } + +} + +.avatarPlaceholder { + /* Placeholder border */ + border-style: dashed; + border-color: var(--matterColorNegative); + border-width: 2px; + + &:hover { + border-color: var(--matterColorAnti); + } +} + +.avatarPlaceholderTextMobile { + @media (--viewportMedium) { + display: none; + } +} +.avatarPlaceholderText { + display: none; + + @media (--viewportMedium) { + max-width: 130px; + text-align: center; + } +} + +.avatarUploadError { + /* Placeholder border */ + border-style: dashed; + border-color: var(--failColor); + border-width: 2px; +} + +.error { + /* Font */ + @apply --marketplaceH4FontStyles; + color: var(--failColor); + margin-top: 18px; + margin-bottom: 0; + + @media (--viewportMedium) { + margin-top: 22px; + margin-bottom: 2px; + } +} + +.changeAvatar { + /* Font */ + @apply --marketplaceH5FontStyles; + + /* Positioning: right */ + position: absolute; + bottom: 27px; + right: -129px; + /* Dimensions */ + width: 105px; + height: 41px; + padding: 9px 10px 9px 35px; + + /* Look and feel (buttonish) */ + background-color: var(--matterColorLight); + background-image: url('data:image/svg+xml;utf8,'); + background-repeat: no-repeat; + background-position: 15px center; + border: solid 1px var(--matterColorNegative); + + margin-top: 0; + margin-bottom: 0; + + @media (--viewportMedium) { + /* Position: under */ + bottom: -8px; + right: auto; + margin-top: 0; + margin-bottom: 0; + } +} + +.uploadingImage { + /* Dimensions */ + width: var(--avatarSize); + height: var(--avatarSize); + + /* Image fitted to container */ + object-fit: cover; + background-color: var(--matterColorNegative); /* Loading BG color */ + border-radius: calc(var(--avatarSize) / 2); + overflow: hidden; + + display: block; + position: relative; + margin: 0; + + @media (--viewportMedium) { + width: var(--avatarSizeDesktop); + height: var(--avatarSizeDesktop); + border-radius: calc(var(--avatarSizeDesktop) / 2); + } +} + +.uploadingImageOverlay { + /* Cover everything (overlay) */ + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + + /* Overlay style */ + background-color: var(--matterColorLight); + opacity: 0.8; + + /* Center content */ + display: flex; + justify-content: center; + align-items: center; +} + +/* Avatar has square aspect ratio */ +/* Default is 3:2 */ +.squareAspectRatio { + padding-bottom: 100%; +} + +.tip { + @apply --marketplaceDefaultFontStyles; + color: var(--matterColorAnti); + margin-top: 0; + margin-bottom: 13px; + + @media (--viewportMedium) { + margin-top: 0; + margin-bottom: 10px; + } + +} + +.fileInfo { + @apply --marketplaceH4FontStyles; + color: var(--matterColorAnti); + margin-top: 0; + margin-bottom: 0; + + @media (--viewportMedium) { + margin-top: 0; + margin-bottom: 0; + } +} + +.nameContainer { + display: flex; + justify-content: space-between; + margin-top: 14px; + + @media (--viewportMedium) { + margin-top: 26px; + } +} + +.firstName { + width: calc(34% - 9px); +} + +.lastName { + width: calc(66% - 9px); +} + +.submitButton { + margin-top: 24px; +} diff --git a/src/containers/ProfileSettingsForm/ProfileSettingsForm.js b/src/containers/ProfileSettingsForm/ProfileSettingsForm.js new file mode 100644 index 00000000..a1fa4f37 --- /dev/null +++ b/src/containers/ProfileSettingsForm/ProfileSettingsForm.js @@ -0,0 +1,225 @@ +import React, { PropTypes } from 'react'; +import { compose } from 'redux'; +import { FormattedMessage, injectIntl, intlShape } from 'react-intl'; +import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form'; +import classNames from 'classnames'; +import { ensureCurrentUser } from '../../util/data'; +import * as validators from '../../util/validators'; +import { Avatar, Button, ImageFromFile, SpinnerIcon, TextInputField } from '../../components'; + +import css from './ProfileSettingsForm.css'; + +const ACCEPT_IMAGES = 'image/*'; + +const RenderAvatar = props => { + const { accept, id, input, label, type, disabled, uploadImageError } = props; + const { name, onChange } = input; + const error = uploadImageError + ?
+ +
+ : null; + + return ( +
+ + { + const file = event.target.files[0]; + const tempId = `${file.name}_${Date.now()}`; + onChange({ id: tempId, file }); + }} + type={type} + /> + {error} +
+ ); +}; + +RenderAvatar.defaultProps = { uploadImageError: null }; +const { bool, func, instanceOf, node, object, shape, string } = PropTypes; + +RenderAvatar.propTypes = { + accept: string.isRequired, + disabled: bool.isRequired, + id: string.isRequired, + input: shape({ + value: object, + onChange: func.isRequired, + name: string.isRequired, + }).isRequired, + label: node.isRequired, + type: string.isRequired, + uploadImageError: instanceOf(Error), +}; + +const ProfileSettingsFormComponent = props => { + const { + className, + currentUser, + form, + handleSubmit, + intl, + invalid, + onImageUpload, + profileImage, + rootClassName, + submitting, + updateInProgress, + updateProfileError, + uploadImageError, + uploadInProgress, + } = props; + + const user = ensureCurrentUser(currentUser); + + // First name + const firstNameLabel = intl.formatMessage({ + id: 'ProfileSettingsForm.firstNameLabel', + }); + const firstNamePlaceholder = intl.formatMessage({ + id: 'ProfileSettingsForm.firstNamePlaceholder', + }); + const firstNameRequiredMessage = intl.formatMessage({ + id: 'ProfileSettingsForm.firstNameRequired', + }); + const firstNameRequired = validators.required(firstNameRequiredMessage); + + // Last name + const lastNameLabel = intl.formatMessage({ + id: 'ProfileSettingsForm.lastNameLabel', + }); + const lastNamePlaceholder = intl.formatMessage({ + id: 'ProfileSettingsForm.lastNamePlaceholder', + }); + const lastNameRequiredMessage = intl.formatMessage({ + id: 'ProfileSettingsForm.lastNameRequired', + }); + const lastNameRequired = validators.required(lastNameRequiredMessage); + + const uploadingOverlay = uploadInProgress + ?
+ : null; + + const hasUploadError = !!uploadImageError && !uploadInProgress; + const errorClasses = classNames({ [css.avatarUploadError]: hasUploadError }); + const avatarImage = uploadInProgress && profileImage.file + ? + {uploadingOverlay} + + : ; + + const chooseAvatarLabel = profileImage.imageId || (uploadInProgress && profileImage.file) + ?
+ {avatarImage} +
+ +
+
+ :
+
+ +
+
+ +
+
; + + const submitError = updateProfileError + ?
+ +
+ : null; + + const classes = classNames(rootClassName || css.root, className); + const submitDisabled = invalid || submitting || uploadInProgress || updateInProgress; + + return ( +
+
+

+ +

+ +
+
+
+
+

+ +

+
+ + +
+
+ {submitError} + +
+ ); +}; + +ProfileSettingsFormComponent.defaultProps = { + rootClassName: null, + className: null, + uploadImageError: null, + updateProfileError: null, +}; + +ProfileSettingsFormComponent.propTypes = { + ...formPropTypes, + rootClassName: string, + className: string, + uploadImageError: instanceOf(Error), + uploadInProgress: bool.isRequired, + updateInProgress: bool.isRequired, + updateProfileError: instanceOf(Error), + intl: intlShape.isRequired, +}; + +const defaultFormName = 'ProfileSettingsForm'; + +const ProfileSettingsForm = compose(reduxForm({ form: defaultFormName }), injectIntl)( + ProfileSettingsFormComponent +); + +export default ProfileSettingsForm; diff --git a/src/containers/index.js b/src/containers/index.js index 57504d88..7bef4382 100644 --- a/src/containers/index.js +++ b/src/containers/index.js @@ -25,6 +25,7 @@ import PasswordForgottenPage from './PasswordForgottenPage/PasswordForgottenPage import PayoutDetailsForm from './PayoutDetailsForm/PayoutDetailsForm'; import PayoutPreferencesPage from './PayoutPreferencesPage/PayoutPreferencesPage'; import ProfilePage from './ProfilePage/ProfilePage'; +import ProfileSettingsForm from './ProfileSettingsForm/ProfileSettingsForm'; import ProfileSettingsPage from './ProfileSettingsPage/ProfileSettingsPage'; import SalePage from './SalePage/SalePage'; import SearchPage from './SearchPage/SearchPage'; @@ -62,6 +63,7 @@ export { PayoutDetailsForm, PayoutPreferencesPage, ProfilePage, + ProfileSettingsForm, ProfileSettingsPage, SalePage, SearchPage, diff --git a/src/translations/en.json b/src/translations/en.json index 1d2430bb..c0ff7c4f 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -251,6 +251,22 @@ "PayoutDetailsForm.streetAddressRequired": "This field is required", "PayoutDetailsForm.submitButtonText": "Save details & publish listing", "PayoutDetailsForm.title": "One more thing: payout preferences", + "ProfileSettingsForm.addYourProfilePicture": "+ Add your profile picture…", + "ProfileSettingsForm.addYourProfilePictureMobile": "+ Add", + "ProfileSettingsForm.changeAvatar": "Change", + "ProfileSettingsForm.fileInfo": ".JPG, .GIF or .PNG max. 10 MB", + "ProfileSettingsForm.firstNameLabel": "First name", + "ProfileSettingsForm.firstNamePlaceholder": "John", + "ProfileSettingsForm.firstNameRequired": "This field is required", + "ProfileSettingsForm.imageUploadFailed": "Whoopsie, something went wrong - please try again.", + "ProfileSettingsForm.lastNameLabel": "Last name", + "ProfileSettingsForm.lastNamePlaceholder": "Doe", + "ProfileSettingsForm.lastNameRequired": "This field is required", + "ProfileSettingsForm.saveChanges": "Save changes", + "ProfileSettingsForm.tip": "Tip: Choose an image where your face is recognizable.", + "ProfileSettingsForm.updateProfileFailed": "Whoopsie, something went wrong - please try again.", + "ProfileSettingsForm.yourName": "Your name", + "ProfileSettingsForm.yourProfilePicture": "Your profile picture", "ResponsiveImage.noImage": "No image", "SaleDetailsPanel.bookingBreakdownTitle": "Booking breakdown", "SaleDetailsPanel.listingAcceptedTitle": "Woohoo! You accepted a request from {customerName} for {listingLink}",