mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-30 18:16:48 +10:00
763 lines
26 KiB
JavaScript
763 lines
26 KiB
JavaScript
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
|
import React, { Component } from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
|
|
import { compose } from 'redux';
|
|
import { connect } from 'react-redux';
|
|
import { withRouter } from 'react-router-dom';
|
|
import classNames from 'classnames';
|
|
import config from '../../config';
|
|
import routeConfiguration from '../../routeConfiguration';
|
|
import { LISTING_STATE_PENDING_APPROVAL, LISTING_STATE_CLOSED, propTypes } from '../../util/types';
|
|
import { types as sdkTypes } from '../../util/sdkLoader';
|
|
import { createSlug } from '../../util/urlHelpers';
|
|
import { formatMoney } from '../../util/currency';
|
|
import { createResourceLocatorString, findRouteByRouteName } from '../../util/routes';
|
|
import { ensureListing, ensureUser, userDisplayName } from '../../util/data';
|
|
import { getListingsById } from '../../ducks/marketplaceData.duck';
|
|
import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck';
|
|
import {
|
|
AvatarLarge,
|
|
AvatarMedium,
|
|
Button,
|
|
ModalInMobile,
|
|
Page,
|
|
ResponsiveImage,
|
|
NamedLink,
|
|
NamedRedirect,
|
|
Modal,
|
|
ImageCarousel,
|
|
InlineTextButton,
|
|
LayoutSingleColumn,
|
|
LayoutWrapperTopbar,
|
|
LayoutWrapperMain,
|
|
LayoutWrapperFooter,
|
|
Footer,
|
|
UserCard,
|
|
Reviews,
|
|
} from '../../components';
|
|
import { BookingDatesForm, TopbarContainer, EnquiryForm } from '../../containers';
|
|
|
|
import { sendEnquiry, loadData, setInitialValues } from './ListingPage.duck';
|
|
import EditIcon from './EditIcon';
|
|
import SectionRulesMaybe from './SectionRulesMaybe';
|
|
import SectionMapMaybe from './SectionMapMaybe';
|
|
import css from './ListingPage.css';
|
|
|
|
// This defines when ModalInMobile shows content as Modal
|
|
const MODAL_BREAKPOINT = 1023;
|
|
|
|
const { UUID } = sdkTypes;
|
|
|
|
const priceData = (price, intl) => {
|
|
if (price && price.currency === config.currency) {
|
|
const formattedPrice = formatMoney(intl, price);
|
|
return { formattedPrice, priceTitle: formattedPrice };
|
|
} else if (price) {
|
|
return {
|
|
formattedPrice: `(${price.currency})`,
|
|
priceTitle: `Unsupported currency (${price.currency})`,
|
|
};
|
|
}
|
|
return {};
|
|
};
|
|
|
|
export const ActionBarMaybe = props => {
|
|
const { isOwnListing, listing, editParams } = props;
|
|
const state = listing.attributes.state;
|
|
const isPendingApproval = state === LISTING_STATE_PENDING_APPROVAL;
|
|
const isClosed = state === LISTING_STATE_CLOSED;
|
|
|
|
if (isOwnListing) {
|
|
let ownListingTextTranslationId = 'ListingPage.ownListing';
|
|
|
|
if (isPendingApproval) {
|
|
ownListingTextTranslationId = 'ListingPage.ownListingPendingApproval';
|
|
} else if (isClosed) {
|
|
ownListingTextTranslationId = 'ListingPage.ownClosedListing';
|
|
}
|
|
|
|
const ownListingTextClasses = classNames(css.ownListingText, {
|
|
[css.ownListingTextPendingApproval]: isPendingApproval,
|
|
});
|
|
|
|
return (
|
|
<div className={css.actionBar}>
|
|
<p className={ownListingTextClasses}>
|
|
<FormattedMessage id={ownListingTextTranslationId} />
|
|
</p>
|
|
<NamedLink className={css.editListingLink} name="EditListingPage" params={editParams}>
|
|
<EditIcon className={css.editIcon} />
|
|
<FormattedMessage id="ListingPage.editListing" />
|
|
</NamedLink>
|
|
</div>
|
|
);
|
|
} else if (isClosed) {
|
|
return (
|
|
<div className={css.actionBar}>
|
|
<p className={css.closedListingText}>
|
|
<FormattedMessage id="ListingPage.closedListing" />
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const { arrayOf, bool, func, object, oneOf, shape, string } = PropTypes;
|
|
|
|
ActionBarMaybe.propTypes = {
|
|
isOwnListing: bool.isRequired,
|
|
listing: propTypes.listing.isRequired,
|
|
editParams: object.isRequired,
|
|
};
|
|
|
|
ActionBarMaybe.displayName = 'ActionBarMaybe';
|
|
|
|
const gotoBookTab = (history, listing) => {
|
|
if (!listing.id) {
|
|
// Listing not fully loaded yet
|
|
return;
|
|
}
|
|
const routes = routeConfiguration();
|
|
history.push(
|
|
createResourceLocatorString(
|
|
'ListingPageBook',
|
|
routes,
|
|
{ id: listing.id.uuid, slug: createSlug(listing.attributes.title) },
|
|
{}
|
|
)
|
|
);
|
|
};
|
|
|
|
const gotoListingTab = (history, listing) => {
|
|
if (!listing.id) {
|
|
// Listing not fully loaded yet
|
|
return;
|
|
}
|
|
const routes = routeConfiguration();
|
|
history.push(
|
|
createResourceLocatorString(
|
|
'ListingPage',
|
|
routes,
|
|
{ id: listing.id.uuid, slug: createSlug(listing.attributes.title) },
|
|
{}
|
|
)
|
|
);
|
|
};
|
|
|
|
// TODO: price unit (per x), custom fields, contact, reviews
|
|
// N.B. All the presentational content needs to be extracted to their own components
|
|
export class ListingPageComponent extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
const { enquiryModalOpenForListingId, params } = props;
|
|
this.state = {
|
|
pageClassNames: [],
|
|
imageCarouselOpen: false,
|
|
enquiryModalOpen: enquiryModalOpenForListingId === params.id,
|
|
};
|
|
|
|
this.handleSubmit = this.handleSubmit.bind(this);
|
|
this.onContactUser = this.onContactUser.bind(this);
|
|
this.onSubmitEnquiry = this.onSubmitEnquiry.bind(this);
|
|
}
|
|
|
|
handleSubmit(values) {
|
|
const { history, getListing, params, useInitialValues } = this.props;
|
|
const listingId = new UUID(params.id);
|
|
const listing = getListing(listingId);
|
|
|
|
const { bookingDates } = values;
|
|
|
|
const initialValues = {
|
|
listing,
|
|
bookingDates: {
|
|
bookingStart: bookingDates.startDate,
|
|
bookingEnd: bookingDates.endDate,
|
|
},
|
|
};
|
|
|
|
const routes = routeConfiguration();
|
|
// Customize checkout page state with current listing and selected bookingDates
|
|
const { setInitialValues } = findRouteByRouteName('CheckoutPage', routes);
|
|
useInitialValues(setInitialValues, initialValues);
|
|
|
|
// Redirect to CheckoutPage
|
|
history.push(
|
|
createResourceLocatorString(
|
|
'CheckoutPage',
|
|
routes,
|
|
{ id: listing.id.uuid, slug: createSlug(listing.attributes.title) },
|
|
{}
|
|
)
|
|
);
|
|
}
|
|
|
|
onContactUser() {
|
|
const { currentUser, history, useInitialValues, params } = this.props;
|
|
|
|
if (!currentUser) {
|
|
const location = window.location;
|
|
const state = { from: `${location.pathname}${location.search}${location.hash}` };
|
|
|
|
// We need to log in before showing the modal, but first we need to ensure
|
|
// that modal does open when user is redirected back to this listingpage
|
|
useInitialValues(setInitialValues, { enquiryModalOpenForListingId: params.id });
|
|
|
|
// signup and return back to listingPage.
|
|
history.push(createResourceLocatorString('SignupPage', routeConfiguration(), {}, {}), state);
|
|
} else {
|
|
this.setState({ enquiryModalOpen: true });
|
|
}
|
|
}
|
|
|
|
onSubmitEnquiry(values) {
|
|
const { history, params, onSendEnquiry } = this.props;
|
|
const routes = routeConfiguration();
|
|
const listingId = new UUID(params.id);
|
|
const { message } = values;
|
|
|
|
onSendEnquiry(listingId, message.trim())
|
|
.then(txId => {
|
|
this.setState({ enquiryModalOpen: false });
|
|
|
|
// Redirect to OrderDetailsPage
|
|
history.push(
|
|
createResourceLocatorString('OrderDetailsPage', routes, { id: txId.uuid }, {})
|
|
);
|
|
})
|
|
.catch(() => {
|
|
// Ignore, error handling in duck file
|
|
});
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
unitType,
|
|
tab,
|
|
isAuthenticated,
|
|
currentUser,
|
|
getListing,
|
|
intl,
|
|
onManageDisableScrolling,
|
|
params,
|
|
scrollingDisabled,
|
|
showListingError,
|
|
history,
|
|
reviews,
|
|
fetchReviewsError,
|
|
sendEnquiryInProgress,
|
|
sendEnquiryError,
|
|
} = this.props;
|
|
|
|
const listingId = new UUID(params.id);
|
|
const currentListing = ensureListing(getListing(listingId));
|
|
const listingSlug = params.slug || createSlug(currentListing.attributes.title || '');
|
|
const {
|
|
description = '',
|
|
geolocation = null,
|
|
price = null,
|
|
title = '',
|
|
publicData,
|
|
} = currentListing.attributes;
|
|
|
|
const { customAttributes } = currentListing.attributes;
|
|
const hasCategoryConfig = config.customAttributes && config.customAttributes.category;
|
|
const hasCategoryAttribute = customAttributes && customAttributes.category;
|
|
const category =
|
|
hasCategoryConfig && hasCategoryAttribute ? (
|
|
<span>
|
|
<FormattedMessage id={`ListingPage.category.${customAttributes.category}`} />
|
|
<span className={css.separator}>•</span>
|
|
</span>
|
|
) : null;
|
|
|
|
const topbar = <TopbarContainer />;
|
|
|
|
const loadingTitle = intl.formatMessage({
|
|
id: 'ListingPage.loadingListingTitle',
|
|
});
|
|
const errorTitle = intl.formatMessage({
|
|
id: 'ListingPage.errorLoadingListingTitle',
|
|
});
|
|
|
|
if (showListingError && showListingError.status === 404) {
|
|
// 404 listing not found
|
|
|
|
return <NamedRedirect name="NotFoundPage" />;
|
|
} else if (showListingError) {
|
|
// Other error in fetching listing
|
|
|
|
return (
|
|
<Page title={errorTitle} scrollingDisabled={scrollingDisabled}>
|
|
<LayoutSingleColumn className={css.pageRoot}>
|
|
<LayoutWrapperTopbar>{topbar}</LayoutWrapperTopbar>
|
|
<LayoutWrapperMain>
|
|
<p className={css.errorText}>
|
|
<FormattedMessage id="ListingPage.errorLoadingListingMessage" />
|
|
</p>
|
|
</LayoutWrapperMain>
|
|
<LayoutWrapperFooter>
|
|
<Footer />
|
|
</LayoutWrapperFooter>
|
|
</LayoutSingleColumn>
|
|
</Page>
|
|
);
|
|
} else if (!currentListing.id) {
|
|
// Still loading the listing
|
|
|
|
return (
|
|
<Page title={loadingTitle} scrollingDisabled={scrollingDisabled}>
|
|
<LayoutSingleColumn className={css.pageRoot}>
|
|
<LayoutWrapperTopbar>{topbar}</LayoutWrapperTopbar>
|
|
<LayoutWrapperMain>
|
|
<p className={css.loadingText}>
|
|
<FormattedMessage id="ListingPage.loadingListingMessage" />
|
|
</p>
|
|
</LayoutWrapperMain>
|
|
<LayoutWrapperFooter>
|
|
<Footer />
|
|
</LayoutWrapperFooter>
|
|
</LayoutSingleColumn>
|
|
</Page>
|
|
);
|
|
}
|
|
|
|
const hasImages = currentListing.images && currentListing.images.length > 0;
|
|
const firstImage = hasImages ? currentListing.images[0] : null;
|
|
|
|
const handleViewPhotosClick = e => {
|
|
// Stop event from bubbling up to prevent image click handler
|
|
// trying to open the carousel as well.
|
|
e.stopPropagation();
|
|
this.setState({
|
|
imageCarouselOpen: true,
|
|
});
|
|
};
|
|
const viewPhotosButton = hasImages ? (
|
|
<button className={css.viewPhotos} onClick={handleViewPhotosClick}>
|
|
<FormattedMessage
|
|
id="ListingPage.viewImagesButton"
|
|
values={{ count: currentListing.images.length }}
|
|
/>
|
|
</button>
|
|
) : null;
|
|
|
|
const authorAvailable = currentListing && currentListing.author;
|
|
const userAndListingAuthorAvailable = !!(currentUser && authorAvailable);
|
|
const isOwnListing =
|
|
userAndListingAuthorAvailable && currentListing.author.id.uuid === currentUser.id.uuid;
|
|
const isClosed = currentListing.attributes.state === LISTING_STATE_CLOSED;
|
|
const showContactUser = !currentUser || (currentUser && !isOwnListing);
|
|
|
|
const currentAuthor = authorAvailable ? currentListing.author : null;
|
|
const ensuredAuthor = ensureUser(currentAuthor);
|
|
|
|
const bannedUserDisplayName = intl.formatMessage({
|
|
id: 'ListingPage.bannedUserDisplayName',
|
|
});
|
|
const authorDisplayName = userDisplayName(ensuredAuthor, bannedUserDisplayName);
|
|
|
|
const bookBtnMessage = intl.formatMessage({ id: 'ListingPage.ctaButtonMessage' });
|
|
const { formattedPrice, priceTitle } = priceData(price, intl);
|
|
|
|
const showClosedListingHelpText = currentListing.id && isClosed;
|
|
const bookingHeading = (
|
|
<div className={css.bookingHeading}>
|
|
<h2 className={css.bookingTitle}>
|
|
<FormattedMessage id="ListingPage.bookingTitle" values={{ title }} />
|
|
</h2>
|
|
<div className={css.bookingHelp}>
|
|
<FormattedMessage
|
|
id={
|
|
showClosedListingHelpText
|
|
? 'ListingPage.bookingHelpClosedListing'
|
|
: 'ListingPage.bookingHelp'
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const handleMobileBookModalClose = () => {
|
|
gotoListingTab(history, currentListing);
|
|
};
|
|
|
|
const handleBookingSubmit = values => {
|
|
const isCurrentlyClosed = currentListing.attributes.state === LISTING_STATE_CLOSED;
|
|
if (isOwnListing || isCurrentlyClosed) {
|
|
window.scrollTo(0, 0);
|
|
} else {
|
|
this.handleSubmit(values);
|
|
}
|
|
};
|
|
|
|
const editParams = { id: listingId.uuid, slug: listingSlug, type: 'edit', tab: 'description' };
|
|
|
|
const handleBookButtonClick = () => {
|
|
const isCurrentlyClosed = currentListing.attributes.state === LISTING_STATE_CLOSED;
|
|
if (isOwnListing || isCurrentlyClosed) {
|
|
window.scrollTo(0, 0);
|
|
} else {
|
|
gotoBookTab(history, currentListing);
|
|
}
|
|
};
|
|
|
|
// Action bar is wrapped with a div that prevents the click events
|
|
// to the parent that would otherwise open the image carousel
|
|
const actionBar = currentListing.id ? (
|
|
<div onClick={e => e.stopPropagation()}>
|
|
<ActionBarMaybe
|
|
isOwnListing={isOwnListing}
|
|
listing={currentListing}
|
|
editParams={editParams}
|
|
/>
|
|
</div>
|
|
) : null;
|
|
|
|
const facebookImages = hasImages
|
|
? currentListing.images.map(image => {
|
|
return image.attributes.sizes.find(i => i.name === 'facebook');
|
|
})
|
|
: [];
|
|
const twitterImages = hasImages
|
|
? currentListing.images.map(image => {
|
|
return image.attributes.sizes.find(i => i.name === 'twitter');
|
|
})
|
|
: [];
|
|
const schemaImages = JSON.stringify(facebookImages.map(img => img.url));
|
|
const siteTitle = config.siteTitle;
|
|
const schemaTitle = intl.formatMessage(
|
|
{ id: 'ListingPage.schemaTitle' },
|
|
{ title, price: formattedPrice, siteTitle }
|
|
);
|
|
|
|
const hostLink = (
|
|
<NamedLink
|
|
className={css.authorNameLink}
|
|
name="ListingPage"
|
|
params={params}
|
|
to={{ hash: '#host' }}
|
|
>
|
|
{authorDisplayName}
|
|
</NamedLink>
|
|
);
|
|
|
|
const reviewsError = (
|
|
<h2 className={css.errorText}>
|
|
<FormattedMessage id="ListingPage.reviewsError" />
|
|
</h2>
|
|
);
|
|
|
|
return (
|
|
<Page
|
|
title={schemaTitle}
|
|
scrollingDisabled={scrollingDisabled}
|
|
author={authorDisplayName}
|
|
contentType="website"
|
|
description={description}
|
|
facebookImages={facebookImages}
|
|
twitterImages={twitterImages}
|
|
schema={{
|
|
'@context': 'http://schema.org',
|
|
'@type': 'ItemPage',
|
|
description: description,
|
|
name: schemaTitle,
|
|
image: schemaImages,
|
|
}}
|
|
>
|
|
<LayoutSingleColumn className={css.pageRoot}>
|
|
<LayoutWrapperTopbar>{topbar}</LayoutWrapperTopbar>
|
|
<LayoutWrapperMain>
|
|
<div>
|
|
<div className={css.threeToTwoWrapper}>
|
|
<div className={css.aspectWrapper} onClick={handleViewPhotosClick}>
|
|
{actionBar}
|
|
<ResponsiveImage
|
|
rootClassName={css.rootForImage}
|
|
alt={title}
|
|
image={firstImage}
|
|
nameSet={[
|
|
{ name: 'landscape-crop', size: '400w' },
|
|
{ name: 'landscape-crop2x', size: '800w' },
|
|
{ name: 'landscape-crop4x', size: '1600w' },
|
|
{ name: 'landscape-crop6x', size: '2400w' },
|
|
]}
|
|
sizes="100vw"
|
|
/>
|
|
{viewPhotosButton}
|
|
</div>
|
|
</div>
|
|
<Modal
|
|
id="ListingPage.imageCarousel"
|
|
scrollLayerClassName={css.carouselModalScrollLayer}
|
|
containerClassName={css.carouselModalContainer}
|
|
lightCloseButton
|
|
isOpen={this.state.imageCarouselOpen}
|
|
onClose={() => this.setState({ imageCarouselOpen: false })}
|
|
onManageDisableScrolling={onManageDisableScrolling}
|
|
>
|
|
<ImageCarousel images={currentListing.images} />
|
|
</Modal>
|
|
|
|
<div className={css.contentContainer}>
|
|
<div className={css.avatarWrapper}>
|
|
<NamedLink name="ListingPage" params={params} to={{ hash: '#host' }}>
|
|
<AvatarLarge
|
|
user={currentAuthor}
|
|
className={css.avatarDesktop}
|
|
disableProfileLink
|
|
/>
|
|
</NamedLink>
|
|
<NamedLink name="ListingPage" params={params} to={{ hash: '#host' }}>
|
|
<AvatarMedium
|
|
user={currentAuthor}
|
|
className={css.avatarMobile}
|
|
disableProfileLink
|
|
/>
|
|
</NamedLink>
|
|
</div>
|
|
|
|
<div className={css.mainContent}>
|
|
<div className={css.headingContainer}>
|
|
<div className={css.desktopPriceContainer}>
|
|
<div className={css.desktopPriceValue} title={priceTitle}>
|
|
{formattedPrice}
|
|
</div>
|
|
<div className={css.desktopPerUnit}>
|
|
<FormattedMessage id="ListingPage.perUnit" />
|
|
</div>
|
|
</div>
|
|
<div className={css.heading}>
|
|
<h1 className={css.title}>{title}</h1>
|
|
<div className={css.author}>
|
|
{category}
|
|
<FormattedMessage id="ListingPage.hostedBy" values={{ name: hostLink }} />
|
|
{showContactUser ? (
|
|
<span className={css.contactWrapper}>
|
|
<span className={css.separator}>•</span>
|
|
<InlineTextButton
|
|
className={css.contactLink}
|
|
onClick={this.onContactUser}
|
|
>
|
|
<FormattedMessage id="ListingPage.contactUser" />
|
|
</InlineTextButton>
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={css.descriptionContainer}>
|
|
<h2 className={css.descriptionTitle}>
|
|
<FormattedMessage id="ListingPage.descriptionTitle" />
|
|
</h2>
|
|
<p className={css.description}>{description}</p>
|
|
</div>
|
|
|
|
<SectionRulesMaybe publicData={publicData} />
|
|
<SectionMapMaybe geolocation={geolocation} publicData={publicData} />
|
|
|
|
<div className={css.reviewsContainer}>
|
|
<h2 className={css.reviewsHeading}>
|
|
<FormattedMessage
|
|
id="ListingPage.reviewsHeading"
|
|
values={{ count: reviews.length }}
|
|
/>
|
|
</h2>
|
|
{fetchReviewsError ? reviewsError : null}
|
|
<Reviews reviews={reviews} />
|
|
</div>
|
|
<div id="host" className={css.yourHostContainer}>
|
|
<h2 className={css.yourHostHeading}>
|
|
<FormattedMessage id="ListingPage.yourHostHeading" />
|
|
</h2>
|
|
{isOwnListing ? (
|
|
<NamedLink className={css.editProfileLink} name="ProfileSettingsPage">
|
|
<FormattedMessage id="ListingPage.editProfileLink" />
|
|
</NamedLink>
|
|
) : null}
|
|
<UserCard
|
|
user={currentListing.author}
|
|
currentUser={currentUser}
|
|
onContactUser={this.onContactUser}
|
|
/>
|
|
<Modal
|
|
id="ListingPage.enquiry"
|
|
contentClassName={css.enquiryModalContent}
|
|
isOpen={isAuthenticated && this.state.enquiryModalOpen}
|
|
onClose={() => this.setState({ enquiryModalOpen: false })}
|
|
onManageDisableScrolling={onManageDisableScrolling}
|
|
>
|
|
<EnquiryForm
|
|
className={css.enquiryForm}
|
|
submitButtonWrapperClassName={css.enquirySubmitButtonWrapper}
|
|
listingTitle={title}
|
|
authorDisplayName={authorDisplayName}
|
|
sendEnquiryError={sendEnquiryError}
|
|
onSubmit={this.onSubmitEnquiry}
|
|
inProgress={sendEnquiryInProgress}
|
|
/>
|
|
</Modal>
|
|
</div>
|
|
</div>
|
|
|
|
<ModalInMobile
|
|
className={css.modalInMobile}
|
|
containerClassName={css.modalContainer}
|
|
id="BookingDatesFormInModal"
|
|
isModalOpenOnMobile={tab === 'book'}
|
|
onClose={handleMobileBookModalClose}
|
|
showAsModalMaxWidth={MODAL_BREAKPOINT}
|
|
onManageDisableScrolling={onManageDisableScrolling}
|
|
>
|
|
<div className={css.modalHeading}>
|
|
<h1 className={css.title}>{title}</h1>
|
|
<div className={css.author}>
|
|
<span className={css.authorName}>
|
|
<FormattedMessage
|
|
id="ListingPage.hostedBy"
|
|
values={{ name: authorDisplayName }}
|
|
/>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{bookingHeading}
|
|
{!isClosed ? (
|
|
<BookingDatesForm
|
|
className={css.bookingForm}
|
|
submitButtonWrapperClassName={css.bookingDatesSubmitButtonWrapper}
|
|
unitType={unitType}
|
|
onSubmit={handleBookingSubmit}
|
|
price={price}
|
|
isOwnListing={isOwnListing}
|
|
/>
|
|
) : null}
|
|
</ModalInMobile>
|
|
<div className={css.openBookingForm}>
|
|
<div className={css.priceContainer}>
|
|
<div className={css.priceValue} title={priceTitle}>
|
|
{formattedPrice}
|
|
</div>
|
|
<div className={css.perUnit}>
|
|
<FormattedMessage id="ListingPage.perUnit" />
|
|
</div>
|
|
</div>
|
|
|
|
{!isClosed ? (
|
|
<Button rootClassName={css.bookButton} onClick={handleBookButtonClick}>
|
|
{bookBtnMessage}
|
|
</Button>
|
|
) : (
|
|
<div className={css.closedListingButton}>
|
|
<FormattedMessage id="ListingPage.closedListingButtonText" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</LayoutWrapperMain>
|
|
<LayoutWrapperFooter>
|
|
<Footer />
|
|
</LayoutWrapperFooter>
|
|
</LayoutSingleColumn>
|
|
</Page>
|
|
);
|
|
}
|
|
}
|
|
|
|
ListingPageComponent.defaultProps = {
|
|
unitType: config.bookingUnitType,
|
|
currentUser: null,
|
|
enquiryModalOpenForListingId: null,
|
|
showListingError: null,
|
|
tab: 'listing',
|
|
reviews: [],
|
|
fetchReviewsError: null,
|
|
sendEnquiryError: null,
|
|
};
|
|
|
|
ListingPageComponent.propTypes = {
|
|
// from withRouter
|
|
history: shape({
|
|
push: func.isRequired,
|
|
}).isRequired,
|
|
|
|
unitType: propTypes.bookingUnitType,
|
|
// from injectIntl
|
|
intl: intlShape.isRequired,
|
|
|
|
params: shape({
|
|
id: string.isRequired,
|
|
slug: string,
|
|
}).isRequired,
|
|
isAuthenticated: bool.isRequired,
|
|
currentUser: propTypes.currentUser,
|
|
getListing: func.isRequired,
|
|
onManageDisableScrolling: func.isRequired,
|
|
scrollingDisabled: bool.isRequired,
|
|
enquiryModalOpenForListingId: string,
|
|
showListingError: propTypes.error,
|
|
tab: oneOf(['book', 'listing']),
|
|
useInitialValues: func.isRequired,
|
|
reviews: arrayOf(propTypes.review),
|
|
fetchReviewsError: propTypes.error,
|
|
sendEnquiryInProgress: bool.isRequired,
|
|
sendEnquiryError: propTypes.error,
|
|
onSendEnquiry: func.isRequired,
|
|
};
|
|
|
|
const mapStateToProps = state => {
|
|
const { isAuthenticated } = state.Auth;
|
|
const {
|
|
showListingError,
|
|
reviews,
|
|
fetchReviewsError,
|
|
sendEnquiryInProgress,
|
|
sendEnquiryError,
|
|
enquiryModalOpenForListingId,
|
|
} = state.ListingPage;
|
|
const { currentUser } = state.user;
|
|
|
|
const getListing = id => {
|
|
const listings = getListingsById(state, [id]);
|
|
return listings.length === 1 ? listings[0] : null;
|
|
};
|
|
|
|
return {
|
|
isAuthenticated,
|
|
currentUser,
|
|
getListing,
|
|
scrollingDisabled: isScrollingDisabled(state),
|
|
enquiryModalOpenForListingId,
|
|
showListingError,
|
|
reviews,
|
|
fetchReviewsError,
|
|
sendEnquiryInProgress,
|
|
sendEnquiryError,
|
|
};
|
|
};
|
|
|
|
const mapDispatchToProps = dispatch => ({
|
|
onManageDisableScrolling: (componentId, disableScrolling) =>
|
|
dispatch(manageDisableScrolling(componentId, disableScrolling)),
|
|
useInitialValues: (setInitialValues, values) => dispatch(setInitialValues(values)),
|
|
onSendEnquiry: (listingId, message) => dispatch(sendEnquiry(listingId, message)),
|
|
});
|
|
|
|
// Note: it is important that the withRouter HOC is **outside** the
|
|
// connect HOC, otherwise React Router won't rerender any Route
|
|
// components since connect implements a shouldComponentUpdate
|
|
// lifecycle hook.
|
|
//
|
|
// See: https://github.com/ReactTraining/react-router/issues/4671
|
|
const ListingPage = compose(withRouter, connect(mapStateToProps, mapDispatchToProps), injectIntl)(
|
|
ListingPageComponent
|
|
);
|
|
|
|
ListingPage.setInitialValues = initialValues => setInitialValues(initialValues);
|
|
ListingPage.loadData = loadData;
|
|
|
|
export default ListingPage;
|