Skip to content

Commit

Permalink
WiP
Browse files Browse the repository at this point in the history
  • Loading branch information
OtterleyW committed Sep 20, 2019
1 parent 7e783e3 commit ba5dd22
Show file tree
Hide file tree
Showing 13 changed files with 697 additions and 13 deletions.
2 changes: 2 additions & 0 deletions src/examples.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import * as UserDisplayName from './components/UserDisplayName/UserDisplayName.e

// forms
import * as BookingDatesForm from './forms/BookingDatesForm/BookingDatesForm.example';
import * as BookingTimeForm from './forms/BookingTimeForm/BookingTimeForm.example';
import * as FieldDateAndTimeInput from './forms/BookingTimeForm/FieldDateAndTimeInput.example';
import * as EditListingAvailabilityForm from './forms/EditListingAvailabilityForm/EditListingAvailabilityForm.example';
import * as EditListingDescriptionForm from './forms/EditListingDescriptionForm/EditListingDescriptionForm.example';
Expand Down Expand Up @@ -104,6 +105,7 @@ export {
BookingBreakdown,
BookingDateRangeFilter,
BookingDatesForm,
BookingTimeForm,
BookingTimeInfo,
BookingPanel,
Button,
Expand Down
87 changes: 87 additions & 0 deletions src/forms/BookingTimeForm/BookingTimeForm.css
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%;
}
}
18 changes: 18 additions & 0 deletions src/forms/BookingTimeForm/BookingTimeForm.example.js
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',
};
272 changes: 272 additions & 0 deletions src/forms/BookingTimeForm/BookingTimeForm.js
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;
Loading

0 comments on commit ba5dd22

Please sign in to comment.