mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-26 06:47:17 +10:00
Merge pull request #969 from sharetribe/extract-section-booking
Extract booking section from listing page
This commit is contained in:
commit
f0534bd6a5
19 changed files with 514 additions and 420 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
207
src/components/BookingPanel/BookingPanel.css
Normal file
207
src/components/BookingPanel/BookingPanel.css
Normal 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;
|
||||
}
|
||||
3
src/components/BookingPanel/BookingPanel.example.css
Normal file
3
src/components/BookingPanel/BookingPanel.example.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.example {
|
||||
width: 312px;
|
||||
}
|
||||
31
src/components/BookingPanel/BookingPanel.example.js
Normal file
31
src/components/BookingPanel/BookingPanel.example.js
Normal 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,
|
||||
},
|
||||
};
|
||||
193
src/components/BookingPanel/BookingPanel.js
Normal file
193
src/components/BookingPanel/BookingPanel.js
Normal 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);
|
||||
|
|
@ -110,6 +110,10 @@
|
|||
font-weight: 400;
|
||||
}
|
||||
|
||||
& :global(.DayPicker__withBorder) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
& :global(.CalendarMonth_caption) {
|
||||
color: var(--matterColorLight);
|
||||
@apply --marketplaceH2FontStyles;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@
|
|||
margin-bottom: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (--viewportMedium) {
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.labelSuccess {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@
|
|||
|
||||
@media (--viewportLarge) {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 22px;
|
||||
margin-bottom: 21px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue