flex-template-web/src/components/EditListingWizard/EditListingWizard.js
2019-01-07 14:04:01 +02:00

345 lines
10 KiB
JavaScript

import React, { Component } from 'react';
import { array, bool, func, number, object, oneOf, shape, string } from 'prop-types';
import { compose } from 'redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import classNames from 'classnames';
import { withViewport } from '../../util/contextHelpers';
import {
LISTING_PAGE_PARAM_TYPE_DRAFT,
LISTING_PAGE_PARAM_TYPE_NEW,
LISTING_PAGE_PARAM_TYPES,
} from '../../util/urlHelpers';
import { ensureListing } from '../../util/data';
import { PayoutDetailsForm } from '../../forms';
import { Modal, NamedRedirect, Tabs } from '../../components';
import EditListingWizardTab, {
AVAILABILITY,
DESCRIPTION,
FEATURES,
POLICY,
LOCATION,
PRICING,
PHOTOS,
} from './EditListingWizardTab';
import css from './EditListingWizard.css';
// TODO: PHOTOS panel needs to be the last one since it currently contains PayoutDetailsForm modal
// All the other panels can be reordered.
export const TABS = [DESCRIPTION, FEATURES, POLICY, LOCATION, PRICING, AVAILABILITY, PHOTOS];
// Tabs are horizontal in small screens
const MAX_HORIZONTAL_NAV_SCREEN_WIDTH = 1023;
const tabLabel = (intl, tab) => {
let key = null;
if (tab === DESCRIPTION) {
key = 'EditListingWizard.tabLabelDescription';
} else if (tab === FEATURES) {
key = 'EditListingWizard.tabLabelFeatures';
} else if (tab === POLICY) {
key = 'EditListingWizard.tabLabelPolicy';
} else if (tab === LOCATION) {
key = 'EditListingWizard.tabLabelLocation';
} else if (tab === PRICING) {
key = 'EditListingWizard.tabLabelPricing';
} else if (tab === AVAILABILITY) {
key = 'EditListingWizard.tabLabelAvailability';
} else if (tab === PHOTOS) {
key = 'EditListingWizard.tabLabelPhotos';
}
return intl.formatMessage({ id: key });
};
/**
* Check if a wizard tab is completed.
*
* @param tab wizard's tab
* @param listing is contains some specific data if tab is completed
*
* @return true if tab / step is completed.
*/
const tabCompleted = (tab, listing) => {
const {
availabilityPlan,
description,
geolocation,
price,
title,
publicData,
} = listing.attributes;
const images = listing.images;
switch (tab) {
case DESCRIPTION:
return !!(description && title);
case FEATURES:
return !!(publicData && publicData.amenities);
case POLICY:
return !!(publicData && typeof publicData.rules !== 'undefined');
case LOCATION:
return !!(geolocation && publicData && publicData.location && publicData.location.address);
case PRICING:
return !!price;
case AVAILABILITY:
return !!availabilityPlan;
case PHOTOS:
return images && images.length > 0;
default:
return false;
}
};
/**
* Check which wizard tabs are active and which are not yet available. Tab is active if previous
* tab is completed. In edit mode all tabs are active.
*
* @param isNew flag if a new listing is being created or an old one being edited
* @param listing data to be checked
*
* @return object containing activity / editability of different tabs of this wizard
*/
const tabsActive = (isNew, listing) => {
return TABS.reduce((acc, tab) => {
const previousTabIndex = TABS.findIndex(t => t === tab) - 1;
const isActive =
previousTabIndex >= 0 ? !isNew || tabCompleted(TABS[previousTabIndex], listing) : true;
return { ...acc, [tab]: isActive };
}, {});
};
const scrollToTab = (tabPrefix, tabId) => {
const el = document.querySelector(`#${tabPrefix}_${tabId}`);
if (el) {
el.scrollIntoView({
block: 'start',
behavior: 'smooth',
});
}
};
// Create a new or edit listing through EditListingWizard
class EditListingWizard extends Component {
constructor(props) {
super(props);
// Having this info in state would trigger unnecessary rerendering
this.hasScrolledToTab = false;
this.state = {
draftId: null,
showPayoutDetails: false,
};
this.handleCreateFlowTabScrolling = this.handleCreateFlowTabScrolling.bind(this);
this.handlePublishListing = this.handlePublishListing.bind(this);
this.handlePayoutModalClose = this.handlePayoutModalClose.bind(this);
this.handlePayoutSubmit = this.handlePayoutSubmit.bind(this);
}
handleCreateFlowTabScrolling(shouldScroll) {
this.hasScrolledToTab = shouldScroll;
}
handlePublishListing(id) {
const { onPublishListingDraft, currentUser } = this.props;
const stripeConnected =
currentUser && currentUser.attributes && currentUser.attributes.stripeConnected;
if (stripeConnected) {
onPublishListingDraft(id);
} else {
this.setState({
draftId: id,
showPayoutDetails: true,
});
}
}
handlePayoutModalClose() {
this.setState({ showPayoutDetails: false });
}
handlePayoutSubmit(values) {
const { fname: firstName, lname: lastName, ...rest } = values;
this.props
.onPayoutDetailsSubmit({ firstName, lastName, ...rest })
.then(() => {
this.setState({ showPayoutDetails: false });
this.props.onManageDisableScrolling('EditListingWizard.payoutModal', false);
this.props.onPublishListingDraft(this.state.draftId);
})
.catch(() => {
// do nothing
});
}
render() {
const {
id,
className,
rootClassName,
params,
listing,
viewport,
intl,
errors,
fetchInProgress,
onManageDisableScrolling,
onPayoutDetailsFormChange,
...rest
} = this.props;
const selectedTab = params.tab;
const isNewListingFlow = [LISTING_PAGE_PARAM_TYPE_NEW, LISTING_PAGE_PARAM_TYPE_DRAFT].includes(
params.type
);
const rootClasses = rootClassName || css.root;
const classes = classNames(rootClasses, className);
const currentListing = ensureListing(listing);
const tabsStatus = tabsActive(isNewListingFlow, currentListing);
// If selectedTab is not active, redirect to the beginning of wizard
if (!tabsStatus[selectedTab]) {
const currentTabIndex = TABS.indexOf(selectedTab);
const nearestActiveTab = TABS.slice(0, currentTabIndex)
.reverse()
.find(t => tabsStatus[t]);
return <NamedRedirect name="EditListingPage" params={{ ...params, tab: nearestActiveTab }} />;
}
const { width } = viewport;
const hasViewport = width > 0;
const hasHorizontalTabLayout = hasViewport && width <= MAX_HORIZONTAL_NAV_SCREEN_WIDTH;
const hasVerticalTabLayout = hasViewport && width > MAX_HORIZONTAL_NAV_SCREEN_WIDTH;
const hasFontsLoaded =
hasViewport && document.documentElement.classList.contains('fontsLoaded');
// Check if scrollToTab call is needed (tab is not visible on mobile)
if (hasVerticalTabLayout) {
this.hasScrolledToTab = true;
} else if (hasHorizontalTabLayout && !this.hasScrolledToTab && hasFontsLoaded) {
const tabPrefix = id;
scrollToTab(tabPrefix, selectedTab);
this.hasScrolledToTab = true;
}
const tabLink = tab => {
return { name: 'EditListingPage', params: { ...params, tab } };
};
return (
<div className={classes}>
<Tabs
rootClassName={css.tabsContainer}
navRootClassName={css.nav}
tabRootClassName={css.tab}
>
{TABS.map(tab => {
return (
<EditListingWizardTab
{...rest}
key={tab}
tabId={`${id}_${tab}`}
tabLabel={tabLabel(intl, tab)}
tabLinkProps={tabLink(tab)}
selected={selectedTab === tab}
disabled={isNewListingFlow && !tabsStatus[tab]}
tab={tab}
intl={intl}
params={params}
listing={listing}
marketplaceTabs={TABS}
errors={errors}
handleCreateFlowTabScrolling={this.handleCreateFlowTabScrolling}
handlePublishListing={this.handlePublishListing}
fetchInProgress={fetchInProgress}
/>
);
})}
</Tabs>
<Modal
id="EditListingWizard.payoutModal"
isOpen={this.state.showPayoutDetails}
onClose={this.handlePayoutModalClose}
onManageDisableScrolling={onManageDisableScrolling}
>
<div className={css.modalPayoutDetailsWrapper}>
<h1 className={css.modalTitle}>
<FormattedMessage id="EditListingPhotosPanel.payoutModalTitleOneMoreThing" />
<br />
<FormattedMessage id="EditListingPhotosPanel.payoutModalTitlePayoutPreferences" />
</h1>
<p className={css.modalMessage}>
<FormattedMessage id="EditListingPhotosPanel.payoutModalInfo" />
</p>
<PayoutDetailsForm
className={css.payoutDetails}
inProgress={fetchInProgress}
createStripeAccountError={errors ? errors.createStripeAccountError : null}
onChange={onPayoutDetailsFormChange}
onSubmit={this.handlePayoutSubmit}
/>
</div>
</Modal>
</div>
);
}
}
EditListingWizard.defaultProps = {
className: null,
rootClassName: null,
listing: null,
};
EditListingWizard.propTypes = {
id: string.isRequired,
className: string,
rootClassName: string,
params: shape({
id: string.isRequired,
slug: string.isRequired,
type: oneOf(LISTING_PAGE_PARAM_TYPES).isRequired,
tab: oneOf(TABS).isRequired,
}).isRequired,
// We cannot use propTypes.listing since the listing might be a draft.
listing: shape({
attributes: shape({
publicData: object,
description: string,
geolocation: object,
pricing: object,
title: string,
}),
images: array,
}),
errors: shape({
createListingDraftError: object,
updateListingError: object,
publishListingError: object,
showListingsError: object,
uploadImageError: object,
createStripeAccountError: object,
}).isRequired,
fetchInProgress: bool.isRequired,
onPayoutDetailsFormChange: func.isRequired,
onPayoutDetailsSubmit: func.isRequired,
onManageDisableScrolling: func.isRequired,
// from withViewport
viewport: shape({
width: number.isRequired,
height: number.isRequired,
}).isRequired,
// from injectIntl
intl: intlShape.isRequired,
};
export default compose(
withViewport,
injectIntl
)(EditListingWizard);