Merge pull request #969 from sharetribe/extract-section-booking

Extract booking section from listing page
This commit is contained in:
Hannu Lyytikäinen 2018-12-20 17:58:30 +02:00 committed by GitHub
commit f0534bd6a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 514 additions and 420 deletions

View file

@ -18,6 +18,8 @@ way to update this template, but currently, we follow a pattern:
[#985](https://github.com/sharetribe/flex-template-web/pull/985)
- [remove] Remove the default built-in email templates. Built-in email templates can be edited in
Console. [#983](https://github.com/sharetribe/flex-template-web/pull/983)
- [change] Extract SectionBooking to a distinct component from ListingPage.
[#969](https://github.com/sharetribe/flex-template-web/pull/969)
## [v2.5.0] 2018-12-17

View file

@ -0,0 +1,207 @@
@import '../../marketplace.css';
/* Booking form inside modalContainer needs special handling */
/* since "inMobile" breakpoint is actually --viewportLarge */
.modalContainer {
@apply --marketplaceModalInMobileBaseStyles;
height: 100vh;
@media (--viewportMedium) {
flex-basis: 576px;
height: 100%;
padding: var(--modalPaddingMedium);
background-color: var(--matterColorLight);
margin-top: 12.5vh;
margin-bottom: 12.5vh;
}
@media (--viewportLarge) {
padding: 0;
background-color: transparent;
margin-top: 0;
margin-bottom: 0;
}
}
.modalHeading {
margin-top: 89px;
margin-bottom: 36px;
padding: 0 24px;
@media (--viewportMedium) {
padding: 0;
}
@media (--viewportLarge) {
display: none;
}
}
.title {
/* Font */
@apply --marketplaceH1FontStyles;
color: var(--matterColor);
/* Layout */
width: 100%;
margin-top: 0;
margin-bottom: 0;
@media (--viewportMedium) {
margin-top: 0;
margin-bottom: 5px;
}
}
.author {
width: 100%;
@apply --marketplaceH4FontStyles;
margin-top: 7px;
margin-bottom: 0;
@media (--viewportMedium) {
margin-top: 0;
margin-bottom: 0;
color: var(--matterColor);
}
}
.bookingHeading {
display: none;
@media (--viewportLarge) {
display: block;
margin-top: -2px;
margin-bottom: 49px;
}
}
.bookingTitle {
@media (--viewportMedium) {
/* Font */
color: var(--matterColor);
margin-top: 0;
margin-bottom: 9px;
}
}
.bookingHelp {
display: none;
@apply --marketplaceH5FontStyles;
@media (--viewportMedium) {
color: var(--matterColor);
display: block;
margin-top: 0;
}
}
.bookingForm {
flex-grow: 1;
display: flex;
flex-direction: column;
margin: 0 0 84px 0;
@media (--viewportMedium) {
margin: 0;
min-height: 400px;
}
@media (--viewportLarge) {
min-width: 312px;
min-height: auto;
}
}
.bookingDatesSubmitButtonWrapper {
flex-shrink: 0;
padding: 0 24px 24px 24px;
width: 100%;
position: fixed;
bottom: 0;
background-color: var(--matterColorLight);
@media (--viewportMedium) {
padding: 0;
width: 100%;
position: static;
bottom: unset;
background-color: transparent;
}
}
.openBookingForm {
/* Ensure that mobile button is over Footer too */
z-index: 9;
position: fixed;
bottom: 0;
left: 0;
width: 100vw;
padding: 18px 24px 18px 16px;
background-color: var(--matterColorLight);
box-shadow: var(--boxShadowTop);
display: flex;
/* Contain repainting to this component only */
/* 3D painting container helps scrolling */
transform: translate3d(0, 0, 0);
@media (--viewportMedium) {
padding: 18px 60px 18px 60px;
}
@media (--viewportLarge) {
display: none;
}
}
.priceContainer {
/* Layout */
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
margin-right: 22px;
padding: 5px 12px;
}
.priceValue {
/* Font */
@apply --marketplaceH2FontStyles;
color: var(--matterColor);
margin-top: 0;
margin-bottom: 0px;
@media (--viewportMedium) {
margin-top: 0;
margin-bottom: 0;
}
}
.perUnit {
/* Font */
@apply --marketplaceTinyFontStyles;
color: var(--matterColor);
margin-top: 0;
margin-bottom: 0px;
@media (--viewportMedium) {
margin-top: 0;
margin-bottom: 0;
}
}
.bookButton {
@apply --marketplaceButtonStylesPrimary;
/* Clear padding that is set for link elements looking like buttons */
padding: 0;
}
.closedListingButton {
border-left: 1px solid var(--matterColorNegative);
width: 100%;
padding: 15px 24px 15px 24px;
text-align: center;
}

View file

@ -0,0 +1,3 @@
.example {
width: 312px;
}

View file

@ -0,0 +1,31 @@
import React from 'react';
import { createListing } from '../../util/test-data';
import { LISTING_STATE_CLOSED } from '../../util/types';
import BookingPanel from './BookingPanel';
import css from './BookingPanel.example.css';
export const Default = {
component: BookingPanel,
props: {
className: css.example,
listing: createListing('listing_1'),
handleBookingSubmit: values => console.log('Submit:', values),
title: <span>Booking title</span>,
subTitle: 'Hosted by Author N',
authorDisplayName: 'Author Name',
onManageDisableScrolling: () => null,
},
};
export const WithClosedListing = {
component: BookingPanel,
props: {
className: css.example,
listing: createListing('listing_1', { state: LISTING_STATE_CLOSED }),
handleBookingSubmit: values => console.log('Submit:', values),
title: <span>Booking title</span>,
subTitle: 'Hosted by Author N',
authorDisplayName: 'Author Name',
onManageDisableScrolling: () => null,
},
};

View file

@ -0,0 +1,193 @@
import React from 'react';
import { compose } from 'redux';
import { withRouter } from 'react-router-dom';
import { intlShape, injectIntl } from 'react-intl';
import { arrayOf, bool, func, node, oneOfType, shape, string } from 'prop-types';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import omit from 'lodash/omit';
import { propTypes, LISTING_STATE_CLOSED, LINE_ITEM_NIGHT, LINE_ITEM_DAY } from '../../util/types';
import { formatMoney } from '../../util/currency';
import { parse, stringify } from '../../util/urlHelpers';
import config from '../../config';
import { ModalInMobile, Button } from '../../components';
import { BookingDatesForm } from '../../forms';
import css from './BookingPanel.css';
// This defines when ModalInMobile shows content as Modal
const MODAL_BREAKPOINT = 1023;
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 {};
};
const openBookModal = (isOwnListing, isClosed, history, location) => {
if (isOwnListing || isClosed) {
window.scrollTo(0, 0);
} else {
const { pathname, search, state } = location;
const searchString = `?${stringify({ ...parse(search), book: true })}`;
history.push(`${pathname}${searchString}`, state);
}
};
const closeBookModal = (history, location) => {
const { pathname, search, state } = location;
const searchParams = omit(parse(search), 'book');
const searchString = `?${stringify(searchParams)}`;
history.push(`${pathname}${searchString}`, state);
};
const BookingPanel = props => {
const {
rootClassName,
className,
listing,
isOwnListing,
unitType,
handleBookingSubmit,
title,
subTitle,
authorDisplayName,
onManageDisableScrolling,
timeSlots,
fetchTimeSlotsError,
history,
location,
intl,
} = props;
const price = listing.attributes.price;
const isClosed = listing.attributes.state === LISTING_STATE_CLOSED;
const showClosedListingHelpText = listing.id && isClosed;
const { formattedPrice, priceTitle } = priceData(price, intl);
const isBook = !!parse(location.search).book;
const subTitleText = !!subTitle
? subTitle
: showClosedListingHelpText
? intl.formatMessage({ id: 'BookingPanel.subTitleClosedListing' })
: null;
const isNightly = unitType === LINE_ITEM_NIGHT;
const isDaily = unitType === LINE_ITEM_DAY;
const unitTranslationKey = isNightly
? 'BookingPanel.perNight'
: isDaily
? 'BookingPanel.perDay'
: 'BookingPanel.perUnit';
const classes = classNames(rootClassName || css.root, className);
return (
<div className={classes}>
<ModalInMobile
containerClassName={css.modalContainer}
id="BookingDatesFormInModal"
isModalOpenOnMobile={isBook}
onClose={() => closeBookModal(history, location)}
showAsModalMaxWidth={MODAL_BREAKPOINT}
onManageDisableScrolling={onManageDisableScrolling}
>
<div className={css.modalHeading}>
<h1 className={css.title}>{title}</h1>
<div className={css.author}>
<FormattedMessage id="BookingPanel.hostedBy" values={{ name: authorDisplayName }} />
</div>
</div>
<div className={css.bookingHeading}>
<h2 className={css.bookingTitle}>{title}</h2>
{subTitleText ? <div className={css.bookingHelp}>{subTitleText}</div> : null}
</div>
{!isClosed ? (
<BookingDatesForm
className={css.bookingForm}
submitButtonWrapperClassName={css.bookingDatesSubmitButtonWrapper}
unitType={unitType}
onSubmit={handleBookingSubmit}
price={price}
isOwnListing={isOwnListing}
timeSlots={timeSlots}
fetchTimeSlotsError={fetchTimeSlotsError}
/>
) : null}
</ModalInMobile>
<div className={css.openBookingForm}>
<div className={css.priceContainer}>
<div className={css.priceValue} title={priceTitle}>
{formattedPrice}
</div>
<div className={css.perUnit}>
<FormattedMessage id={unitTranslationKey} />
</div>
</div>
{!isClosed ? (
<Button
rootClassName={css.bookButton}
onClick={() => openBookModal(isOwnListing, isClosed, history, location)}
>
<FormattedMessage id="BookingPanel.ctaButtonMessage" />
</Button>
) : (
<div className={css.closedListingButton}>
<FormattedMessage id="BookingPanel.closedListingButtonText" />
</div>
)}
</div>
</div>
);
};
BookingPanel.defaultProps = {
rootClassName: null,
className: null,
isOwnListing: false,
subTitle: null,
unitType: config.bookingUnitType,
timeSlots: null,
fetchTimeSlotsError: null,
};
BookingPanel.propTypes = {
rootClassName: string,
className: string,
listing: propTypes.listing.isRequired,
isOwnListing: bool,
unitType: propTypes.bookingUnitType,
handleBookingSubmit: func.isRequired,
title: oneOfType([node, string]).isRequired,
subTitle: oneOfType([node, string]),
authorDisplayName: string.isRequired,
onManageDisableScrolling: func.isRequired,
timeSlots: arrayOf(propTypes.timeSlot),
fetchTimeSlotsError: propTypes.error,
// from withRouter
history: shape({
push: func.isRequired,
}).isRequired,
location: shape({
search: string,
}).isRequired,
// from injectIntl
intl: intlShape.isRequired,
};
export default compose(
withRouter,
injectIntl
)(BookingPanel);

View file

@ -110,6 +110,10 @@
font-weight: 400;
}
& :global(.DayPicker__withBorder) {
border-radius: 0;
}
& :global(.CalendarMonth_caption) {
color: var(--matterColorLight);
@apply --marketplaceH2FontStyles;

View file

@ -23,6 +23,10 @@
margin-bottom: 0;
font-weight: 600;
}
@media (--viewportMedium) {
padding-bottom: 1px;
}
}
.labelSuccess {

View file

@ -99,11 +99,7 @@ export const ListingCardComponent = props => {
})}
</div>
<div className={css.authorInfo}>
<FormattedMessage
className={css.authorName}
id="ListingCard.hostedBy"
values={{ authorName }}
/>
<FormattedMessage id="ListingCard.hostedBy" values={{ authorName }} />
</div>
</div>
</div>

View file

@ -3,6 +3,7 @@ export { default as AddImages } from './AddImages/AddImages';
export { default as Avatar, AvatarMedium, AvatarLarge } from './Avatar/Avatar';
export { default as BookingBreakdown } from './BookingBreakdown/BookingBreakdown';
export { default as Button, PrimaryButton, SecondaryButton, InlineTextButton } from './Button/Button';
export { default as BookingPanel } from './BookingPanel/BookingPanel';
export { default as CookieConsent } from './CookieConsent/CookieConsent';
export { default as Discussion } from './Discussion/Discussion';
export { default as EditListingDescriptionPanel } from './EditListingDescriptionPanel/EditListingDescriptionPanel';

View file

@ -393,12 +393,10 @@ export class CheckoutPageComponent extends Component {
<div className={css.heading}>
<h1 className={css.title}>{title}</h1>
<div className={css.author}>
<span className={css.authorName}>
<FormattedMessage
id="ListingPage.hostedBy"
values={{ name: currentAuthor.attributes.profile.displayName }}
/>
</span>
<FormattedMessage
id="CheckoutPage.hostedBy"
values={{ name: currentAuthor.attributes.profile.displayName }}
/>
</div>
</div>

View file

@ -65,16 +65,14 @@ exports[`CheckoutPage matches snapshot 1`] = `
CheckoutPage.title
</h1>
<div>
<span>
<FormattedMessage
id="ListingPage.hostedBy"
values={
Object {
"name": "author display name",
}
<FormattedMessage
id="CheckoutPage.hostedBy"
values={
Object {
"name": "author display name",
}
/>
</span>
}
/>
</div>
</div>
<div>

View file

@ -1,5 +1,8 @@
@import '../../marketplace.css';
.root {
}
.pageRoot {
display: flex;
padding-bottom: 90px;
@ -213,8 +216,26 @@
@media (--viewportLarge) {
margin-top: 50px;
margin-bottom: 0px;
/* contentContainer's full width - (bookingColumn + gutters) */
flex-basis: calc(100% - 491px);
/* contentContainer's full width - (bookingColumn + two paddings + margin + border) */
flex-basis: calc(100% - 493px);
flex-shrink: 0;
}
}
.bookingPanel {
@media (--viewportLarge) {
display: block;
margin-top: 87px;
margin-left: 60px;
border-left-style: solid;
border-left-width: 1px;
border-left-color: var(--matterColorNegative);
padding-left: 60px;
padding-right: 60px;
/* The calendar needs 312px */
flex-basis: 312px;
flex-shrink: 0;
}
}
@ -548,200 +569,6 @@
}
}
.openBookingForm {
/* Ensure that mobile button is over Footer too */
z-index: 9;
position: fixed;
bottom: 0;
left: 0;
width: 100vw;
padding: 18px 24px 18px 16px;
background-color: var(--matterColorLight);
box-shadow: var(--boxShadowTop);
display: flex;
/* Contain repainting to this component only */
/* 3D painting container helps scrolling */
transform: translate3d(0, 0, 0);
@media (--viewportMedium) {
padding: 18px 60px 18px 60px;
}
@media (--viewportLarge) {
display: none;
}
}
.priceContainer {
/* Layout */
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
margin-right: 22px;
padding: 5px 12px;
}
.priceValue {
/* Font */
@apply --marketplaceH2FontStyles;
color: var(--matterColor);
margin-top: 0;
margin-bottom: 0px;
@media (--viewportMedium) {
margin-top: 0;
margin-bottom: 0;
}
}
.perUnit {
/* Font */
@apply --marketplaceTinyFontStyles;
color: var(--matterColor);
margin-top: 0;
margin-bottom: 0px;
@media (--viewportMedium) {
margin-top: 0;
margin-bottom: 0;
}
}
.modalInMobile {
@media (--viewportLarge) {
display: block;
margin-top: 87px;
margin-left: 60px;
border-left-style: solid;
border-left-width: 1px;
border-left-color: var(--matterColorNegative);
/* Calendar needs 311px width and therefore padding can't be wider than 58px */
padding-left: 60px;
padding-right: 60px;
flex-basis: 311px;
flex-shrink: 0;
}
}
/* Booking form inside modalContainer needs special handling */
/* since "inMobile" breakpoint is actually --viewportLarge */
.modalContainer {
@apply --marketplaceModalInMobileBaseStyles;
height: 100vh;
@media (--viewportMedium) {
flex-basis: 576px;
height: 100%;
padding: var(--modalPaddingMedium);
background-color: var(--matterColorLight);
margin-top: 12.5vh;
margin-bottom: 12.5vh;
}
@media (--viewportLarge) {
padding: 0;
background-color: transparent;
margin-top: 0;
margin-bottom: 0;
}
}
.modalHeading {
margin-top: 89px;
margin-bottom: 36px;
padding: 0 24px;
@media (--viewportMedium) {
padding: 0;
}
@media (--viewportLarge) {
display: none;
}
}
.bookingHeading {
display: none;
@media (--viewportLarge) {
display: block;
margin-top: 6px;
margin-bottom: 47px;
}
}
.bookingTitle {
@media (--viewportMedium) {
/* Font */
color: var(--matterColor);
margin-top: 0;
margin-bottom: 9px;
}
}
.bookingHelp {
display: none;
@apply --marketplaceH5FontStyles;
@media (--viewportMedium) {
color: var(--matterColor);
display: block;
margin-top: 0;
}
}
.bookingForm {
flex-grow: 1;
display: flex;
flex-direction: column;
margin: 0 0 84px 0;
@media (--viewportMedium) {
margin: 0;
min-height: 400px;
}
@media (--viewportLarge) {
min-width: 311px;
min-height: auto;
}
}
.bookingDatesSubmitButtonWrapper {
flex-shrink: 0;
padding: 0 24px 24px 24px;
width: 100%;
position: fixed;
bottom: 0;
background-color: var(--matterColorLight);
@media (--viewportMedium) {
padding: 0;
width: 100%;
position: static;
bottom: unset;
background-color: transparent;
}
}
.bookButton {
@apply --marketplaceButtonStylesPrimary;
/* Clear padding that is set for link elements looking like buttons */
padding: 0;
}
.closedListingButton {
border-left: 1px solid var(--matterColorNegative);
width: 100%;
padding: 15px 24px 15px 24px;
text-align: center;
}
.longWord {
/* fallback option */
word-break: break-all;

View file

@ -15,7 +15,6 @@ import {
LISTING_PAGE_PARAM_TYPE_DRAFT,
LISTING_PAGE_PARAM_TYPE_EDIT,
createSlug,
parse,
} from '../../util/urlHelpers';
import { formatMoney } from '../../util/currency';
import { createResourceLocatorString, findRouteByRouteName } from '../../util/routes';
@ -32,6 +31,7 @@ import {
LayoutWrapperMain,
LayoutWrapperFooter,
Footer,
BookingPanel,
} from '../../components';
import { TopbarContainer, NotFoundPage } from '../../containers';
@ -45,7 +45,6 @@ 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';
const MIN_LENGTH_FOR_LONG_WORDS_IN_TITLE = 16;
@ -65,38 +64,6 @@ const priceData = (price, intl) => {
return {};
};
const openBookModal = (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) },
{ book: true }
)
);
};
const closeBookModal = (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) },
{}
)
);
};
const categoryLabel = (categories, key) => {
const cat = categories.find(c => c.key === key);
return cat ? cat.label : key;
@ -199,7 +166,6 @@ export class ListingPageComponent extends Component {
location,
scrollingDisabled,
showListingError,
history,
reviews,
fetchReviewsError,
sendEnquiryInProgress,
@ -210,7 +176,6 @@ export class ListingPageComponent extends Component {
amenitiesConfig,
} = this.props;
const isBook = !!parse(location.search).book;
const listingId = new UUID(rawParams.id);
const isPendingApprovalVariant = rawParams.variant === LISTING_PAGE_PENDING_APPROVAL_VARIANT;
const isDraftVariant = rawParams.variant === LISTING_PAGE_DRAFT_VARIANT;
@ -264,6 +229,12 @@ export class ListingPageComponent extends Component {
</span>
);
const bookingTitle = intl.formatMessage(
{ id: 'ListingPage.bookingSubTitle' },
{ title: richTitle }
);
const bookingSubTitle = intl.formatMessage({ id: 'ListingPage.bookingSubTitle' });
const topbar = <TopbarContainer />;
if (showListingError && showListingError.status === 404) {
@ -328,7 +299,6 @@ export class ListingPageComponent extends Component {
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;
@ -341,10 +311,6 @@ export class ListingPageComponent extends Component {
const { formattedPrice, priceTitle } = priceData(price, intl);
const handleMobileBookModalClose = () => {
closeBookModal(history, currentListing);
};
const handleBookingSubmit = values => {
const isCurrentlyClosed = currentListing.attributes.state === LISTING_STATE_CLOSED;
if (isOwnListing || isCurrentlyClosed) {
@ -354,15 +320,6 @@ export class ListingPageComponent extends Component {
}
};
const handleBookButtonClick = () => {
const isCurrentlyClosed = currentListing.attributes.state === LISTING_STATE_CLOSED;
if (isOwnListing || isCurrentlyClosed) {
window.scrollTo(0, 0);
} else {
openBookModal(history, currentListing);
}
};
const listingImages = (listing, variantName) =>
(listing.images || [])
.map(image => {
@ -480,20 +437,15 @@ export class ListingPageComponent extends Component {
onManageDisableScrolling={onManageDisableScrolling}
/>
</div>
<SectionBooking
<BookingPanel
className={css.bookingPanel}
listing={currentListing}
isOwnListing={isOwnListing}
isClosed={isClosed}
isBook={isBook}
unitType={unitType}
price={price}
formattedPrice={formattedPrice}
priceTitle={priceTitle}
handleBookingSubmit={handleBookingSubmit}
richTitle={richTitle}
title={bookingTitle}
subTitle={bookingSubTitle}
authorDisplayName={authorDisplayName}
handleBookButtonClick={handleBookButtonClick}
handleMobileBookModalClose={handleMobileBookModalClose}
onManageDisableScrolling={onManageDisableScrolling}
timeSlots={timeSlots}
fetchTimeSlotsError={fetchTimeSlotsError}

View file

@ -1,113 +0,0 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { ModalInMobile, Button } from '../../components';
import { BookingDatesForm } from '../../forms';
import { LINE_ITEM_NIGHT, LINE_ITEM_DAY } from '../../util/types';
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,
timeSlots,
fetchTimeSlotsError,
} = props;
const showClosedListingHelpText = listing.id && isClosed;
const isNightly = unitType === LINE_ITEM_NIGHT;
const isDaily = unitType === LINE_ITEM_DAY;
const unitTranslationKey = isNightly
? 'ListingPage.perNight'
: isDaily
? 'ListingPage.perDay'
: 'ListingPage.perUnit';
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}
timeSlots={timeSlots}
fetchTimeSlotsError={fetchTimeSlotsError}
/>
) : null}
</ModalInMobile>
<div className={css.openBookingForm}>
<div className={css.priceContainer}>
<div className={css.priceValue} title={priceTitle}>
{formattedPrice}
</div>
<div className={css.perUnit}>
<FormattedMessage id={unitTranslationKey} />
</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;

View file

@ -262,15 +262,10 @@ exports[`ListingPage matches snapshot 1`] = `
title="listing1 title"
/>
</div>
<SectionBooking
<withRouter(InjectIntl(BookingPanel))
authorDisplayName="user-1 display name"
fetchTimeSlotsError={null}
formattedPrice={55}
handleBookButtonClick={[Function]}
handleBookingSubmit={[Function]}
handleMobileBookModalClose={[Function]}
isBook={false}
isClosed={false}
isOwnListing={false}
listing={
Object {
@ -310,23 +305,9 @@ exports[`ListingPage matches snapshot 1`] = `
}
}
onManageDisableScrolling={[Function]}
price={
Money {
"amount": 5500,
"currency": "USD",
}
}
priceTitle={55}
richTitle={
<span>
listing1
title
</span>
}
subTitle="ListingPage.bookingSubTitle"
timeSlots={null}
title="ListingPage.bookingSubTitle"
unitType="line-item/night"
/>
</div>

View file

@ -3,6 +3,7 @@ import * as ActivityFeed from './components/ActivityFeed/ActivityFeed.example';
import * as AddImages from './components/AddImages/AddImages.example';
import * as Avatar from './components/Avatar/Avatar.example';
import * as BookingBreakdown from './components/BookingBreakdown/BookingBreakdown.example';
import * as BookingPanel from './components/BookingPanel/BookingPanel.example';
import * as Button from './components/Button/Button.example';
import * as ExpandingTextarea from './components/ExpandingTextarea/ExpandingTextarea.example';
import * as FieldBirthdayInput from './components/FieldBirthdayInput/FieldBirthdayInput.example';
@ -89,6 +90,7 @@ export {
Avatar,
BookingBreakdown,
BookingDatesForm,
BookingPanel,
Button,
Colors,
EditListingDescriptionForm,

View file

@ -71,7 +71,7 @@
@media (--viewportLarge) {
margin-top: 4px;
margin-bottom: 22px;
margin-bottom: 21px;
}
}

View file

@ -67,6 +67,13 @@
"BookingDatesForm.requiredDate": "Oops, make sure your date is correct!",
"BookingDatesForm.timeSlotsError": "Loading listing availability failed. Please refresh the page.",
"BookingDatesForm.youWontBeChargedInfo": "You won't be charged yet",
"BookingPanel.closedListingButtonText": "Sorry, this listing has been closed.",
"BookingPanel.ctaButtonMessage": "Request to book",
"BookingPanel.hostedBy": "Hosted by {name}",
"BookingPanel.perDay": "per day",
"BookingPanel.perNight": "per night",
"BookingPanel.perUnit": "per unit",
"BookingPanel.subTitleClosedListing": "Sorry, this listing has been closed.",
"CheckoutPage.bookingTimeNotAvailableMessage": "Unfortunately, the requested time is already booked.",
"CheckoutPage.errorlistingLinkText": "the sauna page",
"CheckoutPage.goToLandingPage": "Go to homepage",
@ -287,13 +294,10 @@
"ListingCard.unsupportedPrice": "({currency})",
"ListingCard.unsupportedPriceTitle": "Unsupported currency ({currency})",
"ListingPage.bannedUserDisplayName": "Banned user",
"ListingPage.bookingHelp": "Start by choosing your dates.",
"ListingPage.bookingHelpClosedListing": "Sorry, this listing has been closed.",
"ListingPage.bookingSubTitle": "Start by choosing your dates.",
"ListingPage.bookingTitle": "Book {title}",
"ListingPage.closedListing": "This listing has been closed and can't be booked.",
"ListingPage.closedListingButtonText": "Sorry, this listing has been closed.",
"ListingPage.contactUser": "Contact",
"ListingPage.ctaButtonMessage": "Request to book",
"ListingPage.descriptionTitle": "About this sauna",
"ListingPage.editListing": "Edit listing",
"ListingPage.editProfileLink": "Edit profile",

View file

@ -67,6 +67,13 @@
"BookingDatesForm.requiredDate": "Oups, vérifiez que la date est correcte",
"BookingDatesForm.timeSlotsError": "Le chargement des disponibilités pour cette annonce a échoué. Veuillez rafraîchir la page.",
"BookingDatesForm.youWontBeChargedInfo": "Vous ne serez pas facturé immédiatement",
"BookingPanel.closedListingButtonText": "Navré, cette annonce est close.",
"BookingPanel.ctaButtonMessage": "Réserver",
"BookingPanel.hostedBy": "Proposé par {name}",
"BookingPanel.perDay": "par jour",
"BookingPanel.perNight": "par nuit",
"BookingPanel.perUnit": "par unité",
"BookingPanel.subTitleClosedListing": "Navré, cette annonce est close.",
"CheckoutPage.bookingTimeNotAvailableMessage": "Hélas, le créneau souhaité est déjà réservé.",
"CheckoutPage.errorlistingLinkText": "la page de l'annonce",
"CheckoutPage.goToLandingPage": "Aller sur la page d'accueil",
@ -287,13 +294,10 @@
"ListingCard.unsupportedPrice": "({currency})",
"ListingCard.unsupportedPriceTitle": "Devise non supportée ({currency})",
"ListingPage.bannedUserDisplayName": "Utilisateur banni",
"ListingPage.bookingHelp": "À quelle date souhaiteriez-vous réserver ?",
"ListingPage.bookingHelpClosedListing": "Navré, cette annonce est close.",
"ListingPage.bookingSubTitle": "À quelle date souhaiteriez-vous réserver ?",
"ListingPage.bookingTitle": "Réserver {title}",
"ListingPage.closedListing": "Cette annonce est close et ne peut être réservée.",
"ListingPage.closedListingButtonText": "Navré, cette annonce est close.",
"ListingPage.contactUser": "Envoyer un message",
"ListingPage.ctaButtonMessage": "Réserver",
"ListingPage.descriptionTitle": "À propos de ce sauna",
"ListingPage.editListing": "Modifier l'annonce",
"ListingPage.editProfileLink": "Modifier le profil",
@ -796,4 +800,4 @@
"UserCard.heading": "Bonjour, je suis {name}.",
"UserCard.showFullBioLink": "plus",
"UserCard.viewProfileLink": "Voir le profil"
}
}