diff --git a/src/components/EditListingPoliciesPanel/EditListingPoliciesPanel.css b/src/components/EditListingPoliciesPanel/EditListingPoliciesPanel.css new file mode 100644 index 00000000..580c779a --- /dev/null +++ b/src/components/EditListingPoliciesPanel/EditListingPoliciesPanel.css @@ -0,0 +1,22 @@ +@import '../../marketplace.css'; + +.root { + flex-grow: 1; + width: 100%; + height: auto; + display: flex; + flex-direction: column; + padding: 11px 24px 0 24px; +} + +.form { + flex-grow: 1; +} + +.title { + margin-bottom: 19px; + + @media (--viewportLarge) { + margin-bottom: 44px; + } +} diff --git a/src/components/EditListingPoliciesPanel/EditListingPoliciesPanel.js b/src/components/EditListingPoliciesPanel/EditListingPoliciesPanel.js new file mode 100644 index 00000000..15839f6e --- /dev/null +++ b/src/components/EditListingPoliciesPanel/EditListingPoliciesPanel.js @@ -0,0 +1,86 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { FormattedMessage } from 'react-intl'; +import { ensureListing } from '../../util/data'; +import { createSlug } from '../../util/urlHelpers'; +import { NamedLink } from '../../components'; +import { EditListingPoliciesForm } from '../../containers'; + +import css from './EditListingPoliciesPanel.css'; + +const EditListingPoliciesPanel = props => { + const { + className, + rootClassName, + listing, + onSubmit, + onChange, + submitButtonText, + panelUpdated, + updateInProgress, + errors, + } = props; + + const classes = classNames(rootClassName || css.root, className); + const currentListing = ensureListing(listing); + const { title, publicData } = currentListing.attributes; + const listingTitle = title || ''; + const listingLink = currentListing.id ? ( + + {listingTitle} + + ) : ( + '' + ); + + const panelTitle = currentListing.id ? ( + + ) : ( + + ); + + const saunaRules = publicData && publicData.saunaRules ? publicData.saunaRules : ''; + const initialSearchFormValues = { saunaRules }; + + return ( +
+

{panelTitle}

+ +
+ ); +}; + +const { func, object, string, bool } = PropTypes; + +EditListingPoliciesPanel.defaultProps = { + className: null, + rootClassName: null, + listing: null, +}; + +EditListingPoliciesPanel.propTypes = { + className: string, + rootClassName: string, + + // We cannot use propTypes.listing since the listing might be a draft. + listing: object, + + onSubmit: func.isRequired, + onChange: func.isRequired, + submitButtonText: string.isRequired, + panelUpdated: bool.isRequired, + updateInProgress: bool.isRequired, + errors: object.isRequired, +}; + +export default EditListingPoliciesPanel; diff --git a/src/components/EditListingWizard/EditListingWizard.css b/src/components/EditListingWizard/EditListingWizard.css index 6a00506d..a420c71d 100644 --- a/src/components/EditListingWizard/EditListingWizard.css +++ b/src/components/EditListingWizard/EditListingWizard.css @@ -42,6 +42,7 @@ .tab { margin-left: 16px; + white-space: nowrap; &:first-child { margin-left: 0; diff --git a/src/components/EditListingWizard/EditListingWizard.js b/src/components/EditListingWizard/EditListingWizard.js index f1aa91bc..08d895b4 100644 --- a/src/components/EditListingWizard/EditListingWizard.js +++ b/src/components/EditListingWizard/EditListingWizard.js @@ -12,6 +12,7 @@ import { EditListingDescriptionPanel, EditListingLocationPanel, EditListingPhotosPanel, + EditListingPoliciesPanel, EditListingPricingPanel, NamedRedirect, Tabs, @@ -20,10 +21,11 @@ import { import css from './EditListingWizard.css'; const DESCRIPTION = 'description'; +const POLICY = 'policy'; const LOCATION = 'location'; const PRICING = 'pricing'; const PHOTOS = 'photos'; -const STEPS = [DESCRIPTION, LOCATION, PRICING, PHOTOS]; +const STEPS = [DESCRIPTION, POLICY, LOCATION, PRICING, PHOTOS]; // Tabs are horizontal in small screens const MAX_HORIZONTAL_NAV_SCREEN_WIDTH = 1023; @@ -32,6 +34,8 @@ const submitText = (intl, isNew, step) => { let key = null; if (step === DESCRIPTION) { key = isNew ? 'EditListingWizard.saveNewDescription' : 'EditListingWizard.saveEditDescription'; + } else if (step === POLICY) { + key = isNew ? 'EditListingWizard.saveNewPolicies' : 'EditListingWizard.saveEditPolicies'; } else if (step === LOCATION) { key = isNew ? 'EditListingWizard.saveNewLocation' : 'EditListingWizard.saveEditLocation'; } else if (step === PRICING) { @@ -42,6 +46,22 @@ const submitText = (intl, isNew, step) => { return intl.formatMessage({ id: key }); }; +const tabLabel = (intl, step) => { + let key = null; + if (step === DESCRIPTION) { + key = 'EditListingWizard.tabLabelDescription'; + } else if (step === POLICY) { + key = 'EditListingWizard.tabLabelPolicy'; + } else if (step === LOCATION) { + key = 'EditListingWizard.tabLabelLocation'; + } else if (step === PRICING) { + key = 'EditListingWizard.tabLabelPricing'; + } else if (step === PHOTOS) { + key = 'EditListingWizard.tabLabelPhotos'; + } + return intl.formatMessage({ id: key }); +}; + /** * Check which wizard steps are active and which are not yet available. Step is active is previous * step is completed. @@ -53,6 +73,7 @@ const submitText = (intl, isNew, step) => { const stepsActive = listing => { const { description, geolocation, price, title, publicData } = listing.attributes; const descriptionStep = !!(title && description); + const policyStep = !!(publicData && typeof publicData.saunaRules !== 'undefined'); const locationStep = !!( geolocation && publicData && @@ -65,7 +86,8 @@ const stepsActive = listing => { return { [DESCRIPTION]: true, - [LOCATION]: descriptionStep, + [POLICY]: descriptionStep, + [LOCATION]: policyStep, [PRICING]: locationStep, [PHOTOS]: pricingStep, }; @@ -160,21 +182,27 @@ class EditListingWizard extends Component { this.hasScrolledToTab = true; } + const panelProps = tab => { + return { + className: css.panel, + disabled: !stepsStatus[tab], + errors, + listing, + onChange, + panelUpdated: updatedTab === tab, + selected: selectedTab === tab, + submitButtonText: submitText(intl, isNew, tab), + tabId: `${id}_${tab}`, + tabLabel: tabLabel(intl, tab), + tabLinkProps: tabLink(tab), + updateInProgress, + }; + }; + return ( { const { title, description, category } = values; const updateValues = { @@ -186,7 +214,7 @@ class EditListingWizard extends Component { if (isNew) { onUpsertListingDraft(updateValues); - const pathParams = tabParams(LOCATION); + const pathParams = tabParams(POLICY); // Redirect to location tab history.push( createResourceLocatorString('EditListingPage', routeConfiguration(), pathParams, {}) @@ -196,19 +224,30 @@ class EditListingWizard extends Component { } }} /> + { + const { saunaRules = '' } = values; + const updateValues = { + publicData: { + saunaRules, + }, + }; + + if (isNew) { + onUpdateListingDraft(updateValues); + const pathParams = tabParams(LOCATION); + // Redirect to pricing tab + history.push( + createResourceLocatorString('EditListingPage', routeConfiguration(), pathParams, {}) + ); + } else { + update(POLICY, updateValues); + } + }} + /> { const { building = '', location } = values; const { selectedPlace: { address, origin } } = location; @@ -220,7 +259,6 @@ class EditListingWizard extends Component { }; if (isNew) { - // TODO When API supports building number, etc. change this to use those fields instead. onUpdateListingDraft(updateValues); const pathParams = tabParams(PRICING); // Redirect to pricing tab @@ -233,18 +271,7 @@ class EditListingWizard extends Component { }} /> { if (isNew) { onUpdateListingDraft(values); @@ -259,25 +286,14 @@ class EditListingWizard extends Component { }} /> { const { country, images: updatedImages } = values; const updateValues = { ...listing.attributes, country, images: updatedImages }; diff --git a/src/components/index.js b/src/components/index.js index 9a7dea88..9fa3a22e 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -20,6 +20,9 @@ export { default as EditListingLocationPanel, } from './EditListingLocationPanel/EditListingLocationPanel'; export { default as EditListingPhotosPanel } from './EditListingPhotosPanel/EditListingPhotosPanel'; +export { + default as EditListingPoliciesPanel, +} from './EditListingPoliciesPanel/EditListingPoliciesPanel'; export { default as EditListingPricingPanel, } from './EditListingPricingPanel/EditListingPricingPanel'; diff --git a/src/containers/EditListingPoliciesForm/EditListingPoliciesForm.css b/src/containers/EditListingPoliciesForm/EditListingPoliciesForm.css new file mode 100644 index 00000000..5ea21027 --- /dev/null +++ b/src/containers/EditListingPoliciesForm/EditListingPoliciesForm.css @@ -0,0 +1,53 @@ +@import '../../marketplace.css'; + +:root { + --formMargins: { + margin-bottom: 24px; + + @media (--viewportMedium) { + margin-bottom: 32px; + } + } +} + +.root { + /* Dimensions */ + width: 100%; + height: auto; + + /* Layout */ + display: flex; + flex: 1; + flex-direction: column; + + padding-top: 1px; + + @media (--viewportMedium) { + padding-top: 2px; + } +} + +.title { + @apply --formMargins; +} + +.policy { + @apply --formMargins; + flex-shrink: 0; +} + +.error { + color: var(--failColor); +} + +.submitButton { + margin-top: auto; + margin-bottom: 24px; + flex-shrink: 0; + + @media (--viewportLarge) { + display: inline-block; + width: 241px; + margin-top: 100px; + } +} diff --git a/src/containers/EditListingPoliciesForm/EditListingPoliciesForm.example.js b/src/containers/EditListingPoliciesForm/EditListingPoliciesForm.example.js new file mode 100644 index 00000000..87e1ef42 --- /dev/null +++ b/src/containers/EditListingPoliciesForm/EditListingPoliciesForm.example.js @@ -0,0 +1,15 @@ +/* eslint-disable no-console */ +import EditListingPoliciesForm from './EditListingPoliciesForm'; + +export const Empty = { + component: EditListingPoliciesForm, + props: { + onSubmit: values => { + console.log('Submit EditListingPoliciesForm with (unformatted) values:', values); + }, + saveActionMsg: 'Save rules', + updated: false, + updateInProgress: false, + }, + group: 'forms', +}; diff --git a/src/containers/EditListingPoliciesForm/EditListingPoliciesForm.js b/src/containers/EditListingPoliciesForm/EditListingPoliciesForm.js new file mode 100644 index 00000000..473f221f --- /dev/null +++ b/src/containers/EditListingPoliciesForm/EditListingPoliciesForm.js @@ -0,0 +1,89 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { compose } from 'redux'; +import { reduxForm, propTypes as formPropTypes } from 'redux-form'; +import { intlShape, injectIntl, FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; +import { propTypes } from '../../util/types'; +import { Form, Button, TextInputField } from '../../components'; + +import css from './EditListingPoliciesForm.css'; + +export const EditListingPoliciesFormComponent = props => { + const { + className, + disabled, + handleSubmit, + intl, + form, + invalid, + saveActionMsg, + submitting, + updated, + updateError, + updateInProgress, + } = props; + + const rulesLabelMessage = intl.formatMessage({ id: 'EditListingPoliciesForm.rulesLabel' }); + const saunaRulesPlaceholderMessage = intl.formatMessage({ + id: 'EditListingPoliciesForm.rulesPlaceholder', + }); + + const errorMessage = updateError ? ( +

+ +

+ ) : null; + + const classes = classNames(css.root, className); + const submitReady = updated; + const submitInProgress = submitting || updateInProgress; + const submitDisabled = invalid || disabled || submitInProgress; + + return ( +
+ {errorMessage} + + + + + + ); +}; + +EditListingPoliciesFormComponent.defaultProps = { + selectedPlace: null, + updateError: null, +}; + +const { func, string, bool } = PropTypes; + +EditListingPoliciesFormComponent.propTypes = { + ...formPropTypes, + intl: intlShape.isRequired, + onSubmit: func.isRequired, + saveActionMsg: string.isRequired, + selectedPlace: propTypes.place, + updated: bool.isRequired, + updateError: propTypes.error, + updateInProgress: bool.isRequired, +}; + +const formName = 'EditListingPoliciesForm'; + +export default compose(reduxForm({ form: formName }), injectIntl)(EditListingPoliciesFormComponent); diff --git a/src/containers/EditListingPoliciesForm/EditListingPoliciesForm.test.js b/src/containers/EditListingPoliciesForm/EditListingPoliciesForm.test.js new file mode 100644 index 00000000..5f1e06b4 --- /dev/null +++ b/src/containers/EditListingPoliciesForm/EditListingPoliciesForm.test.js @@ -0,0 +1,24 @@ +// NOTE: renderdeep doesn't work due to Google Maps API integration +import React from 'react'; +import { renderShallow } from '../../util/test-helpers'; +import { fakeIntl, fakeFormProps } from '../../util/test-data'; +import { EditListingPoliciesFormComponent } from './EditListingPoliciesForm'; + +const noop = () => null; + +describe('EditListingPoliciesForm', () => { + it('matches snapshot', () => { + const tree = renderShallow( + v} + saveActionMsg="Save rules" + updated={false} + updateInProgress={false} + /> + ); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/containers/EditListingPoliciesForm/__snapshots__/EditListingPoliciesForm.test.js.snap b/src/containers/EditListingPoliciesForm/__snapshots__/EditListingPoliciesForm.test.js.snap new file mode 100644 index 00000000..5e43e422 --- /dev/null +++ b/src/containers/EditListingPoliciesForm/__snapshots__/EditListingPoliciesForm.test.js.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EditListingPoliciesForm matches snapshot 1`] = ` +
+ + + +`; diff --git a/src/containers/index.js b/src/containers/index.js index 4d727114..c3cdfcaf 100644 --- a/src/containers/index.js +++ b/src/containers/index.js @@ -12,6 +12,9 @@ export { } from './EditListingLocationForm/EditListingLocationForm'; export { default as EditListingPage } from './EditListingPage/EditListingPage'; export { default as EditListingPhotosForm } from './EditListingPhotosForm/EditListingPhotosForm'; +export { + default as EditListingPoliciesForm, +} from './EditListingPoliciesForm/EditListingPoliciesForm'; export { default as EditListingPricingForm } from './EditListingPricingForm/EditListingPricingForm'; export { default as EmailVerificationForm } from './EmailVerificationForm/EmailVerificationForm'; export { default as EmailVerificationPage } from './EmailVerificationPage/EmailVerificationPage'; diff --git a/src/examples.js b/src/examples.js index 134fa4d5..baaf535c 100644 --- a/src/examples.js +++ b/src/examples.js @@ -58,6 +58,7 @@ import * as Colors from './containers/StyleguidePage/Colors.example'; import * as EditListingDescriptionForm from './containers/EditListingDescriptionForm/EditListingDescriptionForm.example'; import * as EditListingLocationForm from './containers/EditListingLocationForm/EditListingLocationForm.example'; import * as EditListingPhotosForm from './containers/EditListingPhotosForm/EditListingPhotosForm.example'; +import * as EditListingPoliciesForm from './containers/EditListingPoliciesForm/EditListingPoliciesForm.example'; import * as EditListingPricingForm from './containers/EditListingPricingForm/EditListingPricingForm.example'; import * as EmailVerificationForm from './containers/EmailVerificationForm/EmailVerificationForm.example'; import * as EnquiryForm from './containers/EnquiryForm/EnquiryForm.example'; @@ -85,6 +86,7 @@ export { EditListingDescriptionForm, EditListingLocationForm, EditListingPhotosForm, + EditListingPoliciesForm, EditListingPricingForm, EmailVerificationForm, EnquiryForm, diff --git a/src/translations/en.json b/src/translations/en.json index 64a31c56..6cceb854 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -147,6 +147,11 @@ "EditListingPhotosPanel.payoutModalTitleOneMoreThing": "One more thing:", "EditListingPhotosPanel.payoutModalTitlePayoutPreferences": "Payout preferences", "EditListingPhotosPanel.title": "Edit the photos of {listingTitle}", + "EditListingPoliciesForm.rulesLabel": "Sauna rules", + "EditListingPoliciesForm.rulesPlaceholder": "Describe the no-nos", + "EditListingPoliciesForm.updateFailed": "Failed to update listing. Please try again.", + "EditListingPoliciesPanel.createListingTitle": "Time to set some ground rules for the bathers.", + "EditListingPoliciesPanel.title": "Edit the rules of {listingTitle}", "EditListingPricingForm.priceInputPlaceholder": "Choose your price…", "EditListingPricingForm.pricePerUnit": "Price per night in euros", "EditListingPricingForm.priceRequired": "You need to add a valid price.", @@ -157,14 +162,17 @@ "EditListingWizard.saveEditDescription": "Save description", "EditListingWizard.saveEditLocation": "Save location", "EditListingWizard.saveEditPhotos": "Save photos", + "EditListingWizard.saveEditPolicies": "Save rules", "EditListingWizard.saveEditPricing": "Save pricing", - "EditListingWizard.saveNewDescription": "Next: Location", + "EditListingWizard.saveNewDescription": "Next: Policies", "EditListingWizard.saveNewLocation": "Next: Pricing", "EditListingWizard.saveNewPhotos": "Publish listing", + "EditListingWizard.saveNewPolicies": "Next: location", "EditListingWizard.saveNewPricing": "Next: Photos", "EditListingWizard.tabLabelDescription": "Description", "EditListingWizard.tabLabelLocation": "Location", "EditListingWizard.tabLabelPhotos": "Photos", + "EditListingWizard.tabLabelPolicy": "Sauna rules", "EditListingWizard.tabLabelPricing": "Pricing", "EmailVerificationForm.finishAccountSetup": "We will need to send you email notifications. To allow this, please verify your email address {email}.", "EmailVerificationForm.successButtonText": "Continue exploring",