mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-29 05:00:43 +10:00
Merge pull request #800 from sharetribe/refactor-listing-page-contents
Split ListingPage into smaller sections
This commit is contained in:
commit
7d0e64c078
14 changed files with 789 additions and 772 deletions
61
src/containers/ListingPage/ActionBarMaybe.js
Normal file
61
src/containers/ListingPage/ActionBarMaybe.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import React from 'react';
|
||||
import { bool, oneOfType, object } from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import { LISTING_STATE_PENDING_APPROVAL, LISTING_STATE_CLOSED, propTypes } from '../../util/types';
|
||||
import { NamedLink } from '../../components';
|
||||
import EditIcon from './EditIcon';
|
||||
|
||||
import css from './ListingPage.css';
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
ActionBarMaybe.propTypes = {
|
||||
isOwnListing: bool.isRequired,
|
||||
listing: oneOfType([propTypes.listing, propTypes.ownListing]).isRequired,
|
||||
editParams: object.isRequired,
|
||||
};
|
||||
|
||||
ActionBarMaybe.displayName = 'ActionBarMaybe';
|
||||
|
||||
export default ActionBarMaybe;
|
||||
|
|
@ -17,6 +17,9 @@
|
|||
margin: 24px;
|
||||
}
|
||||
|
||||
.sectionImages {
|
||||
}
|
||||
|
||||
.threeToTwoWrapper {
|
||||
/* Layout */
|
||||
display: block;
|
||||
|
|
@ -216,7 +219,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.avatarWrapper {
|
||||
.sectionAvatar {
|
||||
/* Position (over the listing image)*/
|
||||
margin-left: 24px;
|
||||
margin-top: -31px;
|
||||
|
|
@ -252,7 +255,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.headingContainer {
|
||||
.sectionHeading {
|
||||
margin-top: 22px;
|
||||
margin-bottom: 34px;
|
||||
|
||||
|
|
@ -356,7 +359,7 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.descriptionContainer {
|
||||
.sectionDescription {
|
||||
padding: 0 24px;
|
||||
margin-bottom: 35px;
|
||||
|
||||
|
|
@ -379,7 +382,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.featuresContainer {
|
||||
.sectionFeatures {
|
||||
padding: 0 24px;
|
||||
margin-bottom: 32px;
|
||||
|
||||
|
|
@ -415,7 +418,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.locationContainer {
|
||||
.sectionMap {
|
||||
padding: 0 24px;
|
||||
margin-bottom: 35px;
|
||||
|
||||
|
|
@ -438,7 +441,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.reviewsContainer {
|
||||
.sectionReviews {
|
||||
padding: 0 24px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
|
|
@ -464,7 +467,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.yourHostContainer {
|
||||
.sectionHost {
|
||||
position: relative;
|
||||
padding: 0 24px;
|
||||
margin-bottom: 5px;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
import React, { Component } from 'react';
|
||||
import { arrayOf, bool, func, object, shape, string, oneOf, oneOfType } from 'prop-types';
|
||||
import { array, arrayOf, bool, func, shape, string, oneOf } 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';
|
||||
|
|
@ -18,38 +17,31 @@ import { richText } from '../../util/richText';
|
|||
import { getMarketplaceEntities } 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,
|
||||
PropertyGroup,
|
||||
} from '../../components';
|
||||
import { BookingDatesForm, TopbarContainer, EnquiryForm, NotFoundPage } from '../../containers';
|
||||
import { TopbarContainer, NotFoundPage } from '../../containers';
|
||||
|
||||
import { sendEnquiry, loadData, setInitialValues } from './ListingPage.duck';
|
||||
import EditIcon from './EditIcon';
|
||||
import SectionImages from './SectionImages';
|
||||
import SectionAvatar from './SectionAvatar';
|
||||
import SectionHeading from './SectionHeading';
|
||||
import SectionDescription from './SectionDescription';
|
||||
import SectionFeatures from './SectionFeatures';
|
||||
import SectionReviews from './SectionReviews';
|
||||
import SectionHost from './SectionHost';
|
||||
import SectionRulesMaybe from './SectionRulesMaybe';
|
||||
import SectionMapMaybe from './SectionMapMaybe';
|
||||
import SectionBooking from './SectionBooking';
|
||||
import css from './ListingPage.css';
|
||||
|
||||
// This defines when ModalInMobile shows content as Modal
|
||||
const MODAL_BREAKPOINT = 1023;
|
||||
const MIN_LENGTH_FOR_LONG_WORDS_IN_TITLE = 16;
|
||||
const MIN_LENGTH_FOR_LONG_WORDS_IN_DESCRIPTION = 20;
|
||||
|
||||
const { UUID } = sdkTypes;
|
||||
|
||||
|
|
@ -66,56 +58,6 @@ const priceData = (price, intl) => {
|
|||
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;
|
||||
};
|
||||
|
||||
ActionBarMaybe.propTypes = {
|
||||
isOwnListing: bool.isRequired,
|
||||
listing: oneOfType([propTypes.listing, propTypes.ownListing]).isRequired,
|
||||
editParams: object.isRequired,
|
||||
};
|
||||
|
||||
ActionBarMaybe.displayName = 'ActionBarMaybe';
|
||||
|
||||
const openBookModal = (history, listing) => {
|
||||
if (!listing.id) {
|
||||
// Listing not fully loaded yet
|
||||
|
|
@ -153,8 +95,6 @@ const categoryLabel = (categories, key) => {
|
|||
return cat ? cat.label : key;
|
||||
};
|
||||
|
||||
// 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);
|
||||
|
|
@ -257,6 +197,8 @@ export class ListingPageComponent extends Component {
|
|||
fetchReviewsError,
|
||||
sendEnquiryInProgress,
|
||||
sendEnquiryError,
|
||||
categoriesConfig,
|
||||
amenitiesConfig,
|
||||
} = this.props;
|
||||
|
||||
const isBook = !!parse(location.search).book;
|
||||
|
|
@ -304,23 +246,8 @@ export class ListingPageComponent extends Component {
|
|||
</span>
|
||||
);
|
||||
|
||||
const category =
|
||||
publicData && publicData.category ? (
|
||||
<span>
|
||||
{categoryLabel(config.custom.categories, publicData.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
|
||||
|
||||
|
|
@ -328,6 +255,10 @@ export class ListingPageComponent extends Component {
|
|||
} else if (showListingError) {
|
||||
// Other error in fetching listing
|
||||
|
||||
const errorTitle = intl.formatMessage({
|
||||
id: 'ListingPage.errorLoadingListingTitle',
|
||||
});
|
||||
|
||||
return (
|
||||
<Page title={errorTitle} scrollingDisabled={scrollingDisabled}>
|
||||
<LayoutSingleColumn className={css.pageRoot}>
|
||||
|
|
@ -346,6 +277,10 @@ export class ListingPageComponent extends Component {
|
|||
} else if (!currentListing.id) {
|
||||
// Still loading the listing
|
||||
|
||||
const loadingTitle = intl.formatMessage({
|
||||
id: 'ListingPage.loadingListingTitle',
|
||||
});
|
||||
|
||||
return (
|
||||
<Page title={loadingTitle} scrollingDisabled={scrollingDisabled}>
|
||||
<LayoutSingleColumn className={css.pageRoot}>
|
||||
|
|
@ -363,9 +298,6 @@ export class ListingPageComponent extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
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.
|
||||
|
|
@ -374,15 +306,6 @@ export class ListingPageComponent extends Component {
|
|||
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 =
|
||||
|
|
@ -398,27 +321,8 @@ export class ListingPageComponent extends Component {
|
|||
});
|
||||
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: richTitle }} />
|
||||
</h2>
|
||||
<div className={css.bookingHelp}>
|
||||
<FormattedMessage
|
||||
id={
|
||||
showClosedListingHelpText
|
||||
? 'ListingPage.bookingHelpClosedListing'
|
||||
: 'ListingPage.bookingHelp'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleMobileBookModalClose = () => {
|
||||
closeBookModal(history, currentListing);
|
||||
};
|
||||
|
|
@ -432,8 +336,6 @@ export class ListingPageComponent extends Component {
|
|||
}
|
||||
};
|
||||
|
||||
const editParams = { id: listingId.uuid, slug: listingSlug, type: 'edit', tab: 'description' };
|
||||
|
||||
const handleBookButtonClick = () => {
|
||||
const isCurrentlyClosed = currentListing.attributes.state === LISTING_STATE_CLOSED;
|
||||
if (isOwnListing || isCurrentlyClosed) {
|
||||
|
|
@ -443,18 +345,6 @@ export class ListingPageComponent extends Component {
|
|||
}
|
||||
};
|
||||
|
||||
// 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 listingImages = (listing, variantName) =>
|
||||
(listing.images || [])
|
||||
.map(image => {
|
||||
|
|
@ -490,11 +380,13 @@ export class ListingPageComponent extends Component {
|
|||
</NamedLink>
|
||||
);
|
||||
|
||||
const reviewsError = (
|
||||
<h2 className={css.errorText}>
|
||||
<FormattedMessage id="ListingPage.reviewsError" />
|
||||
</h2>
|
||||
);
|
||||
const category =
|
||||
publicData && publicData.category ? (
|
||||
<span>
|
||||
{categoryLabel(categoriesConfig, publicData.category)}
|
||||
<span className={css.separator}>•</span>
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<Page
|
||||
|
|
@ -517,212 +409,76 @@ export class ListingPageComponent extends Component {
|
|||
<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 })}
|
||||
<SectionImages
|
||||
title={title}
|
||||
listing={currentListing}
|
||||
isOwnListing={isOwnListing}
|
||||
editParams={{
|
||||
id: listingId.uuid,
|
||||
slug: listingSlug,
|
||||
type: 'edit',
|
||||
tab: 'description',
|
||||
}}
|
||||
imageCarouselOpen={this.state.imageCarouselOpen}
|
||||
onImageCarouselClose={() => this.setState({ imageCarouselOpen: false })}
|
||||
handleViewPhotosClick={handleViewPhotosClick}
|
||||
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>
|
||||
|
||||
<SectionAvatar user={currentAuthor} params={params} />
|
||||
<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}>{richTitle}</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}>
|
||||
{richText(description, {
|
||||
longWordMinLength: MIN_LENGTH_FOR_LONG_WORDS_IN_DESCRIPTION,
|
||||
longWordClass: css.longWord,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={css.featuresContainer}>
|
||||
<h2 className={css.featuresTitle}>
|
||||
<FormattedMessage id="ListingPage.featuresTitle" />
|
||||
</h2>
|
||||
<PropertyGroup
|
||||
id="ListingPage.amenities"
|
||||
options={config.custom.amenities}
|
||||
selectedOptions={publicData.amenities}
|
||||
twoColumns={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SectionHeading
|
||||
priceTitle={priceTitle}
|
||||
formattedPrice={formattedPrice}
|
||||
richTitle={richTitle}
|
||||
category={category}
|
||||
hostLink={hostLink}
|
||||
showContactUser={showContactUser}
|
||||
onContactUser={this.onContactUser}
|
||||
/>
|
||||
<SectionDescription description={description} />
|
||||
<SectionFeatures
|
||||
options={amenitiesConfig}
|
||||
selectedOptions={publicData.amenities}
|
||||
/>
|
||||
<SectionRulesMaybe publicData={publicData} />
|
||||
<SectionMapMaybe
|
||||
geolocation={geolocation}
|
||||
publicData={publicData}
|
||||
listingId={currentListing.id}
|
||||
/>
|
||||
|
||||
<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>
|
||||
<SectionReviews reviews={reviews} fetchReviewsError={fetchReviewsError} />
|
||||
<SectionHost
|
||||
title={title}
|
||||
listing={currentListing}
|
||||
isOwnListing={isOwnListing}
|
||||
authorDisplayName={authorDisplayName}
|
||||
onContactUser={this.onContactUser}
|
||||
isEnquiryModalOpen={isAuthenticated && this.state.enquiryModalOpen}
|
||||
onCloseEnquiryModal={() => this.setState({ enquiryModalOpen: false })}
|
||||
sendEnquiryError={sendEnquiryError}
|
||||
sendEnquiryInProgress={sendEnquiryInProgress}
|
||||
onSubmitEnquiry={this.onSubmitEnquiry}
|
||||
currentUser={currentUser}
|
||||
onManageDisableScrolling={onManageDisableScrolling}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ModalInMobile
|
||||
className={css.modalInMobile}
|
||||
containerClassName={css.modalContainer}
|
||||
id="BookingDatesFormInModal"
|
||||
isModalOpenOnMobile={isBook}
|
||||
onClose={handleMobileBookModalClose}
|
||||
showAsModalMaxWidth={MODAL_BREAKPOINT}
|
||||
<SectionBooking
|
||||
listing={currentListing}
|
||||
isOwnListing={isOwnListing}
|
||||
isClosed={isClosed}
|
||||
isBook={isBook}
|
||||
unitType={unitType}
|
||||
price={price}
|
||||
formattedPrice={formattedPrice}
|
||||
priceTitle={priceTitle}
|
||||
handleBookingSubmit={handleBookingSubmit}
|
||||
richTitle={richTitle}
|
||||
authorDisplayName={authorDisplayName}
|
||||
handleBookButtonClick={handleBookButtonClick}
|
||||
handleMobileBookModalClose={handleMobileBookModalClose}
|
||||
onManageDisableScrolling={onManageDisableScrolling}
|
||||
>
|
||||
<div className={css.modalHeading}>
|
||||
<h1 className={css.title}>{richTitle}</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>
|
||||
|
|
@ -743,6 +499,8 @@ ListingPageComponent.defaultProps = {
|
|||
reviews: [],
|
||||
fetchReviewsError: null,
|
||||
sendEnquiryError: null,
|
||||
categoriesConfig: config.custom.categories,
|
||||
amenitiesConfig: config.custom.amenities,
|
||||
};
|
||||
|
||||
ListingPageComponent.propTypes = {
|
||||
|
|
@ -778,6 +536,9 @@ ListingPageComponent.propTypes = {
|
|||
sendEnquiryInProgress: bool.isRequired,
|
||||
sendEnquiryError: propTypes.error,
|
||||
onSendEnquiry: func.isRequired,
|
||||
|
||||
categoriesConfig: array,
|
||||
amenitiesConfig: array,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
|
|
|
|||
|
|
@ -24,11 +24,20 @@ import { showListingRequest, showListingError, showListing } from './ListingPage
|
|||
// Otherwise, ListingPage itself is not initialized correctly when routeConfiguration is imported
|
||||
// (loadData call fails).
|
||||
import routeConfiguration from '../../routeConfiguration';
|
||||
import { ListingPageComponent, ActionBarMaybe } from './ListingPage';
|
||||
import { ListingPageComponent } from './ListingPage';
|
||||
import ActionBarMaybe from './ActionBarMaybe';
|
||||
|
||||
const { UUID } = sdkTypes;
|
||||
const noop = () => null;
|
||||
|
||||
const categoriesConfig = [{ key: 'cat1', label: 'Cat 1' }, { key: 'cat2', label: 'Cat 2' }];
|
||||
|
||||
const amenitiesConfig = [
|
||||
{ key: 'feat1', label: 'Feat 1' },
|
||||
{ key: 'feat2', label: 'Feat 2' },
|
||||
{ key: 'feat3', label: 'Feat 3' },
|
||||
];
|
||||
|
||||
describe('ListingPage', () => {
|
||||
it('matches snapshot', () => {
|
||||
const currentUser = createCurrentUser('user-2');
|
||||
|
|
@ -66,6 +75,8 @@ describe('ListingPage', () => {
|
|||
onResendVerificationEmail: noop,
|
||||
sendEnquiryInProgress: false,
|
||||
onSendEnquiry: noop,
|
||||
categoriesConfig,
|
||||
amenitiesConfig,
|
||||
};
|
||||
|
||||
const tree = renderShallow(<ListingPageComponent {...props} />);
|
||||
|
|
|
|||
20
src/containers/ListingPage/SectionAvatar.js
Normal file
20
src/containers/ListingPage/SectionAvatar.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
import { NamedLink, AvatarLarge, AvatarMedium } from '../../components';
|
||||
|
||||
import css from './ListingPage.css';
|
||||
|
||||
const SectionAvatar = props => {
|
||||
const { user, params } = props;
|
||||
return (
|
||||
<div className={css.sectionAvatar}>
|
||||
<NamedLink name="ListingPage" params={params} to={{ hash: '#host' }}>
|
||||
<AvatarLarge user={user} className={css.avatarDesktop} disableProfileLink />
|
||||
</NamedLink>
|
||||
<NamedLink name="ListingPage" params={params} to={{ hash: '#host' }}>
|
||||
<AvatarMedium user={user} className={css.avatarMobile} disableProfileLink />
|
||||
</NamedLink>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionAvatar;
|
||||
98
src/containers/ListingPage/SectionBooking.js
Normal file
98
src/containers/ListingPage/SectionBooking.js
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { ModalInMobile, Button } from '../../components';
|
||||
import { BookingDatesForm } from '../../containers';
|
||||
|
||||
import css from './ListingPage.css';
|
||||
|
||||
// This defines when ModalInMobile shows content as Modal
|
||||
const MODAL_BREAKPOINT = 1023;
|
||||
|
||||
const SectionBooking = props => {
|
||||
const {
|
||||
listing,
|
||||
isOwnListing,
|
||||
isClosed,
|
||||
isBook,
|
||||
unitType,
|
||||
price,
|
||||
formattedPrice,
|
||||
priceTitle,
|
||||
handleBookingSubmit,
|
||||
richTitle,
|
||||
authorDisplayName,
|
||||
handleBookButtonClick,
|
||||
handleMobileBookModalClose,
|
||||
onManageDisableScrolling,
|
||||
} = props;
|
||||
const showClosedListingHelpText = listing.id && isClosed;
|
||||
return (
|
||||
<div>
|
||||
<ModalInMobile
|
||||
className={css.modalInMobile}
|
||||
containerClassName={css.modalContainer}
|
||||
id="BookingDatesFormInModal"
|
||||
isModalOpenOnMobile={isBook}
|
||||
onClose={handleMobileBookModalClose}
|
||||
showAsModalMaxWidth={MODAL_BREAKPOINT}
|
||||
onManageDisableScrolling={onManageDisableScrolling}
|
||||
>
|
||||
<div className={css.modalHeading}>
|
||||
<h1 className={css.title}>{richTitle}</h1>
|
||||
<div className={css.author}>
|
||||
<span className={css.authorName}>
|
||||
<FormattedMessage id="ListingPage.hostedBy" values={{ name: authorDisplayName }} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={css.bookingHeading}>
|
||||
<h2 className={css.bookingTitle}>
|
||||
<FormattedMessage id="ListingPage.bookingTitle" values={{ title: richTitle }} />
|
||||
</h2>
|
||||
<div className={css.bookingHelp}>
|
||||
<FormattedMessage
|
||||
id={
|
||||
showClosedListingHelpText
|
||||
? 'ListingPage.bookingHelpClosedListing'
|
||||
: 'ListingPage.bookingHelp'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!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}>
|
||||
<FormattedMessage id="ListingPage.ctaButtonMessage" />
|
||||
</Button>
|
||||
) : (
|
||||
<div className={css.closedListingButton}>
|
||||
<FormattedMessage id="ListingPage.closedListingButtonText" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionBooking;
|
||||
26
src/containers/ListingPage/SectionDescription.js
Normal file
26
src/containers/ListingPage/SectionDescription.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { richText } from '../../util/richText';
|
||||
|
||||
import css from './ListingPage.css';
|
||||
|
||||
const MIN_LENGTH_FOR_LONG_WORDS_IN_DESCRIPTION = 20;
|
||||
|
||||
const SectionDescription = props => {
|
||||
const { description } = props;
|
||||
return (
|
||||
<div className={css.sectionDescription}>
|
||||
<h2 className={css.descriptionTitle}>
|
||||
<FormattedMessage id="ListingPage.descriptionTitle" />
|
||||
</h2>
|
||||
<p className={css.description}>
|
||||
{richText(description, {
|
||||
longWordMinLength: MIN_LENGTH_FOR_LONG_WORDS_IN_DESCRIPTION,
|
||||
longWordClass: css.longWord,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionDescription;
|
||||
24
src/containers/ListingPage/SectionFeatures.js
Normal file
24
src/containers/ListingPage/SectionFeatures.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { PropertyGroup } from '../../components';
|
||||
|
||||
import css from './ListingPage.css';
|
||||
|
||||
const SectionFeatures = props => {
|
||||
const { options, selectedOptions } = props;
|
||||
return (
|
||||
<div className={css.sectionFeatures}>
|
||||
<h2 className={css.featuresTitle}>
|
||||
<FormattedMessage id="ListingPage.featuresTitle" />
|
||||
</h2>
|
||||
<PropertyGroup
|
||||
id="ListingPage.amenities"
|
||||
options={options}
|
||||
selectedOptions={selectedOptions}
|
||||
twoColumns={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionFeatures;
|
||||
46
src/containers/ListingPage/SectionHeading.js
Normal file
46
src/containers/ListingPage/SectionHeading.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { InlineTextButton } from '../../components';
|
||||
|
||||
import css from './ListingPage.css';
|
||||
|
||||
const SectionHeading = props => {
|
||||
const {
|
||||
priceTitle,
|
||||
formattedPrice,
|
||||
richTitle,
|
||||
category,
|
||||
hostLink,
|
||||
showContactUser,
|
||||
onContactUser,
|
||||
} = props;
|
||||
return (
|
||||
<div className={css.sectionHeading}>
|
||||
<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}>{richTitle}</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={onContactUser}>
|
||||
<FormattedMessage id="ListingPage.contactUser" />
|
||||
</InlineTextButton>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionHeading;
|
||||
55
src/containers/ListingPage/SectionHost.js
Normal file
55
src/containers/ListingPage/SectionHost.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { NamedLink, UserCard, Modal } from '../../components';
|
||||
import { EnquiryForm } from '../../containers';
|
||||
|
||||
import css from './ListingPage.css';
|
||||
|
||||
const SectionHost = props => {
|
||||
const {
|
||||
title,
|
||||
listing,
|
||||
isOwnListing,
|
||||
authorDisplayName,
|
||||
onContactUser,
|
||||
isEnquiryModalOpen,
|
||||
onCloseEnquiryModal,
|
||||
sendEnquiryError,
|
||||
sendEnquiryInProgress,
|
||||
onSubmitEnquiry,
|
||||
currentUser,
|
||||
onManageDisableScrolling,
|
||||
} = props;
|
||||
return (
|
||||
<div id="host" className={css.sectionHost}>
|
||||
<h2 className={css.yourHostHeading}>
|
||||
<FormattedMessage id="ListingPage.yourHostHeading" />
|
||||
</h2>
|
||||
{isOwnListing ? (
|
||||
<NamedLink className={css.editProfileLink} name="ProfileSettingsPage">
|
||||
<FormattedMessage id="ListingPage.editProfileLink" />
|
||||
</NamedLink>
|
||||
) : null}
|
||||
<UserCard user={listing.author} currentUser={currentUser} onContactUser={onContactUser} />
|
||||
<Modal
|
||||
id="ListingPage.enquiry"
|
||||
contentClassName={css.enquiryModalContent}
|
||||
isOpen={isEnquiryModalOpen}
|
||||
onClose={onCloseEnquiryModal}
|
||||
onManageDisableScrolling={onManageDisableScrolling}
|
||||
>
|
||||
<EnquiryForm
|
||||
className={css.enquiryForm}
|
||||
submitButtonWrapperClassName={css.enquirySubmitButtonWrapper}
|
||||
listingTitle={title}
|
||||
authorDisplayName={authorDisplayName}
|
||||
sendEnquiryError={sendEnquiryError}
|
||||
onSubmit={onSubmitEnquiry}
|
||||
inProgress={sendEnquiryInProgress}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionHost;
|
||||
75
src/containers/ListingPage/SectionImages.js
Normal file
75
src/containers/ListingPage/SectionImages.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { ResponsiveImage, Modal, ImageCarousel } from '../../components';
|
||||
import ActionBarMaybe from './ActionBarMaybe';
|
||||
|
||||
import css from './ListingPage.css';
|
||||
|
||||
const SectionImages = props => {
|
||||
const {
|
||||
title,
|
||||
listing,
|
||||
isOwnListing,
|
||||
editParams,
|
||||
handleViewPhotosClick,
|
||||
imageCarouselOpen,
|
||||
onImageCarouselClose,
|
||||
onManageDisableScrolling,
|
||||
} = props;
|
||||
|
||||
const hasImages = listing.images && listing.images.length > 0;
|
||||
const firstImage = hasImages ? listing.images[0] : null;
|
||||
|
||||
// Action bar is wrapped with a div that prevents the click events
|
||||
// to the parent that would otherwise open the image carousel
|
||||
const actionBar = listing.id ? (
|
||||
<div onClick={e => e.stopPropagation()}>
|
||||
<ActionBarMaybe isOwnListing={isOwnListing} listing={listing} editParams={editParams} />
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const viewPhotosButton = hasImages ? (
|
||||
<button className={css.viewPhotos} onClick={handleViewPhotosClick}>
|
||||
<FormattedMessage
|
||||
id="ListingPage.viewImagesButton"
|
||||
values={{ count: listing.images.length }}
|
||||
/>
|
||||
</button>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className={css.sectionImages}>
|
||||
<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={imageCarouselOpen}
|
||||
onClose={onImageCarouselClose}
|
||||
onManageDisableScrolling={onManageDisableScrolling}
|
||||
>
|
||||
<ImageCarousel images={listing.images} />
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionImages;
|
||||
|
|
@ -17,7 +17,7 @@ const SectionMapMaybe = props => {
|
|||
}
|
||||
|
||||
const address = publicData.location ? publicData.location.address : '';
|
||||
const classes = classNames(rootClassName || css.locationContainer, className);
|
||||
const classes = classNames(rootClassName || css.sectionMap, className);
|
||||
|
||||
const mapProps = config.coordinates.fuzzy
|
||||
? { obfuscatedCenter: obfuscatedCoordinates(geolocation, listingId ? listingId.uuid : null) }
|
||||
|
|
|
|||
27
src/containers/ListingPage/SectionReviews.js
Normal file
27
src/containers/ListingPage/SectionReviews.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Reviews } from '../../components';
|
||||
|
||||
import css from './ListingPage.css';
|
||||
|
||||
const SectionReviews = props => {
|
||||
const { reviews, fetchReviewsError } = props;
|
||||
|
||||
const reviewsError = (
|
||||
<h2 className={css.errorText}>
|
||||
<FormattedMessage id="ListingPage.reviewsError" />
|
||||
</h2>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={css.sectionReviews}>
|
||||
<h2 className={css.reviewsHeading}>
|
||||
<FormattedMessage id="ListingPage.reviewsHeading" values={{ count: reviews.length }} />
|
||||
</h2>
|
||||
{fetchReviewsError ? reviewsError : null}
|
||||
<Reviews reviews={reviews} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionReviews;
|
||||
|
|
@ -34,300 +34,140 @@ exports[`ListingPage matches snapshot 1`] = `
|
|||
rootClassName={null}
|
||||
>
|
||||
<div>
|
||||
<SectionImages
|
||||
editParams={
|
||||
Object {
|
||||
"id": "listing1",
|
||||
"slug": "listing1-title",
|
||||
"tab": "description",
|
||||
"type": "edit",
|
||||
}
|
||||
}
|
||||
handleViewPhotosClick={[Function]}
|
||||
imageCarouselOpen={false}
|
||||
isOwnListing={false}
|
||||
listing={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"deleted": false,
|
||||
"description": "listing1 description",
|
||||
"geolocation": LatLng {
|
||||
"lat": 40,
|
||||
"lng": 60,
|
||||
},
|
||||
"price": Money {
|
||||
"amount": 5500,
|
||||
"currency": "USD",
|
||||
},
|
||||
"publicData": Object {},
|
||||
"state": "published",
|
||||
"title": "listing1 title",
|
||||
},
|
||||
"author": Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "TT",
|
||||
"displayName": "user-1 display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user-1",
|
||||
},
|
||||
"type": "user",
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "listing1",
|
||||
},
|
||||
"images": Array [],
|
||||
"type": "listing",
|
||||
}
|
||||
}
|
||||
onImageCarouselClose={[Function]}
|
||||
onManageDisableScrolling={[Function]}
|
||||
title="listing1 title"
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
onClick={[Function]}
|
||||
>
|
||||
<div
|
||||
onClick={[Function]}
|
||||
>
|
||||
<ActionBarMaybe
|
||||
editParams={
|
||||
Object {
|
||||
"id": "listing1",
|
||||
"slug": "listing1-title",
|
||||
"tab": "description",
|
||||
"type": "edit",
|
||||
<SectionAvatar
|
||||
params={
|
||||
Object {
|
||||
"id": "listing1",
|
||||
"slug": "listing1-title",
|
||||
}
|
||||
}
|
||||
user={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "TT",
|
||||
"displayName": "user-1 display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user-1",
|
||||
},
|
||||
"type": "user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<SectionHeading
|
||||
category={null}
|
||||
formattedPrice={55}
|
||||
hostLink={
|
||||
<NamedLink
|
||||
className={undefined}
|
||||
name="ListingPage"
|
||||
params={
|
||||
Object {
|
||||
"id": "listing1",
|
||||
"slug": "listing1-title",
|
||||
}
|
||||
}
|
||||
}
|
||||
isOwnListing={false}
|
||||
listing={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"deleted": false,
|
||||
"description": "listing1 description",
|
||||
"geolocation": LatLng {
|
||||
"lat": 40,
|
||||
"lng": 60,
|
||||
},
|
||||
"price": Money {
|
||||
"amount": 5500,
|
||||
"currency": "USD",
|
||||
},
|
||||
"publicData": Object {},
|
||||
"state": "published",
|
||||
"title": "listing1 title",
|
||||
},
|
||||
"author": Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "TT",
|
||||
"displayName": "user-1 display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user-1",
|
||||
},
|
||||
"type": "user",
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "listing1",
|
||||
},
|
||||
"images": Array [],
|
||||
"type": "listing",
|
||||
to={
|
||||
Object {
|
||||
"hash": "#host",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<ResponsiveImage
|
||||
alt="listing1 title"
|
||||
className={null}
|
||||
image={null}
|
||||
nameSet={
|
||||
>
|
||||
user-1 display name
|
||||
</NamedLink>
|
||||
}
|
||||
onContactUser={[Function]}
|
||||
priceTitle={55}
|
||||
richTitle={
|
||||
<span>
|
||||
|
||||
listing1
|
||||
|
||||
title
|
||||
|
||||
</span>
|
||||
}
|
||||
showContactUser={true}
|
||||
/>
|
||||
<SectionDescription
|
||||
description="listing1 description"
|
||||
/>
|
||||
<SectionFeatures
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"name": "landscape-crop",
|
||||
"size": "400w",
|
||||
"key": "feat1",
|
||||
"label": "Feat 1",
|
||||
},
|
||||
Object {
|
||||
"name": "landscape-crop2x",
|
||||
"size": "800w",
|
||||
"key": "feat2",
|
||||
"label": "Feat 2",
|
||||
},
|
||||
Object {
|
||||
"name": "landscape-crop4x",
|
||||
"size": "1600w",
|
||||
},
|
||||
Object {
|
||||
"name": "landscape-crop6x",
|
||||
"size": "2400w",
|
||||
"key": "feat3",
|
||||
"label": "Feat 3",
|
||||
},
|
||||
]
|
||||
}
|
||||
noImageMessage={null}
|
||||
rootClassName={null}
|
||||
sizes="100vw"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<InjectIntl(ModalComponent)
|
||||
id="ListingPage.imageCarousel"
|
||||
isOpen={false}
|
||||
lightCloseButton={true}
|
||||
onClose={[Function]}
|
||||
onManageDisableScrolling={[Function]}
|
||||
>
|
||||
<InjectIntl(ImageCarousel)
|
||||
images={Array []}
|
||||
/>
|
||||
</InjectIntl(ModalComponent)>
|
||||
<div>
|
||||
<div>
|
||||
<NamedLink
|
||||
name="ListingPage"
|
||||
params={
|
||||
Object {
|
||||
"id": "listing1",
|
||||
"slug": "listing1-title",
|
||||
}
|
||||
}
|
||||
to={
|
||||
Object {
|
||||
"hash": "#host",
|
||||
}
|
||||
}
|
||||
>
|
||||
<AvatarLarge
|
||||
disableProfileLink={true}
|
||||
user={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "TT",
|
||||
"displayName": "user-1 display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user-1",
|
||||
},
|
||||
"type": "user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</NamedLink>
|
||||
<NamedLink
|
||||
name="ListingPage"
|
||||
params={
|
||||
Object {
|
||||
"id": "listing1",
|
||||
"slug": "listing1-title",
|
||||
}
|
||||
}
|
||||
to={
|
||||
Object {
|
||||
"hash": "#host",
|
||||
}
|
||||
}
|
||||
>
|
||||
<AvatarMedium
|
||||
disableProfileLink={true}
|
||||
user={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "TT",
|
||||
"displayName": "user-1 display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user-1",
|
||||
},
|
||||
"type": "user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</NamedLink>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
title={55}
|
||||
>
|
||||
55
|
||||
</div>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="ListingPage.perUnit"
|
||||
values={Object {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h1>
|
||||
<span>
|
||||
listing1
|
||||
|
||||
title
|
||||
</span>
|
||||
</h1>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="ListingPage.hostedBy"
|
||||
values={
|
||||
Object {
|
||||
"name": <NamedLink
|
||||
className={undefined}
|
||||
name="ListingPage"
|
||||
params={
|
||||
Object {
|
||||
"id": "listing1",
|
||||
"slug": "listing1-title",
|
||||
}
|
||||
}
|
||||
to={
|
||||
Object {
|
||||
"hash": "#host",
|
||||
}
|
||||
}
|
||||
>
|
||||
user-1 display name
|
||||
</NamedLink>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<span>
|
||||
<span>
|
||||
•
|
||||
</span>
|
||||
<InlineTextButton
|
||||
onClick={[Function]}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="ListingPage.contactUser"
|
||||
values={Object {}}
|
||||
/>
|
||||
</InlineTextButton>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="ListingPage.descriptionTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h2>
|
||||
<p>
|
||||
listing1
|
||||
|
||||
description
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="ListingPage.featuresTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h2>
|
||||
<PropertyGroup
|
||||
className={null}
|
||||
id="ListingPage.amenities"
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "towels",
|
||||
"label": "Towels",
|
||||
},
|
||||
Object {
|
||||
"key": "bathroom",
|
||||
"label": "Bathroom",
|
||||
},
|
||||
Object {
|
||||
"key": "swimming_pool",
|
||||
"label": "Swimming pool",
|
||||
},
|
||||
Object {
|
||||
"key": "own_drinks",
|
||||
"label": "Own drinks allowed",
|
||||
},
|
||||
Object {
|
||||
"key": "jacuzzi",
|
||||
"label": "Jacuzzi",
|
||||
},
|
||||
Object {
|
||||
"key": "audiovisual_entertainment",
|
||||
"label": "Audiovisual entertainment",
|
||||
},
|
||||
Object {
|
||||
"key": "barbeque",
|
||||
"label": "Barbeque",
|
||||
},
|
||||
Object {
|
||||
"key": "own_food_allowed",
|
||||
"label": "Own food allowed",
|
||||
},
|
||||
]
|
||||
}
|
||||
rootClassName={null}
|
||||
selectedOptions={Array []}
|
||||
twoColumns={true}
|
||||
/>
|
||||
</div>
|
||||
<SectionRulesMaybe
|
||||
className={null}
|
||||
publicData={Object {}}
|
||||
|
|
@ -349,56 +189,52 @@ exports[`ListingPage matches snapshot 1`] = `
|
|||
publicData={Object {}}
|
||||
rootClassName={null}
|
||||
/>
|
||||
<div>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="ListingPage.reviewsHeading"
|
||||
values={
|
||||
Object {
|
||||
"count": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</h2>
|
||||
<InjectIntl(ReviewsComponent)
|
||||
reviews={Array []}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
id="host"
|
||||
>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="ListingPage.yourHostHeading"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h2>
|
||||
<UserCard
|
||||
className={null}
|
||||
currentUser={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"email": "user-2@example.com",
|
||||
"emailVerified": true,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "user-2 abbreviated name",
|
||||
"displayName": "user-2 display name",
|
||||
"firstName": "user-2 first name",
|
||||
"lastName": "user-2 last name",
|
||||
},
|
||||
"stripeConnected": true,
|
||||
<SectionReviews
|
||||
fetchReviewsError={null}
|
||||
reviews={Array []}
|
||||
/>
|
||||
<SectionHost
|
||||
authorDisplayName="user-1 display name"
|
||||
currentUser={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"email": "user-2@example.com",
|
||||
"emailVerified": true,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "user-2 abbreviated name",
|
||||
"displayName": "user-2 display name",
|
||||
"firstName": "user-2 first name",
|
||||
"lastName": "user-2 last name",
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user-2",
|
||||
},
|
||||
"type": "currentUser",
|
||||
}
|
||||
"stripeConnected": true,
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user-2",
|
||||
},
|
||||
"type": "currentUser",
|
||||
}
|
||||
onContactUser={[Function]}
|
||||
rootClassName={null}
|
||||
user={
|
||||
Object {
|
||||
}
|
||||
isEnquiryModalOpen={false}
|
||||
isOwnListing={false}
|
||||
listing={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"deleted": false,
|
||||
"description": "listing1 description",
|
||||
"geolocation": LatLng {
|
||||
"lat": 40,
|
||||
"lng": 60,
|
||||
},
|
||||
"price": Money {
|
||||
"amount": 5500,
|
||||
"currency": "USD",
|
||||
},
|
||||
"publicData": Object {},
|
||||
"state": "published",
|
||||
"title": "listing1 title",
|
||||
},
|
||||
"author": Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
|
|
@ -410,114 +246,88 @@ exports[`ListingPage matches snapshot 1`] = `
|
|||
"uuid": "user-1",
|
||||
},
|
||||
"type": "user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<InjectIntl(ModalComponent)
|
||||
id="ListingPage.enquiry"
|
||||
isOpen={false}
|
||||
onClose={[Function]}
|
||||
onManageDisableScrolling={[Function]}
|
||||
>
|
||||
<ReduxForm
|
||||
authorDisplayName="user-1 display name"
|
||||
inProgress={false}
|
||||
listingTitle="listing1 title"
|
||||
onSubmit={[Function]}
|
||||
sendEnquiryError={null}
|
||||
/>
|
||||
</InjectIntl(ModalComponent)>
|
||||
</div>
|
||||
</div>
|
||||
<withViewport(ModalInMobileComponent)
|
||||
id="BookingDatesFormInModal"
|
||||
isModalOpenOnMobile={false}
|
||||
onClose={[Function]}
|
||||
onManageDisableScrolling={[Function]}
|
||||
showAsModalMaxWidth={1023}
|
||||
>
|
||||
<div>
|
||||
<h1>
|
||||
<span>
|
||||
listing1
|
||||
|
||||
title
|
||||
</span>
|
||||
</h1>
|
||||
<div>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="ListingPage.hostedBy"
|
||||
values={
|
||||
Object {
|
||||
"name": "user-1 display name",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="ListingPage.bookingTitle"
|
||||
values={
|
||||
Object {
|
||||
"title": <span>
|
||||
|
||||
listing1
|
||||
|
||||
title
|
||||
|
||||
</span>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</h2>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="ListingPage.bookingHelp"
|
||||
values={Object {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<BookingDatesForm
|
||||
isOwnListing={false}
|
||||
onSubmit={[Function]}
|
||||
price={
|
||||
Money {
|
||||
"amount": 5500,
|
||||
"currency": "USD",
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "listing1",
|
||||
},
|
||||
"images": Array [],
|
||||
"type": "listing",
|
||||
}
|
||||
}
|
||||
unitType="line-item/night"
|
||||
onCloseEnquiryModal={[Function]}
|
||||
onContactUser={[Function]}
|
||||
onManageDisableScrolling={[Function]}
|
||||
onSubmitEnquiry={[Function]}
|
||||
sendEnquiryError={null}
|
||||
sendEnquiryInProgress={false}
|
||||
title="listing1 title"
|
||||
/>
|
||||
</withViewport(ModalInMobileComponent)>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
title={55}
|
||||
>
|
||||
55
|
||||
</div>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="ListingPage.perUnit"
|
||||
values={Object {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className={null}
|
||||
disabled={false}
|
||||
inProgress={false}
|
||||
onClick={[Function]}
|
||||
ready={false}
|
||||
rootClassName={null}
|
||||
>
|
||||
ListingPage.ctaButtonMessage
|
||||
</Button>
|
||||
</div>
|
||||
<SectionBooking
|
||||
authorDisplayName="user-1 display name"
|
||||
formattedPrice={55}
|
||||
handleBookButtonClick={[Function]}
|
||||
handleBookingSubmit={[Function]}
|
||||
handleMobileBookModalClose={[Function]}
|
||||
isBook={false}
|
||||
isClosed={false}
|
||||
isOwnListing={false}
|
||||
listing={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"deleted": false,
|
||||
"description": "listing1 description",
|
||||
"geolocation": LatLng {
|
||||
"lat": 40,
|
||||
"lng": 60,
|
||||
},
|
||||
"price": Money {
|
||||
"amount": 5500,
|
||||
"currency": "USD",
|
||||
},
|
||||
"publicData": Object {},
|
||||
"state": "published",
|
||||
"title": "listing1 title",
|
||||
},
|
||||
"author": Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "TT",
|
||||
"displayName": "user-1 display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user-1",
|
||||
},
|
||||
"type": "user",
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "listing1",
|
||||
},
|
||||
"images": Array [],
|
||||
"type": "listing",
|
||||
}
|
||||
}
|
||||
onManageDisableScrolling={[Function]}
|
||||
price={
|
||||
Money {
|
||||
"amount": 5500,
|
||||
"currency": "USD",
|
||||
}
|
||||
}
|
||||
priceTitle={55}
|
||||
richTitle={
|
||||
<span>
|
||||
|
||||
listing1
|
||||
|
||||
title
|
||||
|
||||
</span>
|
||||
}
|
||||
unitType="line-item/night"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutWrapperMain>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue