mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-29 05:00:43 +10:00
Merge pull request #873 from sharetribe/date-input-availability
Show available time slots in date input
This commit is contained in:
commit
f1c4228d36
8 changed files with 135 additions and 14 deletions
|
|
@ -199,7 +199,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) {
|
||||
|
|
|
|||
|
|
@ -5,12 +5,20 @@
|
|||
* N.B. *isOutsideRange* in defaultProps is defining what dates are available to booking.
|
||||
*/
|
||||
import React, { Component } from 'react';
|
||||
import { bool, func, instanceOf, shape, string } from 'prop-types';
|
||||
import { SingleDatePicker, isInclusivelyAfterDay, isInclusivelyBeforeDay } from 'react-dates';
|
||||
import { bool, func, instanceOf, shape, string, arrayOf } from 'prop-types';
|
||||
import {
|
||||
SingleDatePicker,
|
||||
isInclusivelyAfterDay,
|
||||
isInclusivelyBeforeDay,
|
||||
isSameDay,
|
||||
} from 'react-dates';
|
||||
import { intlShape, injectIntl } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import moment from 'moment';
|
||||
import config from '../../config';
|
||||
import { propTypes, TIME_SLOT_DAY } from '../../util/types';
|
||||
import { dateFromAPIToLocalNoon } from '../../util/dates';
|
||||
import { ensureTimeSlot } from '../../util/data';
|
||||
|
||||
import NextMonthIcon from './NextMonthIcon';
|
||||
import PreviousMonthIcon from './PreviousMonthIcon';
|
||||
|
|
@ -97,6 +105,17 @@ const defaultProps = {
|
|||
},
|
||||
};
|
||||
|
||||
// Checks if time slot (propTypes.timeSlot) start time equals a day (moment)
|
||||
const timeSlotEqualsDay = (timeSlot, 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);
|
||||
|
||||
const isDay = ensureTimeSlot(timeSlot).attributes.type === TIME_SLOT_DAY;
|
||||
return isDay && isSameDay(day, moment(localStartDate));
|
||||
};
|
||||
|
||||
class DateInputComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
|
@ -143,6 +162,7 @@ class DateInputComponent extends Component {
|
|||
value,
|
||||
children,
|
||||
render,
|
||||
timeSlots,
|
||||
...datePickerProps
|
||||
} = this.props;
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
|
@ -151,6 +171,10 @@ class DateInputComponent extends Component {
|
|||
|
||||
const date = value && value.date instanceof Date ? moment(value.date) : initialMoment;
|
||||
|
||||
const isDayBlocked = timeSlots
|
||||
? day => !timeSlots.find(timeSlot => timeSlotEqualsDay(timeSlot, day))
|
||||
: () => false;
|
||||
|
||||
const placeholder = placeholderText || intl.formatMessage({ id: 'FieldDateInput.placeholder' });
|
||||
|
||||
const screenReaderInputText =
|
||||
|
|
@ -180,6 +204,7 @@ class DateInputComponent extends Component {
|
|||
placeholder={placeholder}
|
||||
screenReaderInputMessage={screenReaderInputText}
|
||||
phrases={{ closeDatePicker: closeDatePickerText, clearDate: clearDateText }}
|
||||
isDayBlocked={isDayBlocked}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -190,6 +215,7 @@ DateInputComponent.defaultProps = {
|
|||
className: null,
|
||||
useMobileMargins: false,
|
||||
...defaultProps,
|
||||
timeSlots: null,
|
||||
};
|
||||
|
||||
DateInputComponent.propTypes = {
|
||||
|
|
@ -213,6 +239,7 @@ DateInputComponent.propTypes = {
|
|||
value: shape({
|
||||
date: instanceOf(Date),
|
||||
}),
|
||||
timeSlots: arrayOf(propTypes.timeSlot),
|
||||
};
|
||||
|
||||
export default injectIntl(DateInputComponent);
|
||||
|
|
|
|||
|
|
@ -4,13 +4,24 @@ import { Form as FinalForm, FormSpy } from 'react-final-form';
|
|||
import moment from 'moment';
|
||||
import { Button } from '../../components';
|
||||
import { required, bookingDateRequired, composeValidators } from '../../util/validators';
|
||||
import { createTimeSlots } from '../../util/test-data';
|
||||
import FieldDateInput from './FieldDateInput';
|
||||
|
||||
const createAvailableTimeSlots = (dayCount, availableDayCount) => {
|
||||
const slots = createTimeSlots(new Date(), dayCount);
|
||||
const availableSlotIndices = Array.from({ length: availableDayCount }, () =>
|
||||
Math.floor(Math.random() * dayCount)
|
||||
);
|
||||
|
||||
return availableSlotIndices.sort().map(i => slots[i]);
|
||||
};
|
||||
|
||||
const FormComponent = props => (
|
||||
<FinalForm
|
||||
{...props}
|
||||
render={fieldRenderProps => {
|
||||
const {
|
||||
style,
|
||||
form,
|
||||
handleSubmit,
|
||||
onChange,
|
||||
|
|
@ -26,6 +37,7 @@ const FormComponent = props => (
|
|||
|
||||
return (
|
||||
<form
|
||||
style={style}
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
handleSubmit(e);
|
||||
|
|
@ -42,15 +54,14 @@ const FormComponent = props => (
|
|||
/>
|
||||
);
|
||||
|
||||
const defaultFormName = 'FieldDateInputExampleForm';
|
||||
|
||||
export const Empty = {
|
||||
component: FormComponent,
|
||||
props: {
|
||||
style: { marginBottom: '140px' },
|
||||
dateInputProps: {
|
||||
name: 'bookingDate',
|
||||
useMobileMargins: false,
|
||||
id: `${defaultFormName}.bookingDate`,
|
||||
id: `EmptyDateInputForm.bookingDate`,
|
||||
label: 'Date',
|
||||
placeholderText: moment().format('ddd, MMMM D'),
|
||||
format: null,
|
||||
|
|
@ -70,3 +81,31 @@ export const Empty = {
|
|||
},
|
||||
group: 'custom inputs',
|
||||
};
|
||||
|
||||
export const WithAvailableTimeSlots = {
|
||||
component: FormComponent,
|
||||
props: {
|
||||
dateInputProps: {
|
||||
name: 'bookingDate',
|
||||
useMobileMargins: false,
|
||||
id: `AvailableTimeSlotsDateInputForm.bookingDate`,
|
||||
label: 'Date',
|
||||
placeholderText: moment().format('ddd, MMMM D'),
|
||||
format: null,
|
||||
timeSlots: createAvailableTimeSlots(90, 60),
|
||||
validate: composeValidators(required('Required'), bookingDateRequired('Date is not valid')),
|
||||
onBlur: () => console.log('onBlur called from DateInput props.'),
|
||||
onFocus: () => console.log('onFocus called from DateInput props.'),
|
||||
},
|
||||
onChange: formState => {
|
||||
const { date } = formState.values;
|
||||
if (date) {
|
||||
console.log('Changed to', moment(date).format('L'));
|
||||
}
|
||||
},
|
||||
onSubmit: values => {
|
||||
console.log('Submitting a form with values:', values);
|
||||
},
|
||||
},
|
||||
group: 'custom inputs',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@
|
|||
* you should convert value.date to start date and end date before submitting it to API
|
||||
*/
|
||||
import React, { Component } from 'react';
|
||||
import { bool, object, string } from 'prop-types';
|
||||
import { bool, object, string, arrayOf } from 'prop-types';
|
||||
import { Field } from 'react-final-form';
|
||||
import classNames from 'classnames';
|
||||
import { ValidationError } from '../../components';
|
||||
import { propTypes } from '../../util/types';
|
||||
|
||||
import DateInput from './DateInput';
|
||||
import css from './FieldDateInput.css';
|
||||
|
|
@ -77,6 +78,7 @@ FieldDateInputComponent.defaultProps = {
|
|||
id: null,
|
||||
label: null,
|
||||
placeholderText: null,
|
||||
timeSlots: null,
|
||||
};
|
||||
|
||||
FieldDateInputComponent.propTypes = {
|
||||
|
|
@ -86,6 +88,7 @@ FieldDateInputComponent.propTypes = {
|
|||
id: string,
|
||||
label: string,
|
||||
placeholderText: string,
|
||||
timeSlots: arrayOf(propTypes.timeSlot),
|
||||
input: object.isRequired,
|
||||
meta: object.isRequired,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -497,7 +497,7 @@ ListingPageComponent.defaultProps = {
|
|||
showListingError: null,
|
||||
reviews: [],
|
||||
fetchReviewsError: null,
|
||||
timeSlots: [],
|
||||
timeSlots: null,
|
||||
fetchTimeSlotsError: null,
|
||||
sendEnquiryError: null,
|
||||
categoriesConfig: config.custom.categories,
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ export const denormalisedResponseEntities = sdkResponse => {
|
|||
/**
|
||||
* Create shell objects to ensure that attributes etc. exists.
|
||||
*
|
||||
* @param {Object} transaction entity object, which is to be ensured agains null values
|
||||
* @param {Object} transaction entity object, which is to be ensured against null values
|
||||
*/
|
||||
export const ensureTransaction = (transaction, booking = null, listing = null, provider = null) => {
|
||||
const empty = {
|
||||
|
|
@ -150,7 +150,7 @@ export const ensureTransaction = (transaction, booking = null, listing = null, p
|
|||
/**
|
||||
* Create shell objects to ensure that attributes etc. exists.
|
||||
*
|
||||
* @param {Object} booking entity object, which is to be ensured agains null values
|
||||
* @param {Object} booking entity object, which is to be ensured against null values
|
||||
*/
|
||||
export const ensureBooking = booking => {
|
||||
const empty = { id: null, type: 'booking', attributes: {} };
|
||||
|
|
@ -160,7 +160,7 @@ export const ensureBooking = booking => {
|
|||
/**
|
||||
* Create shell objects to ensure that attributes etc. exists.
|
||||
*
|
||||
* @param {Object} listing entity object, which is to be ensured agains null values
|
||||
* @param {Object} listing entity object, which is to be ensured against null values
|
||||
*/
|
||||
export const ensureListing = listing => {
|
||||
const empty = {
|
||||
|
|
@ -175,7 +175,7 @@ export const ensureListing = listing => {
|
|||
/**
|
||||
* Create shell objects to ensure that attributes etc. exists.
|
||||
*
|
||||
* @param {Object} listing entity object, which is to be ensured agains null values
|
||||
* @param {Object} listing entity object, which is to be ensured against null values
|
||||
*/
|
||||
export const ensureOwnListing = listing => {
|
||||
const empty = {
|
||||
|
|
@ -190,7 +190,7 @@ export const ensureOwnListing = listing => {
|
|||
/**
|
||||
* Create shell objects to ensure that attributes etc. exists.
|
||||
*
|
||||
* @param {Object} user entity object, which is to be ensured agains null values
|
||||
* @param {Object} user entity object, which is to be ensured against null values
|
||||
*/
|
||||
export const ensureUser = user => {
|
||||
const empty = { id: null, type: 'user', attributes: { profile: {} } };
|
||||
|
|
@ -200,13 +200,23 @@ export const ensureUser = user => {
|
|||
/**
|
||||
* Create shell objects to ensure that attributes etc. exists.
|
||||
*
|
||||
* @param {Object} current user entity object, which is to be ensured agains null values
|
||||
* @param {Object} current user entity object, which is to be ensured against null values
|
||||
*/
|
||||
export const ensureCurrentUser = user => {
|
||||
const empty = { id: null, type: 'currentUser', attributes: { profile: {} }, profileImage: {} };
|
||||
return { ...empty, ...user };
|
||||
};
|
||||
|
||||
/**
|
||||
* Create shell objects to ensure that attributes etc. exists.
|
||||
*
|
||||
* @param {Object} time slot entity object, which is to be ensured against null values
|
||||
*/
|
||||
export const ensureTimeSlot = timeSlot => {
|
||||
const empty = { id: null, type: 'timeSlot', attributes: {} };
|
||||
return { ...empty, ...timeSlot };
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the display name of the given user. This function handles
|
||||
* missing data (e.g. when the user object is still being downloaded),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import Decimal from 'decimal.js';
|
||||
import moment from 'moment';
|
||||
import { types as sdkTypes } from './sdkLoader';
|
||||
import { nightsBetween } from '../util/dates';
|
||||
import {
|
||||
|
|
@ -7,6 +8,7 @@ import {
|
|||
TX_TRANSITION_ACTOR_CUSTOMER,
|
||||
TX_TRANSITION_ACTOR_PROVIDER,
|
||||
LISTING_STATE_PUBLISHED,
|
||||
TIME_SLOT_DAY,
|
||||
} from '../util/types';
|
||||
|
||||
const { UUID, LatLng, Money } = sdkTypes;
|
||||
|
|
@ -212,6 +214,34 @@ export const createReview = (id, attributes = {}, includes = {}) => {
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an array of time slot objects.
|
||||
*
|
||||
* @param {Date} startDate date when the time slots start
|
||||
* @param {Number} numberOfDays number of time slots to create
|
||||
*
|
||||
* @return {Array} array of time slots
|
||||
*/
|
||||
export const createTimeSlots = (startDate, numberOfDays) => {
|
||||
const startTime = moment.utc(startDate).startOf('day');
|
||||
|
||||
return Array.from({ length: numberOfDays }, (v, i) => i).map(i => {
|
||||
return {
|
||||
id: new UUID(i),
|
||||
type: 'timeSlot',
|
||||
attributes: {
|
||||
start: moment(startTime)
|
||||
.add(i, 'days')
|
||||
.toDate(),
|
||||
end: moment(startTime)
|
||||
.add(i + 1, 'days')
|
||||
.toDate(),
|
||||
type: TIME_SLOT_DAY,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Default config for currency formatting in tests and examples.
|
||||
export const currencyConfig = {
|
||||
style: 'currency',
|
||||
|
|
|
|||
|
|
@ -187,11 +187,15 @@ propTypes.booking = shape({
|
|||
}),
|
||||
});
|
||||
|
||||
// A time slot that covers one day, having a start and end date.
|
||||
export const TIME_SLOT_DAY = 'time-slot/day';
|
||||
|
||||
// Denormalised time slot object
|
||||
propTypes.timeSlot = shape({
|
||||
id: propTypes.uuid.isRequired,
|
||||
type: propTypes.value('timeSlot').isRequired,
|
||||
attributes: shape({
|
||||
type: oneOf([TIME_SLOT_DAY]).isRequired,
|
||||
end: instanceOf(Date).isRequired,
|
||||
start: instanceOf(Date).isRequired,
|
||||
}),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue