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 (
+
+ );
+};
+
+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",