-
Notifications
You must be signed in to change notification settings - Fork 379
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
697 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
@import '../../marketplace.css'; | ||
|
||
.root { | ||
display: flex; | ||
flex-direction: column; | ||
} | ||
|
||
.bookingDates { | ||
flex-shrink: 0; | ||
margin-bottom: 38px; | ||
|
||
/* Ensure that calendar dropdown gets some stacking context relative to other form items below */ | ||
z-index: 1; | ||
} | ||
|
||
.priceBreakdownContainer { | ||
padding: 0 24px; | ||
margin-bottom: 40px; | ||
|
||
@media (--viewportMedium) { | ||
padding: 0; | ||
margin-bottom: 0; | ||
margin-top: 1px; | ||
} | ||
} | ||
|
||
.priceBreakdownTitle { | ||
/* Font */ | ||
color: var(--matterColorAnti); | ||
|
||
margin-top: 5px; | ||
margin-bottom: 14px; | ||
@media (--viewportMedium) { | ||
margin-top: 5px; | ||
margin-bottom: 26px; | ||
} | ||
} | ||
|
||
.receipt { | ||
flex-shrink: 0; | ||
margin: 0 0 24px 0; | ||
} | ||
|
||
.error { | ||
color: var(--failColor); | ||
margin: 0 24px; | ||
display: inline-block; | ||
} | ||
|
||
.timeSlotsError { | ||
@apply --marketplaceH4FontStyles; | ||
color: var(--failColor); | ||
margin: 0 24px 12px 24px; | ||
|
||
@media (--viewportMedium) { | ||
margin: 0 0 12px 0; | ||
} | ||
} | ||
|
||
.smallPrint { | ||
@apply --marketplaceTinyFontStyles; | ||
color: var(--matterColorAnti); | ||
text-align: center; | ||
margin: auto 24px 20px 24px; | ||
flex-shrink: 0; | ||
|
||
@media (--viewportMedium) { | ||
margin-top: auto; | ||
margin-bottom: 20px; | ||
} | ||
|
||
@media (--viewportLarge) { | ||
margin-top: 4px; | ||
margin-bottom: 21px; | ||
} | ||
} | ||
|
||
.submitButtonWrapper { | ||
flex-shrink: 0; | ||
padding: 0 24px 24px 24px; | ||
width: 100%; | ||
|
||
@media (--viewportMedium) { | ||
padding: 0; | ||
width: 100%; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/* eslint-disable no-console */ | ||
import { types as sdkTypes } from '../../util/sdkLoader'; | ||
import { LINE_ITEM_NIGHT } from '../../util/types'; | ||
import BookingTimeForm from './BookingTimeForm'; | ||
|
||
const { Money } = sdkTypes; | ||
|
||
export const Form = { | ||
component: BookingTimeForm, | ||
props: { | ||
unitType: LINE_ITEM_NIGHT, | ||
onSubmit: values => { | ||
console.log('Submit BookingTimeForm with values:', values); | ||
}, | ||
price: new Money(1099, 'USD'), | ||
}, | ||
group: 'forms', | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,272 @@ | ||
import React, { Component } from 'react'; | ||
import { string, bool, arrayOf } from 'prop-types'; | ||
import { compose } from 'redux'; | ||
import { Form as FinalForm } from 'react-final-form'; | ||
import { FormattedMessage, intlShape, injectIntl } from '../../util/reactIntl'; | ||
import classNames from 'classnames'; | ||
import moment from 'moment'; | ||
import { required, bookingDatesRequired, composeValidators } from '../../util/validators'; | ||
import { START_DATE, END_DATE } from '../../util/dates'; | ||
import { propTypes } from '../../util/types'; | ||
import config from '../../config'; | ||
import { Form, PrimaryButton } from '../../components'; | ||
import EstimatedBreakdownMaybe from './EstimatedBreakdownMaybe'; | ||
import FieldDateAndTimeInput from './FieldDateAndTimeInput'; | ||
|
||
import css from './BookingTimeForm.css'; | ||
|
||
const identity = v => v; | ||
|
||
export class BookingTimeFormComponent extends Component { | ||
constructor(props) { | ||
super(props); | ||
this.state = { focusedInput: null }; | ||
this.handleFormSubmit = this.handleFormSubmit.bind(this); | ||
this.onFocusedInputChange = this.onFocusedInputChange.bind(this); | ||
} | ||
|
||
// Function that can be passed to nested components | ||
// so that they can notify this component when the | ||
// focused input changes. | ||
onFocusedInputChange(focusedInput) { | ||
this.setState({ focusedInput }); | ||
} | ||
|
||
// In case start or end date for the booking is missing | ||
// focus on that input, otherwise continue with the | ||
// default handleSubmit function. | ||
handleFormSubmit(e) { | ||
const { startDate, endDate } = e.bookingDates || {}; | ||
if (!startDate) { | ||
e.preventDefault(); | ||
this.setState({ focusedInput: START_DATE }); | ||
} else if (!endDate) { | ||
e.preventDefault(); | ||
this.setState({ focusedInput: END_DATE }); | ||
} else { | ||
this.props.onSubmit(e); | ||
} | ||
} | ||
|
||
render() { | ||
const { rootClassName, className, price: unitPrice, ...rest } = this.props; | ||
const classes = classNames(rootClassName || css.root, className); | ||
|
||
if (!unitPrice) { | ||
return ( | ||
<div className={classes}> | ||
<p className={css.error}> | ||
<FormattedMessage id="BookingTimeForm.listingPriceMissing" /> | ||
</p> | ||
</div> | ||
); | ||
} | ||
if (unitPrice.currency !== config.currency) { | ||
return ( | ||
<div className={classes}> | ||
<p className={css.error}> | ||
<FormattedMessage id="BookingTimeForm.listingCurrencyInvalid" /> | ||
</p> | ||
</div> | ||
); | ||
} | ||
|
||
return ( | ||
<FinalForm | ||
{...rest} | ||
unitPrice={unitPrice} | ||
onSubmit={this.handleFormSubmit} | ||
render={fieldRenderProps => { | ||
const { | ||
endDatePlaceholder, | ||
startDatePlaceholder, | ||
formId, | ||
handleSubmit, | ||
intl, | ||
isOwnListing, | ||
submitButtonWrapperClassName, | ||
unitPrice, | ||
unitType, | ||
values, | ||
timeSlots, | ||
fetchTimeSlotsError, | ||
} = fieldRenderProps; | ||
const { startDate, endDate } = values && values.bookingDates ? values.bookingDates : {}; | ||
|
||
const bookingStartLabel = intl.formatMessage({ | ||
id: 'BookingTimeForm.bookingStartTitle', | ||
}); | ||
const bookingEndLabel = intl.formatMessage({ id: 'BookingTimeForm.bookingEndTitle' }); | ||
const requiredMessage = intl.formatMessage({ id: 'BookingTimeForm.requiredDate' }); | ||
const startDateErrorMessage = intl.formatMessage({ | ||
id: 'FieldDateRangeInput.invalidStartDate', | ||
}); | ||
const endDateErrorMessage = intl.formatMessage({ | ||
id: 'FieldDateRangeInput.invalidEndDate', | ||
}); | ||
const timeSlotsError = fetchTimeSlotsError ? ( | ||
<p className={css.timeSlotsError}> | ||
<FormattedMessage id="BookingTimeForm.timeSlotsError" /> | ||
</p> | ||
) : null; | ||
|
||
// This is the place to collect breakdown estimation data. See the | ||
// EstimatedBreakdownMaybe component to change the calculations | ||
// for customized payment processes. | ||
const bookingData = | ||
startDate && endDate | ||
? { | ||
unitType, | ||
unitPrice, | ||
startDate, | ||
endDate, | ||
|
||
// NOTE: If unitType is `line-item/units`, a new picker | ||
// for the quantity should be added to the form. | ||
quantity: 1, | ||
} | ||
: null; | ||
const bookingInfo = bookingData ? ( | ||
<div className={css.priceBreakdownContainer}> | ||
<h3 className={css.priceBreakdownTitle}> | ||
<FormattedMessage id="BookingTimeForm.priceBreakdownTitle" /> | ||
</h3> | ||
<EstimatedBreakdownMaybe bookingData={bookingData} /> | ||
</div> | ||
) : null; | ||
|
||
const dateFormatOptions = { | ||
weekday: 'short', | ||
month: 'short', | ||
day: 'numeric', | ||
}; | ||
|
||
const now = moment(); | ||
const today = now.startOf('day').toDate(); | ||
const tomorrow = now | ||
.startOf('day') | ||
.add(1, 'days') | ||
.toDate(); | ||
const startDatePlaceholderText = | ||
startDatePlaceholder || intl.formatDate(today, dateFormatOptions); | ||
const endDatePlaceholderText = | ||
endDatePlaceholder || intl.formatDate(tomorrow, dateFormatOptions); | ||
const submitButtonClasses = classNames( | ||
submitButtonWrapperClassName || css.submitButtonWrapper | ||
); | ||
|
||
const startDateInputProps = { | ||
name: 'bookingStartDate', | ||
id: `${formId}.bookingStartDate`, | ||
label: bookingStartLabel, | ||
placeholderText: startDatePlaceholderText, | ||
focusedInput: this.state.focusedInput, | ||
onFocusedInputChange: this.onFocusedInputChange, | ||
format: identity, | ||
onBlur: () => console.log('onBlur called from DateInput props.'), | ||
onFocus: () => console.log('onFocus called from DateInput props.'), | ||
validate: composeValidators( | ||
required(requiredMessage), | ||
bookingDatesRequired(startDateErrorMessage, endDateErrorMessage) | ||
), | ||
}; | ||
const endDateInputProps = { | ||
name: 'bookingEndDate', | ||
id: `${formId}.bookingEndDate`, | ||
label: bookingEndLabel, | ||
placeholderText: endDatePlaceholderText, | ||
focusedInput: this.state.focusedInput, | ||
onFocusedInputChange: this.onFocusedInputChange, | ||
format: identity, | ||
timeSlots: timeSlots, | ||
onBlur: () => console.log('onBlur called from DateInput props.'), | ||
onFocus: () => console.log('onFocus called from DateInput props.'), | ||
validate: composeValidators( | ||
required(requiredMessage), | ||
bookingDatesRequired(startDateErrorMessage, endDateErrorMessage) | ||
), | ||
}; | ||
|
||
const startTimeInputProps = {}; | ||
const endTimeInputProps = {}; | ||
|
||
return ( | ||
<Form onSubmit={handleSubmit} className={classes}> | ||
{timeSlotsError} | ||
|
||
<FieldDateAndTimeInput | ||
className={css.bookingDates} | ||
name="bookingDates" | ||
unitType={unitType} | ||
focusedInput={this.state.focusedInput} | ||
onFocusedInputChange={this.onFocusedInputChange} | ||
format={identity} | ||
timeSlots={timeSlots} | ||
useMobileMargins | ||
intl={intl} | ||
validate={composeValidators( | ||
required(requiredMessage), | ||
bookingDatesRequired(startDateErrorMessage, endDateErrorMessage) | ||
)} | ||
startDateInputProps={startDateInputProps} | ||
endDateInputProps={endDateInputProps} | ||
startTimeInputProps={startTimeInputProps} | ||
endTimeInputProps={endTimeInputProps} | ||
values={values} | ||
/> | ||
{bookingInfo} | ||
<p className={css.smallPrint}> | ||
<FormattedMessage | ||
id={ | ||
isOwnListing | ||
? 'BookingTimeForm.ownListing' | ||
: 'BookingTimeForm.youWontBeChargedInfo' | ||
} | ||
/> | ||
</p> | ||
<div className={submitButtonClasses}> | ||
<PrimaryButton type="submit"> | ||
<FormattedMessage id="BookingTimeForm.requestToBook" /> | ||
</PrimaryButton> | ||
</div> | ||
</Form> | ||
); | ||
}} | ||
/> | ||
); | ||
} | ||
} | ||
|
||
BookingTimeFormComponent.defaultProps = { | ||
rootClassName: null, | ||
className: null, | ||
submitButtonWrapperClassName: null, | ||
price: null, | ||
isOwnListing: false, | ||
startDatePlaceholder: null, | ||
endDatePlaceholder: null, | ||
timeSlots: null, | ||
}; | ||
|
||
BookingTimeFormComponent.propTypes = { | ||
rootClassName: string, | ||
className: string, | ||
submitButtonWrapperClassName: string, | ||
|
||
unitType: propTypes.bookingUnitType.isRequired, | ||
price: propTypes.money, | ||
isOwnListing: bool, | ||
timeSlots: arrayOf(propTypes.timeSlot), | ||
|
||
// from injectIntl | ||
intl: intlShape.isRequired, | ||
|
||
// for tests | ||
startDatePlaceholder: string, | ||
endDatePlaceholder: string, | ||
}; | ||
|
||
const BookingTimeForm = compose(injectIntl)(BookingTimeFormComponent); | ||
BookingTimeForm.displayName = 'BookingTimeForm'; | ||
|
||
export default BookingTimeForm; |
Oops, something went wrong.