Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
lyyder committed Aug 8, 2018
1 parent f1c4228 commit 19addae
Show file tree
Hide file tree
Showing 5 changed files with 318 additions and 38 deletions.
11 changes: 10 additions & 1 deletion src/components/FieldDateRangeInput/DateRangeInput.css
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@
background-color: var(--successColor);
border-top-right-radius: calc(var(--DateRangeInput_selectionHeight) / 2);
border-bottom-right-radius: calc(var(--DateRangeInput_selectionHeight) / 2);
color: var(--matterColorLight);
}
& :global(.CalendarDay:hover .renderedDay) {
display: flex;
Expand All @@ -209,7 +210,15 @@
color: var(--marketplaceColorDark);
border: 0;
}
& :global(.CalendarDay__blocked_out_of_range .renderedDay) {
/* Remove default bg-color and use our extra span instead '.renderedDay' */
& :global(.CalendarDay__blocked_calendar),
& :global(.CalendarDay__blocked_calendar:active),
& :global(.CalendarDay__blocked_calendar:hover) {
background-color: transparent;
color: var(--marketplaceColorDark);
border: 0;
}
& :global(.CalendarDay__blocked_out_of_range .CalendarDay__blocked_calendar .renderedDay) {
background-color: transparent;
}
& :global(.DateInput_fang) {
Expand Down
200 changes: 200 additions & 0 deletions src/components/FieldDateRangeInput/DateRangeInput.helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import moment from 'moment';
import { isSameDay, isInclusivelyAfterDay, isInclusivelyBeforeDay } from 'react-dates';

import { ensureTimeSlot } from '../../util/data';
import { START_DATE, END_DATE, dateFromAPIToLocalNoon } from '../../util/dates';
import { LINE_ITEM_DAY, TIME_SLOT_DAY } from '../../util/types';
import config from '../../config';

// Checks if time slot (propTypes.timeSlot) start time equals a day (moment)
const timeSlotEqualsDay = (timeSlot, day) => {
if (ensureTimeSlot(timeSlot).attributes.type === TIME_SLOT_DAY) {
// Time slots describe available dates by providing a start and
// an end date which is the following day. In the single date picker
// the start date is used to represent available dates.
const localStartDate = dateFromAPIToLocalNoon(timeSlot.attributes.start);

return isSameDay(day, moment(localStartDate));
} else {
return false;
}
};

/**
* Return a boolean indicating if given date can be found in an array
* of tile slots (start dates).
*/
const timeSlotsContain = (timeSlots, date) => {
return timeSlots.findIndex(slot => timeSlotEqualsDay(slot, date)) > -1;
};

const lastBlockedBetweenExclusive = (timeSlots, startDate, endDate) => {
if (startDate.isSame(endDate, 'date')) {
return undefined;
}

return timeSlotsContain(timeSlots, endDate)
? lastBlockedBetweenExclusive(timeSlots, startDate, moment(endDate).subtract(1, 'days'))
: endDate;
};

/**
* Find first blocked date between two dates.
* If none is found, undefined is returned.
*
* @param {Array} timeSlots propTypes.timeSlot objects
* @param {Moment} startDate start date, exclusive
* @param {Moment} endDate end date, exclusive
*/
const firstBlockedBetween = (timeSlots, startDate, endDate) => {
const firstDate = moment(startDate).add(1, 'days');
if (firstDate.isSame(endDate, 'date')) {
return undefined;
}

return timeSlotsContain(timeSlots, firstDate)
? firstBlockedBetween(timeSlots, firstDate, endDate)
: firstDate;
};

/**
* Find last blocked date between two dates.
* If none is found, undefined is returned.
*
* @param {Array} timeSlots propTypes.timeSlot objects
* @param {Moment} startDate start date, exclusive
* @param {Moment} endDate end date, exclusive
*/
const lastBlockedBetween = (timeSlots, startDate, endDate) => {
const previousDate = moment(endDate).subtract(1, 'days');
if (previousDate.isSame(startDate, 'date')) {
return undefined;
}

return timeSlotsContain(timeSlots, previousDate)
? lastBlockedBetween(timeSlots, startDate, previousDate)
: previousDate;
};

/**
* Check if a blocked date can be found between two dates.
*
* @param {Array} timeSlots propTypes.timeSlot objects
* @param {Moment} startDate start date, exclusive
* @param {Moment} endDate end date, exclusive
*/
export const isBlockedBetween = (timeSlots, startDate, endDate) =>
!!firstBlockedBetween(timeSlots, startDate, endDate);

export const isStartDateSelected = (timeSlots, startDate, endDate, focusedInput) =>
timeSlots && startDate && (!endDate || focusedInput === END_DATE) && focusedInput !== START_DATE;

export const apiEndDateToPickerDate = (unitType, endDate) => {
const isValid = endDate instanceof Date;
const isDaily = unitType === LINE_ITEM_DAY;

if (!isValid) {
return null;
} else if (isDaily) {
// API end dates are exlusive, so we need to shift them with daily
// booking.
return moment(endDate).subtract(1, 'days');
} else {
return moment(endDate);
}
};

export const pickerEndDateToApiDate = (unitType, endDate) => {
const isValid = endDate instanceof moment;
const isDaily = unitType === LINE_ITEM_DAY;

if (!isValid) {
return null;
} else if (isDaily) {
// API end dates are exlusive, so we need to shift them with daily
// booking.
return endDate.add(1, 'days').toDate();
} else {
return endDate.toDate();
}
};
/**
* Returns an isDayBlocked function that can be passed to
* a react-dates DateRangePicker component.
*/
export const isDayBlockedFn = (timeSlots, startDate, endDate, focusedInput) => {
const endOfRange = config.dayCountAvailableForBooking - 1;
const lastBookableDate = moment().add(endOfRange, 'days');

// start date selected, end date missing
const startDateSelected = isStartDateSelected(timeSlots, startDate, endDate, focusedInput);

const nextBookingStarts = startDateSelected
? firstBlockedBetween(timeSlots, startDate, moment(lastBookableDate).add(1, 'days'))
: null;

return nextBookingStarts || !timeSlots
? () => false
: day => !timeSlots.find(timeSlot => timeSlotEqualsDay(timeSlot, day));
};

/**
* Returns an isOutsideRange function that can be passed to
* a react-dates DateRangePicker component.
*/
export const isOutsideRangeFn = (
timeSlots,
startDate,
endDate,
previousStartDate,
focusedInput,
unitType
) => {
const endOfRange = config.dayCountAvailableForBooking - 1;
const lastBookableDate = moment().add(endOfRange, 'days');

// start date selected, end date missing
const startDateSelected = isStartDateSelected(timeSlots, startDate, endDate, focusedInput);
const nextBookingStarts = startDateSelected
? firstBlockedBetween(timeSlots, startDate, moment(lastBookableDate).add(1, 'days'))
: null;

if (nextBookingStarts) {
// end the range so that the booking can end at latest on
// nightly booking: the day the next booking starts
// daily booking: the day before the next booking starts
return day => {
const lastDayToEndBooking = apiEndDateToPickerDate(unitType, nextBookingStarts.toDate());

return (
!isInclusivelyAfterDay(day, startDate) || !isInclusivelyBeforeDay(day, lastDayToEndBooking)
);
};
}

// end date selected, start date missing
// -> limit the earliest start date for the booking so that it
// needs to be after the previous booked date
const endDateSelected = timeSlots && endDate && !startDate && focusedInput !== END_DATE;
const previousBookedDate = endDateSelected
? lastBlockedBetween(timeSlots, moment(), endDate)
: null;

if (previousBookedDate) {
return day => {
const firstDayToStartBooking = moment(previousBookedDate).add(1, 'days');
return (
!isInclusivelyAfterDay(day, firstDayToStartBooking) ||
!isInclusivelyBeforeDay(day, lastBookableDate)
);
};
}

// standard isOutsideRange function
return day => {
return (
!isInclusivelyAfterDay(day, moment()) ||
!isInclusivelyBeforeDay(day, moment().add(endOfRange, 'days'))
);
};
};
82 changes: 50 additions & 32 deletions src/components/FieldDateRangeInput/DateRangeInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,21 @@
* N.B. *isOutsideRange* in defaultProps is defining what dates are available to booking.
*/
import React, { Component } from 'react';
import { bool, func, instanceOf, oneOf, shape, string } from 'prop-types';
import { bool, func, instanceOf, oneOf, shape, string, arrayOf } from 'prop-types';
import { DateRangePicker, isInclusivelyAfterDay, isInclusivelyBeforeDay } from 'react-dates';
import { intlShape, injectIntl } from 'react-intl';
import classNames from 'classnames';
import moment from 'moment';
import { START_DATE, END_DATE } from '../../util/dates';
import { LINE_ITEM_DAY, propTypes } from '../../util/types';
import config from '../../config';
import {
isDayBlockedFn,
isOutsideRangeFn,
isBlockedBetween,
apiEndDateToPickerDate,
pickerEndDateToApiDate,
} from './DateRangeInput.helpers';

import NextMonthIcon from './NextMonthIcon';
import PreviousMonthIcon from './PreviousMonthIcon';
Expand All @@ -30,36 +37,6 @@ export const ANCHOR_LEFT = 'left';
// value and moves on to another input within this component.
const BLUR_TIMEOUT = 100;

const apiEndDateToPickerDate = (unitType, endDate) => {
const isValid = endDate instanceof Date;
const isDaily = unitType === LINE_ITEM_DAY;

if (!isValid) {
return null;
} else if (isDaily) {
// API end dates are exlusive, so we need to shift them with daily
// booking.
return moment(endDate).subtract(1, 'days');
} else {
return moment(endDate);
}
};

const pickerEndDateToApiDate = (unitType, endDate) => {
const isValid = endDate instanceof moment;
const isDaily = unitType === LINE_ITEM_DAY;

if (!isValid) {
return null;
} else if (isDaily) {
// API end dates are exlusive, so we need to shift them with daily
// booking.
return endDate.add(1, 'days').toDate();
} else {
return endDate.toDate();
}
};

// Possible configuration options of React-dates
const defaultProps = {
initialDates: null, // Possible initial date passed for the component
Expand Down Expand Up @@ -145,6 +122,8 @@ class DateRangeInputComponent extends Component {

this.state = {
focusedInput: null,
currentStartDate: null,
previousStartDate: null,
};

this.blurTimeoutId = null;
Expand All @@ -170,6 +149,12 @@ class DateRangeInputComponent extends Component {
const { startDate, endDate } = dates;
const startDateAsDate = startDate instanceof moment ? startDate.toDate() : null;
const endDateAsDate = pickerEndDateToApiDate(unitType, endDate);

this.setState(prevState => ({
currentStartDate: startDateAsDate,
previousStartDate: prevState.currentStartDate,
}));

this.props.onChange({ startDate: startDateAsDate, endDate: endDateAsDate });
}

Expand Down Expand Up @@ -208,6 +193,7 @@ class DateRangeInputComponent extends Component {
value,
children,
render,
timeSlots,
...datePickerProps
} = this.props;
/* eslint-enable no-unused-vars */
Expand All @@ -220,6 +206,34 @@ class DateRangeInputComponent extends Component {
const endDate =
apiEndDateToPickerDate(unitType, value ? value.endDate : null) || initialEndMoment;

// both dates are selected, a new start date before the previous start
// date is selected
const startDateUpdated =
timeSlots &&
startDate &&
endDate &&
this.state.previousStartDate &&
startDate.isBefore(this.state.previousStartDate);

// clear the end date in case a blocked date can be found
// between previous start date and new start date
const clearEndDate = startDateUpdated
? isBlockedBetween(timeSlots, startDate, moment(this.state.previousStartDate).add(1, 'days'))
: false;

const endDateMaybe = clearEndDate ? null : endDate;

let isDayBlocked = isDayBlockedFn(timeSlots, startDate, endDate, this.state.focusedInput);

let isOutsideRange = isOutsideRangeFn(
timeSlots,
startDate,
endDateMaybe,
this.state.previousStartDate,
this.state.focusedInput,
unitType
);

const startDatePlaceholderTxt =
startDatePlaceholderText ||
intl.formatMessage({ id: 'FieldDateRangeInput.startDatePlaceholderText' });
Expand Down Expand Up @@ -247,13 +261,15 @@ class DateRangeInputComponent extends Component {
focusedInput={this.state.focusedInput}
onFocusChange={this.onFocusChange}
startDate={startDate}
endDate={endDate}
endDate={endDateMaybe}
minimumNights={isDaily ? 0 : 1}
onDatesChange={this.onDatesChange}
startDatePlaceholderText={startDatePlaceholderTxt}
endDatePlaceholderText={endDatePlaceholderTxt}
screenReaderInputMessage={screenReaderInputText}
phrases={{ closeDatePicker: closeDatePickerText, clearDate: clearDateText }}
isDayBlocked={isDayBlocked}
isOutsideRange={isOutsideRange}
/>
</div>
);
Expand All @@ -263,6 +279,7 @@ class DateRangeInputComponent extends Component {
DateRangeInputComponent.defaultProps = {
className: null,
useMobileMargins: false,
timeSlots: null,
...defaultProps,
};

Expand Down Expand Up @@ -291,6 +308,7 @@ DateRangeInputComponent.propTypes = {
startDate: instanceOf(Date),
endDate: instanceOf(Date),
}),
timeSlots: arrayOf(propTypes.timeSlot),
};

export default injectIntl(DateRangeInputComponent);
Loading

0 comments on commit 19addae

Please sign in to comment.