mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-28 12:43:11 +10:00
commit
d0581861c4
37 changed files with 554 additions and 127 deletions
18
src/components/BookingInfo/BookingInfo.css
Normal file
18
src/components/BookingInfo/BookingInfo.css
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.totalDivider {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.totalPrice {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
import { types } from '../../util/sdkLoader';
|
||||
import BookingInfo from './BookingInfo';
|
||||
|
||||
export const Empty = {
|
||||
component: BookingInfo,
|
||||
props: {
|
||||
pricePerDay: '55\u20AC',
|
||||
bookingPeriod: 'Jan 2nd - Jan 4th',
|
||||
bookingDuration: '3 days',
|
||||
total: '165\u20AC',
|
||||
unitPrice: new types.Money(10, 'USD'),
|
||||
bookingStart: new Date('Fri, 14 Apr 2017 GMT'),
|
||||
bookingEnd: new Date('Sun, 16 Apr 2017 GMT'),
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,32 +1,91 @@
|
|||
/**
|
||||
* This component will show the booking info and calculated total price.
|
||||
* I.e. dates and other details related to payment decision in receipt format.
|
||||
*/
|
||||
import React, { PropTypes } from 'react';
|
||||
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
|
||||
import Decimal from 'decimal.js';
|
||||
import moment from 'moment';
|
||||
import classNames from 'classnames';
|
||||
import config from '../../config';
|
||||
import { convertMoneyToNumber } from '../../util/currency';
|
||||
import { types } from '../../util/sdkLoader';
|
||||
import css from './BookingInfo.css';
|
||||
|
||||
const BookingInfoComponent = props => {
|
||||
const { bookingStart, bookingEnd, className, intl, unitPrice } = props;
|
||||
|
||||
const hasSelectedDays = bookingStart && bookingEnd;
|
||||
const bookingPeriod = hasSelectedDays
|
||||
? <FormattedMessage
|
||||
id="BookingInfo.bookingPeriod"
|
||||
values={{
|
||||
bookingStart: moment(bookingStart).format('L'),
|
||||
bookingEnd: moment(bookingEnd).format('L'),
|
||||
}}
|
||||
/>
|
||||
: '';
|
||||
const nightCount = hasSelectedDays ? moment(bookingEnd).diff(moment(bookingStart), 'days') : null;
|
||||
const nightCountMessage = nightCount
|
||||
? <FormattedMessage id="BookingInfo.nightCount" values={{ count: nightCount }} />
|
||||
: null;
|
||||
|
||||
const priceAsNumber = convertMoneyToNumber(unitPrice, config.currencyConfig.subUnitDivisor);
|
||||
const formattedPrice = intl.formatNumber(priceAsNumber, config.currencyConfig);
|
||||
const totalPriceAsNumber = hasSelectedDays
|
||||
? new Decimal(priceAsNumber).times(nightCount).toNumber()
|
||||
: null;
|
||||
const formattedTotalPrice = hasSelectedDays
|
||||
? intl.formatNumber(totalPriceAsNumber, config.currencyConfig)
|
||||
: null;
|
||||
|
||||
const BookingInfo = props => {
|
||||
const { pricePerDay, bookingPeriod, bookingDuration, total } = props;
|
||||
return (
|
||||
<dl>
|
||||
<dt>
|
||||
Price per day:
|
||||
</dt>
|
||||
<dd>{pricePerDay}</dd>
|
||||
<dt>
|
||||
Booking period:
|
||||
</dt>
|
||||
<dd>{`${bookingPeriod}, ${bookingDuration}`}</dd>
|
||||
<dt>
|
||||
Total
|
||||
</dt>
|
||||
<dd>{total}</dd>
|
||||
</dl>
|
||||
<div className={classNames(css.container, className)}>
|
||||
<div className={css.row}>
|
||||
<div className={css.priceUnitLabel}>
|
||||
<FormattedMessage id="BookingInfo.pricePerDay" />
|
||||
</div>
|
||||
<div className={css.priceUnitPrice}>
|
||||
{formattedPrice}
|
||||
</div>
|
||||
</div>
|
||||
<div className={css.row}>
|
||||
<div className={css.bookingPeriodLabel}>
|
||||
<FormattedMessage id="BookingInfo.bookingPeriodLabel" />
|
||||
<br />
|
||||
{bookingPeriod}
|
||||
</div>
|
||||
<div className={css.bookedDatesCount}>
|
||||
{nightCountMessage}
|
||||
</div>
|
||||
</div>
|
||||
<hr className={css.totalDivider} />
|
||||
<div className={css.row}>
|
||||
<div className={css.totalLabel}>
|
||||
<FormattedMessage id="BookingInfo.total" />
|
||||
</div>
|
||||
<div className={css.totalPrice}>
|
||||
{formattedTotalPrice}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const { string } = PropTypes;
|
||||
BookingInfoComponent.defaultProps = { bookingStart: null, bookingEnd: null, className: '' };
|
||||
|
||||
BookingInfo.propTypes = {
|
||||
pricePerDay: string.isRequired,
|
||||
bookingPeriod: string.isRequired,
|
||||
bookingDuration: string.isRequired,
|
||||
total: string.isRequired,
|
||||
const { instanceOf, string } = PropTypes;
|
||||
|
||||
BookingInfoComponent.propTypes = {
|
||||
bookingStart: instanceOf(Date),
|
||||
bookingEnd: instanceOf(Date),
|
||||
className: string,
|
||||
intl: intlShape.isRequired,
|
||||
unitPrice: instanceOf(types.Money).isRequired,
|
||||
};
|
||||
|
||||
const BookingInfo = injectIntl(BookingInfoComponent);
|
||||
|
||||
BookingInfo.displayName = 'BookingInfo';
|
||||
|
||||
export default BookingInfo;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import React from 'react';
|
||||
import { fakeIntl } from '../../util/test-data';
|
||||
import { renderDeep } from '../../util/test-helpers';
|
||||
import { types } from '../../util/sdkLoader';
|
||||
import BookingInfo from './BookingInfo';
|
||||
|
||||
describe('BookingInfo', () => {
|
||||
it('matches snapshot', () => {
|
||||
const tree = renderDeep(
|
||||
<BookingInfo
|
||||
pricePerDay="55\\u20AC"
|
||||
bookingPeriod="Jan 2nd - Jan 4th"
|
||||
bookingDuration="3 days"
|
||||
total="165\u20AC"
|
||||
unitPrice={new types.Money(1000, 'USD')}
|
||||
bookingStart={new Date('Fri, 14 Apr 2017 GMT')}
|
||||
bookingEnd={new Date('Sun, 16 Apr 2017 GMT')}
|
||||
intl={fakeIntl}
|
||||
/>
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
|
|
|
|||
|
|
@ -1,22 +1,52 @@
|
|||
exports[`BookingInfo matches snapshot 1`] = `
|
||||
<dl>
|
||||
<dt>
|
||||
Price per day:
|
||||
</dt>
|
||||
<dd>
|
||||
55\\\\u20AC
|
||||
</dd>
|
||||
<dt>
|
||||
Booking period:
|
||||
</dt>
|
||||
<dd>
|
||||
Jan 2nd - Jan 4th, 3 days
|
||||
</dd>
|
||||
<dt>
|
||||
Total
|
||||
</dt>
|
||||
<dd>
|
||||
165\\u20AC
|
||||
</dd>
|
||||
</dl>
|
||||
<div
|
||||
className="">
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<span>
|
||||
Price per day:
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
$10.00
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<span>
|
||||
Booking period:
|
||||
</span>
|
||||
<br />
|
||||
<span>
|
||||
04/14/2017 - 04/16/2017
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<span>
|
||||
2 days
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr
|
||||
className={undefined} />
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<span>
|
||||
Total:
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
$20.00
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
import React, { PropTypes } from 'react';
|
||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||
import en from 'react-intl/locale-data/en';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* eslint-disable import/prefer-default-export, no-console */
|
||||
/* eslint-disable no-console */
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form';
|
||||
import moment from 'moment';
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
* Styles for SingleDatePicker can be found from 'public/reactDates.css'.
|
||||
* CSS modules can't handle global styles so they are currently added separately
|
||||
*/
|
||||
import React, { PropTypes } from 'react';
|
||||
import momentPropTypes from 'react-moment-proptypes';
|
||||
import moment from 'moment';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { intlShape, injectIntl } from 'react-intl';
|
||||
import { SingleDatePicker, isInclusivelyAfterDay } from 'react-dates';
|
||||
import moment from 'moment';
|
||||
|
||||
const HORIZONTAL_ORIENTATION = 'horizontal';
|
||||
const ANCHOR_LEFT = 'left';
|
||||
|
|
@ -18,10 +18,10 @@ const defaultProps = {
|
|||
|
||||
// input related props
|
||||
id: 'date',
|
||||
placeholder: 'Date',
|
||||
placeholder: null, // Handled inside component
|
||||
disabled: false,
|
||||
required: true,
|
||||
screenReaderInputMessage: '',
|
||||
required: false,
|
||||
screenReaderInputMessage: null, // Handled inside component
|
||||
showClearDate: false,
|
||||
|
||||
// calendar presentation and interaction related props
|
||||
|
|
@ -52,12 +52,12 @@ const defaultProps = {
|
|||
displayFormat: () => moment.localeData().longDateFormat('L'),
|
||||
monthFormat: 'MMMM YYYY',
|
||||
phrases: {
|
||||
closeDatePicker: 'Close',
|
||||
clearDate: 'Clear Date',
|
||||
closeDatePicker: null, // Handled inside component
|
||||
clearDate: null, // Handled inside component
|
||||
},
|
||||
};
|
||||
|
||||
class DateInput extends React.Component {
|
||||
class DateInputComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
|
@ -69,7 +69,7 @@ class DateInput extends React.Component {
|
|||
}
|
||||
|
||||
onDateChange(date) {
|
||||
this.props.onChange(date instanceof moment ? date.clone() : null);
|
||||
this.props.onChange(date instanceof moment ? date.toDate() : null);
|
||||
}
|
||||
|
||||
onFocusChange({ focused }) {
|
||||
|
|
@ -85,9 +85,34 @@ class DateInput extends React.Component {
|
|||
|
||||
render() {
|
||||
const { focused } = this.state;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { initialDate, onBlur, onChange, onFocus, value, ...datePickerProps } = this.props;
|
||||
const date = value instanceof moment ? value.clone() : initialDate;
|
||||
/* eslint-disable no-unused-vars */
|
||||
const {
|
||||
initialDate,
|
||||
intl,
|
||||
placeholder,
|
||||
onBlur,
|
||||
onChange,
|
||||
onFocus,
|
||||
phrases,
|
||||
screenReaderInputMessage,
|
||||
value,
|
||||
...datePickerProps
|
||||
} = this.props;
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
const initialMoment = initialDate ? moment(initialDate) : null;
|
||||
const date = value instanceof Date ? moment(value) : initialMoment;
|
||||
|
||||
const placeholderText = placeholder ||
|
||||
intl.formatMessage({ id: 'DateInput.defaultPlaceholder' });
|
||||
const screenReaderInputText = screenReaderInputMessage ||
|
||||
intl.formatMessage({ id: 'DateInput.screenReaderInputMessage' });
|
||||
const closeDatePickerText = phrases.closeDatePicker
|
||||
? phrases.closeDatePicker
|
||||
: intl.formatMessage({ id: 'DateInput.closeDatePicker' });
|
||||
const clearDateText = phrases.clearDate
|
||||
? phrases.clearDate
|
||||
: intl.formatMessage({ id: 'DateInput.clearDate' });
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -98,23 +123,37 @@ class DateInput extends React.Component {
|
|||
focused={focused}
|
||||
onDateChange={this.onDateChange}
|
||||
onFocusChange={this.onFocusChange}
|
||||
placeholder={placeholderText}
|
||||
screenReaderInputMessage={screenReaderInputText}
|
||||
phrases={{ closeDatePicker: closeDatePickerText, clearDate: clearDateText }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DateInput.defaultProps = defaultProps;
|
||||
DateInputComponent.defaultProps = defaultProps;
|
||||
|
||||
const { func, string } = PropTypes;
|
||||
const { func, instanceOf, shape, string } = PropTypes;
|
||||
|
||||
DateInput.propTypes = {
|
||||
initialDate: momentPropTypes.momentObj,
|
||||
DateInputComponent.propTypes = {
|
||||
initialDate: instanceOf(Date),
|
||||
intl: intlShape.isRequired,
|
||||
isOutsideRange: func,
|
||||
onChange: func.isRequired,
|
||||
onBlur: func.isRequired,
|
||||
onFocus: func.isRequired,
|
||||
phrases: shape({
|
||||
closeDatePicker: string,
|
||||
clearDate: string,
|
||||
}),
|
||||
placeholder: string,
|
||||
value: momentPropTypes.momentObj,
|
||||
screenReaderInputMessage: string,
|
||||
value: instanceOf(Date),
|
||||
};
|
||||
|
||||
const DateInput = injectIntl(DateInputComponent);
|
||||
|
||||
DateInput.displayName = 'DateInput';
|
||||
|
||||
export default DateInput;
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ exports[`DateInput matches snapshot 1`] = `
|
|||
<div
|
||||
className="DateInput">
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-label="Date"
|
||||
aria-describedby="DateInput__screen-reader-message-date-input"
|
||||
aria-label="Date input"
|
||||
autoComplete="off"
|
||||
className="DateInput__input needsclick"
|
||||
disabled={false}
|
||||
|
|
@ -18,15 +18,19 @@ exports[`DateInput matches snapshot 1`] = `
|
|||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="Date"
|
||||
placeholder="Date input"
|
||||
readOnly={false}
|
||||
required={true}
|
||||
required={false}
|
||||
type="text"
|
||||
value="" />
|
||||
|
||||
<p
|
||||
className="screen-reader-only"
|
||||
id="DateInput__screen-reader-message-date-input">
|
||||
Date input
|
||||
</p>
|
||||
<div
|
||||
className="DateInput__display-text">
|
||||
Date
|
||||
Date input
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* eslint-disable no-console, import/prefer-default-export */
|
||||
/* eslint-disable no-console */
|
||||
import React from 'react';
|
||||
import ListingCard from './ListingCard';
|
||||
import { createUser, createListing, currencyConfig, fakeIntl } from '../../util/test-data';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* eslint-disable no-console, import/prefer-default-export */
|
||||
/* eslint-disable no-console */
|
||||
import React, { Component } from 'react';
|
||||
import { Button } from '../../components';
|
||||
import ModalInMobile from './ModalInMobile';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* eslint-disable no-console, import/prefer-default-export */
|
||||
/* eslint-disable no-console */
|
||||
import NamedLink from './NamedLink';
|
||||
|
||||
export const NamedLinkToSearchPage = {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import { BookingInfo, NamedLink } from '../../components';
|
||||
import { NamedLink } from '../../components';
|
||||
|
||||
import css from './OrderDetailsPanel.css';
|
||||
|
||||
|
|
@ -24,14 +24,13 @@ ContactInfo.propTypes = {
|
|||
};
|
||||
|
||||
const OrderDetailsPanel = props => {
|
||||
const { className, orderId, title, imageUrl, info, contact, confirmationCode } = props;
|
||||
const { className, orderId, title, imageUrl, contact, confirmationCode } = props;
|
||||
return (
|
||||
<div className={className}>
|
||||
<img alt={title} src={imageUrl} style={{ width: '100%' }} />
|
||||
<h3>{title}</h3>
|
||||
<ContactInfo {...contact} />
|
||||
<p>Confirmation code {confirmationCode}</p>
|
||||
<BookingInfo {...info} />
|
||||
<p>Cancel booking</p>
|
||||
<NamedLink className={css.buttonLink} name="OrderDiscussionPage" params={{ id: orderId }}>
|
||||
You have a new message!
|
||||
|
|
@ -47,7 +46,6 @@ OrderDetailsPanel.propTypes = {
|
|||
orderId: oneOfType([string, number]).isRequired,
|
||||
title: string.isRequired,
|
||||
imageUrl: string.isRequired,
|
||||
info: object.isRequired,
|
||||
contact: object.isRequired,
|
||||
confirmationCode: string.isRequired,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,11 +20,6 @@ exports[`OrderDetailsPanel matches snapshot 1`] = `
|
|||
Confirmation code
|
||||
some-test-confirmation-code
|
||||
</p>
|
||||
<BookingInfo
|
||||
bookingDuration="some booking duration"
|
||||
bookingPeriod="some booking period"
|
||||
pricePerDay="10$"
|
||||
total="100$" />
|
||||
<p>
|
||||
Cancel booking
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import AddImages from './AddImages/AddImages';
|
|||
import BookingInfo from './BookingInfo/BookingInfo';
|
||||
import Button from './Button/Button';
|
||||
import CurrencyInput from './CurrencyInput/CurrencyInput';
|
||||
import DateInput from './DateInput/DateInput';
|
||||
import Discussion from './Discussion/Discussion';
|
||||
import FilterPanel from './FilterPanel/FilterPanel';
|
||||
import HeroSection from './HeroSection/HeroSection';
|
||||
|
|
@ -30,6 +31,7 @@ export {
|
|||
BookingInfo,
|
||||
Button,
|
||||
CurrencyInput,
|
||||
DateInput,
|
||||
Discussion,
|
||||
FilterPanel,
|
||||
HeroSection,
|
||||
|
|
|
|||
23
src/containers/BookingDatesForm/BookingDatesForm.css
Normal file
23
src/containers/BookingDatesForm/BookingDatesForm.css
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
.receipt {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.totalDivider {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.totalPrice {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
margin-top: 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.smallPrint {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
13
src/containers/BookingDatesForm/BookingDatesForm.example.js
Normal file
13
src/containers/BookingDatesForm/BookingDatesForm.example.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/* eslint-disable no-console */
|
||||
import { types } from '../../util/sdkLoader';
|
||||
import BookingDatesForm from './BookingDatesForm';
|
||||
|
||||
export const Empty = {
|
||||
component: BookingDatesForm,
|
||||
props: {
|
||||
onSubmit: values => {
|
||||
console.log('Submit BookingDatesForm with values:', values);
|
||||
},
|
||||
price: new types.Money(1099, 'USD'),
|
||||
},
|
||||
};
|
||||
147
src/containers/BookingDatesForm/BookingDatesForm.js
Normal file
147
src/containers/BookingDatesForm/BookingDatesForm.js
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import { compose } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { Field, reduxForm, formValueSelector, propTypes as formPropTypes } from 'redux-form';
|
||||
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
|
||||
import { isInclusivelyAfterDay, isInclusivelyBeforeDay } from 'react-dates';
|
||||
import moment from 'moment';
|
||||
import { types } from '../../util/sdkLoader';
|
||||
import { required } from '../../util/validators';
|
||||
import { Button, BookingInfo, DateInput } from '../../components';
|
||||
import css from './BookingDatesForm.css';
|
||||
|
||||
const EnhancedDateInput = props => {
|
||||
const { input, isOutsideRange, labelMessage, placeholder, meta } = props;
|
||||
const { onBlur, onChange, onFocus, value } = input;
|
||||
const { touched, error } = meta;
|
||||
const maybeIsOutsideRange = isOutsideRange ? { isOutsideRange } : {};
|
||||
const inputProps = { ...maybeIsOutsideRange, onBlur, onChange, onFocus, placeholder, value };
|
||||
|
||||
return (
|
||||
<div>
|
||||
{labelMessage ? <label htmlFor="bookingStart">{labelMessage}</label> : null}
|
||||
<DateInput {...inputProps} />
|
||||
{touched && error ? <span className={css.error}>{error}</span> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
EnhancedDateInput.defaultProps = {
|
||||
input: null,
|
||||
isOutsideRange: null,
|
||||
labelMessage: null,
|
||||
placeholder: 'Date',
|
||||
};
|
||||
|
||||
const { bool, func, instanceOf, node, object, shape, string } = PropTypes;
|
||||
|
||||
EnhancedDateInput.propTypes = {
|
||||
input: object,
|
||||
isOutsideRange: func,
|
||||
labelMessage: node,
|
||||
meta: shape({
|
||||
touched: bool,
|
||||
error: string,
|
||||
}).isRequired,
|
||||
placeholder: string,
|
||||
};
|
||||
|
||||
export const BookingDatesFormComponent = props => {
|
||||
const {
|
||||
bookingStart,
|
||||
bookingEnd,
|
||||
className,
|
||||
handleSubmit,
|
||||
intl,
|
||||
price,
|
||||
pristine,
|
||||
submitting,
|
||||
} = props;
|
||||
const placeholderText = intl.formatMessage({ id: 'BookingDatesForm.placeholder' });
|
||||
const bookingStartLabel = intl.formatMessage({ id: 'BookingDatesForm.bookingStartTitle'});
|
||||
const bookingEndLabel = intl.formatMessage({ id: 'BookingDatesForm.bookingEndTitle'});
|
||||
const requiredMessage = intl.formatMessage({ id: 'BookingDatesForm.requiredDate' });
|
||||
|
||||
// A day is outside range if it is between today and booking end date (if end date has been chosen)
|
||||
const isOutsideRangeStart = bookingEnd
|
||||
? {
|
||||
isOutsideRange: day =>
|
||||
!(isInclusivelyAfterDay(day, moment()) &&
|
||||
isInclusivelyBeforeDay(day, moment(bookingEnd))),
|
||||
}
|
||||
: {};
|
||||
|
||||
// A day is outside range if it is after booking start date (or today if none is chosen)
|
||||
const startOfBookingEndRange = bookingStart ? moment(bookingStart) : moment();
|
||||
const isOutsideRangeEnd = bookingStart
|
||||
? { isOutsideRange: day => !isInclusivelyAfterDay(day, startOfBookingEndRange) }
|
||||
: {};
|
||||
|
||||
const bookingInfo = price
|
||||
? <BookingInfo
|
||||
className={css.receipt}
|
||||
bookingStart={bookingStart}
|
||||
bookingEnd={bookingEnd}
|
||||
unitPrice={price}
|
||||
/>
|
||||
: null;
|
||||
|
||||
const invalid = !(bookingStart && bookingEnd);
|
||||
|
||||
return (
|
||||
<form className={className} onSubmit={handleSubmit}>
|
||||
<Field
|
||||
name="bookingStart"
|
||||
labelMessage={bookingStartLabel}
|
||||
component={EnhancedDateInput}
|
||||
format={null}
|
||||
placeholder={placeholderText}
|
||||
{...isOutsideRangeStart}
|
||||
validate={[required(requiredMessage)]}
|
||||
/>
|
||||
<Field
|
||||
name="bookingEnd"
|
||||
labelMessage={bookingEndLabel}
|
||||
component={EnhancedDateInput}
|
||||
format={null}
|
||||
placeholder={placeholderText}
|
||||
{...isOutsideRangeEnd}
|
||||
validate={[required(requiredMessage)]}
|
||||
/>
|
||||
{bookingInfo}
|
||||
<p className={css.smallPrint}>
|
||||
<FormattedMessage id="BookingDatesForm.youWontBeChargedInfo" />
|
||||
</p>
|
||||
<Button type="submit" disabled={pristine || submitting || invalid}>
|
||||
<FormattedMessage id="BookingDatesForm.requestToBook" />
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
BookingDatesFormComponent.propTypes = {
|
||||
...formPropTypes,
|
||||
intl: intlShape.isRequired,
|
||||
price: instanceOf(types.Money).isRequired,
|
||||
};
|
||||
|
||||
const formName = 'bookingDates';
|
||||
|
||||
// When a field depends on the value of another field, we must connect
|
||||
// to the store and select the required values to inject to the
|
||||
// component.
|
||||
//
|
||||
// See: http://redux-form.com/6.6.1/examples/selectingFormValues/
|
||||
const selector = formValueSelector(formName);
|
||||
const mapStateToProps = state => {
|
||||
return selector(state, 'bookingStart', 'bookingEnd');
|
||||
};
|
||||
|
||||
const BookingDatesForm = compose(
|
||||
connect(mapStateToProps),
|
||||
reduxForm({ form: formName }),
|
||||
injectIntl
|
||||
)(BookingDatesFormComponent);
|
||||
BookingDatesForm.displayName = 'BookingDatesForm';
|
||||
|
||||
export default BookingDatesForm;
|
||||
22
src/containers/BookingDatesForm/BookingDatesForm.test.js
Normal file
22
src/containers/BookingDatesForm/BookingDatesForm.test.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
import { types } from '../../util/sdkLoader';
|
||||
import { renderShallow } from '../../util/test-helpers';
|
||||
import { fakeIntl, fakeFormProps } from '../../util/test-data';
|
||||
import { BookingDatesFormComponent } from './BookingDatesForm';
|
||||
|
||||
const noop = () => null;
|
||||
|
||||
describe('BookingDatesForm', () => {
|
||||
it('matches snapshot', () => {
|
||||
const tree = renderShallow(
|
||||
<BookingDatesFormComponent
|
||||
{...fakeFormProps}
|
||||
intl={fakeIntl}
|
||||
dispatch={noop}
|
||||
onSubmit={v => v}
|
||||
price={new types.Money(1099, 'USD')}
|
||||
/>
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
exports[`BookingDatesForm matches snapshot 1`] = `
|
||||
<form
|
||||
onSubmit={[Function]}>
|
||||
<Field
|
||||
component={[Function]}
|
||||
format={null}
|
||||
labelMessage="BookingDatesForm.bookingStartTitle"
|
||||
name="bookingStart"
|
||||
placeholder="BookingDatesForm.placeholder"
|
||||
validate={
|
||||
Array [
|
||||
[Function],
|
||||
]
|
||||
} />
|
||||
<Field
|
||||
component={[Function]}
|
||||
format={null}
|
||||
labelMessage="BookingDatesForm.bookingEndTitle"
|
||||
name="bookingEnd"
|
||||
placeholder="BookingDatesForm.placeholder"
|
||||
validate={
|
||||
Array [
|
||||
[Function],
|
||||
]
|
||||
} />
|
||||
<BookingInfo
|
||||
unitPrice={
|
||||
Money {
|
||||
"amount": 1099,
|
||||
"currency": "USD",
|
||||
}
|
||||
} />
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="BookingDatesForm.youWontBeChargedInfo"
|
||||
values={Object {}} />
|
||||
</p>
|
||||
<Button
|
||||
className={null}
|
||||
disabled={true}
|
||||
type="submit">
|
||||
<FormattedMessage
|
||||
id="BookingDatesForm.requestToBook"
|
||||
values={Object {}} />
|
||||
</Button>
|
||||
</form>
|
||||
`;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
/* eslint-disable no-console, import/prefer-default-export */
|
||||
/* eslint-disable no-console */
|
||||
import ChangeAccountPasswordForm from './ChangeAccountPasswordForm';
|
||||
|
||||
export const Empty = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* eslint-disable no-console, import/prefer-default-export */
|
||||
/* eslint-disable no-console */
|
||||
import ChangePasswordForm from './ChangePasswordForm';
|
||||
|
||||
export const Empty = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* eslint-disable no-console, import/prefer-default-export */
|
||||
/* eslint-disable no-console */
|
||||
import EditListingForm from './EditListingForm';
|
||||
|
||||
export const Empty = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* eslint-disable no-console, import/prefer-default-export */
|
||||
/* eslint-disable no-console */
|
||||
import HeroSearchForm from './HeroSearchForm';
|
||||
|
||||
export const Empty = {
|
||||
|
|
|
|||
|
|
@ -67,7 +67,11 @@
|
|||
background-color: #eee;
|
||||
}
|
||||
|
||||
.openDatePickerForm {
|
||||
.bookingForm {
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
.openBookingForm {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import config from '../../config';
|
|||
import { types } from '../../util/sdkLoader';
|
||||
import { convertMoneyToNumber } from '../../util/currency';
|
||||
import { Button, Map, ModalInMobile, PageLayout } from '../../components';
|
||||
import { BookingDatesForm } from '../../containers';
|
||||
import { getListingsById } from '../../ducks/sdk.duck';
|
||||
import { showListing } from './ListingPage.duck';
|
||||
import css from './ListingPage.css';
|
||||
|
|
@ -39,9 +40,22 @@ export class ListingPageComponent extends Component {
|
|||
isBookingModalOpenOnMobile: tab && tab === 'book',
|
||||
pageClassNames: '',
|
||||
};
|
||||
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
this.togglePageClassNames = this.togglePageClassNames.bind(this);
|
||||
}
|
||||
|
||||
onSubmit(values) {
|
||||
const { marketplaceData, params } = this.props;
|
||||
const id = new UUID(params.id);
|
||||
const listingsById = getListingsById(marketplaceData, [id]);
|
||||
const currentListing = listingsById.length > 0 ? listingsById[0] : null;
|
||||
|
||||
this.setState({ isBookingModalOpenOnMobile: false });
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Submitting: bookedDates', values, 'and price', currentListing.attributes.price);
|
||||
}
|
||||
|
||||
togglePageClassNames(className, addClass = true) {
|
||||
this.setState(prevState => {
|
||||
const prevPageClassNames = prevState.pageClassNames.split(' ');
|
||||
|
|
@ -116,10 +130,10 @@ export class ListingPageComponent extends Component {
|
|||
title={bookBtnMessage}
|
||||
togglePageClassNames={this.togglePageClassNames}
|
||||
>
|
||||
<span>ModalInMobile content</span>
|
||||
<BookingDatesForm className={css.bookingForm} onSubmit={this.onSubmit} price={price} />
|
||||
</ModalInMobile>
|
||||
{map ? <div className={css.map}>{map}</div> : null}
|
||||
<div className={css.openDatePickerForm}>
|
||||
<div className={css.openBookingForm}>
|
||||
<Button onClick={() => this.setState({ isBookingModalOpenOnMobile: true })}>
|
||||
{bookBtnMessage}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -11,14 +11,19 @@ const { UUID } = types;
|
|||
describe('ListingPage', () => {
|
||||
it('matches snapshot', () => {
|
||||
const marketplaceData = { entities: { listing: { listing1: createListing('listing1') } } };
|
||||
const tree = renderShallow(
|
||||
<ListingPageComponent
|
||||
params={{ slug: 'listing1-title', id: 'listing1' }}
|
||||
marketplaceData={marketplaceData}
|
||||
intl={fakeIntl}
|
||||
onLoadListing={l => l}
|
||||
/>
|
||||
);
|
||||
const props = {
|
||||
flattenedRoutes: [],
|
||||
location: { search: '' },
|
||||
history: {
|
||||
push: () => console.log('HistoryPush called'),
|
||||
},
|
||||
params: { slug: 'listing1-title', id: 'listing1' },
|
||||
marketplaceData,
|
||||
intl: fakeIntl,
|
||||
onLoadListing: () => {},
|
||||
};
|
||||
|
||||
const tree = renderShallow(<ListingPageComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -24,9 +24,14 @@ exports[`ListingPage matches snapshot 1`] = `
|
|||
showAsModalMaxWidth={2500}
|
||||
title="ListingPage.ctaButtonMessage"
|
||||
togglePageClassNames={[Function]}>
|
||||
<span>
|
||||
ModalInMobile content
|
||||
</span>
|
||||
<BookingDatesForm
|
||||
onSubmit={[Function]}
|
||||
price={
|
||||
Money {
|
||||
"amount": 5500,
|
||||
"currency": "USD",
|
||||
}
|
||||
} />
|
||||
</InjectIntl(ModalInMobileComponent)>
|
||||
<div>
|
||||
<Map
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* eslint-disable no-console, import/prefer-default-export */
|
||||
/* eslint-disable no-console */
|
||||
import LoginForm from './LoginForm';
|
||||
|
||||
export const Empty = {
|
||||
|
|
|
|||
|
|
@ -14,12 +14,6 @@ const OrderPage = props => {
|
|||
title,
|
||||
orderId,
|
||||
imageUrl: 'http://placehold.it/750x470',
|
||||
info: {
|
||||
pricePerDay: '55\u20AC',
|
||||
bookingPeriod: 'Jan 2nd - Jan 4th',
|
||||
bookingDuration: '3 days',
|
||||
total: '165\u20AC',
|
||||
},
|
||||
contact: {
|
||||
addressLine1: '350 5th Avenue',
|
||||
addressLine2: 'New York, NY 10118',
|
||||
|
|
|
|||
|
|
@ -35,14 +35,6 @@ exports[`OrderPage matches snapshot 1`] = `
|
|||
}
|
||||
}
|
||||
imageUrl="http://placehold.it/750x470"
|
||||
info={
|
||||
Object {
|
||||
"bookingDuration": "3 days",
|
||||
"bookingPeriod": "Jan 2nd - Jan 4th",
|
||||
"pricePerDay": "55€",
|
||||
"total": "165€",
|
||||
}
|
||||
}
|
||||
orderId={1234}
|
||||
title="Banyan Studios" />
|
||||
<OrderDiscussionPanel
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* eslint-disable no-console, import/prefer-default-export */
|
||||
/* eslint-disable no-console */
|
||||
import PasswordForgottenForm from './PasswordForgottenForm';
|
||||
|
||||
export const Empty = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* eslint-disable no-console, import/prefer-default-export */
|
||||
/* eslint-disable no-console */
|
||||
import SignUpForm from './SignUpForm';
|
||||
|
||||
export const Empty = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import AuthenticationPage from './AuthenticationPage/AuthenticationPage';
|
||||
import BookingDatesForm from './BookingDatesForm/BookingDatesForm';
|
||||
import ChangeAccountPasswordForm from './ChangeAccountPasswordForm/ChangeAccountPasswordForm';
|
||||
import ChangePasswordForm from './ChangePasswordForm/ChangePasswordForm';
|
||||
import CheckoutPage from './CheckoutPage/CheckoutPage';
|
||||
|
|
@ -29,6 +30,7 @@ import Topbar from './Topbar/Topbar';
|
|||
|
||||
export {
|
||||
AuthenticationPage,
|
||||
BookingDatesForm,
|
||||
ChangeAccountPasswordForm,
|
||||
ChangePasswordForm,
|
||||
CheckoutPage,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import * as StripeBankAccountToken
|
|||
from './components/StripeBankAccountToken/StripeBankAccountToken.example';
|
||||
|
||||
// containers
|
||||
import * as BookingDatesForm from './containers/BookingDatesForm/BookingDatesForm.example';
|
||||
import * as ChangeAccountPasswordForm
|
||||
from './containers/ChangeAccountPasswordForm/ChangeAccountPasswordForm.example';
|
||||
import * as ChangePasswordForm from './containers/ChangePasswordForm/ChangePasswordForm.example';
|
||||
|
|
@ -26,6 +27,7 @@ import * as StripePaymentForm from './containers/StripePaymentForm/StripePayment
|
|||
|
||||
export {
|
||||
AddImages,
|
||||
BookingDatesForm,
|
||||
BookingInfo,
|
||||
ChangeAccountPasswordForm,
|
||||
ChangePasswordForm,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,23 @@
|
|||
{
|
||||
"AuthenticationPage.loginFailed": "The email and password you entered did not match our records. Please double-check and try again.",
|
||||
"AuthenticationPage.loginRequiredFor": "You must log in to view the page at",
|
||||
"BookingDatesForm.bookingEndTitle": "End date",
|
||||
"BookingDatesForm.bookingStartTitle": "Start date",
|
||||
"BookingDatesForm.placeholder": "mm/dd/yyyy",
|
||||
"BookingDatesForm.requiredDate": "Oops, make sure your date is correct!",
|
||||
"BookingDatesForm.requestToBook": "Request to book",
|
||||
"BookingDatesForm.youWontBeChargedInfo": "You won't be charged yet",
|
||||
"BookingInfo.bookingPeriodLabel": "Booking period:",
|
||||
"BookingInfo.bookingPeriod": "{bookingStart} - {bookingEnd}",
|
||||
"BookingInfo.nightCount": "{count, number} {count, plural, one {day} other {days}}",
|
||||
"BookingInfo.pricePerDay":"Price per day:",
|
||||
"BookingInfo.total": "Total:",
|
||||
"CheckoutPage.title": "Book {listingTitle}",
|
||||
"CheckoutPage.paymentTitle": "Payment",
|
||||
"DateInput.clearDate": "Clear Date",
|
||||
"DateInput.closeDatePicker": "Close",
|
||||
"DateInput.defaultPlaceholder": "Date input",
|
||||
"DateInput.screenReaderInputMessage": "Date input",
|
||||
"CheckoutPage.paymentInfo": "You'll only be charged if your request is accepted by the provider.",
|
||||
"EditListingForm.bankAccountNumberRequired": "You need to add a bank account number.",
|
||||
"EditListingForm.descriptionRequired": "You need to add a description.",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
// async actions use request - success / failure pattern
|
||||
const REQUEST = 'REQUEST';
|
||||
const SUCCESS = 'SUCCESS';
|
||||
|
|
@ -15,4 +13,3 @@ export const createRequestTypes = base =>
|
|||
},
|
||||
{}
|
||||
);
|
||||
/* eslint-enable import/prefer-default-export */
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue