diff --git a/src/components/BookingTimeInfo/BookingTimeInfo.css b/src/components/BookingTimeInfo/BookingTimeInfo.css new file mode 100644 index 00000000..0fff0e5e --- /dev/null +++ b/src/components/BookingTimeInfo/BookingTimeInfo.css @@ -0,0 +1,14 @@ +@import '../../marketplace.css'; + +.root { +} + +.bookingInfo { + display: flex; + align-items: center; + flex-wrap: wrap; +} + +.dateSection { + margin-right: 5px; +} diff --git a/src/components/BookingTimeInfo/BookingTimeInfo.example.js b/src/components/BookingTimeInfo/BookingTimeInfo.example.js new file mode 100644 index 00000000..41f292f4 --- /dev/null +++ b/src/components/BookingTimeInfo/BookingTimeInfo.example.js @@ -0,0 +1,141 @@ +import BookingTimeInfo from './BookingTimeInfo'; +import { + fakeIntl, + createBooking, + createTransaction, + createUser, + createListing, +} from '../../util/test-data'; +import { LINE_ITEM_DAY, LINE_ITEM_NIGHT, LINE_ITEM_UNITS } from '../../util/types'; + +export const DateAndTimeSingleDay = { + component: BookingTimeInfo, + props: { + isOrder: true, + intl: fakeIntl, + tx: createTransaction({ + customer: createUser('user1'), + provider: createUser('user2'), + listing: createListing('Listing'), + booking: createBooking('example-booking', { + displayStart: new Date(Date.UTC(2019, 8, 30, 3, 0)), + displayEnd: new Date(Date.UTC(2019, 8, 30, 4, 0)), + start: new Date(Date.UTC(2019, 8, 30, 3, 0)), + end: new Date(Date.UTC(2019, 8, 30, 4, 0)), + }), + }), + unitType: LINE_ITEM_UNITS, + dateType: 'datetime', + }, + group: 'inbox', +}; + +export const DateAndTimeMultipleDays = { + component: BookingTimeInfo, + props: { + isOrder: true, + intl: fakeIntl, + tx: createTransaction({ + customer: createUser('user1'), + provider: createUser('user2'), + listing: createListing('Listing'), + booking: createBooking('example-booking', { + displayStart: new Date(Date.UTC(2019, 8, 28, 3, 0)), + displayEnd: new Date(Date.UTC(2019, 8, 30, 5, 0)), + start: new Date(Date.UTC(2019, 8, 28, 3, 0)), + end: new Date(Date.UTC(2019, 8, 30, 5, 0)), + }), + }), + unitType: LINE_ITEM_UNITS, + dateType: 'datetime', + }, + group: 'inbox', +}; + +export const OnlyDateSingleDay = { + component: BookingTimeInfo, + props: { + isOrder: true, + intl: fakeIntl, + tx: createTransaction({ + customer: createUser('user1'), + provider: createUser('user2'), + listing: createListing('Listing'), + booking: createBooking('example-booking', { + displayStart: new Date(Date.UTC(2019, 8, 29, 3, 0)), + displayEnd: new Date(Date.UTC(2019, 8, 30, 4, 0)), + start: new Date(Date.UTC(2019, 8, 29, 3, 0)), + end: new Date(Date.UTC(2019, 8, 30, 4, 0)), + }), + }), + unitType: LINE_ITEM_DAY, + dateType: 'date', + }, + group: 'inbox', +}; + +export const OnlyDateMultipleDays = { + component: BookingTimeInfo, + props: { + isOrder: true, + intl: fakeIntl, + tx: createTransaction({ + customer: createUser('user1'), + provider: createUser('user2'), + listing: createListing('Listing'), + booking: createBooking('example-booking', { + displayStart: new Date(Date.UTC(2019, 8, 28, 3, 0)), + displayEnd: new Date(Date.UTC(2019, 8, 30, 5, 0)), + start: new Date(Date.UTC(2019, 8, 28, 3, 0)), + end: new Date(Date.UTC(2019, 8, 30, 5, 0)), + }), + }), + unitType: LINE_ITEM_DAY, + dateType: 'date', + }, + group: 'inbox', +}; + +export const OnlyDateSingleNight = { + component: BookingTimeInfo, + props: { + isOrder: true, + intl: fakeIntl, + tx: createTransaction({ + customer: createUser('user1'), + provider: createUser('user2'), + listing: createListing('Listing'), + booking: createBooking('example-booking', { + displayStart: new Date(Date.UTC(2019, 8, 29, 3, 0)), + displayEnd: new Date(Date.UTC(2019, 8, 30, 4, 0)), + start: new Date(Date.UTC(2019, 8, 29, 3, 0)), + end: new Date(Date.UTC(2019, 8, 30, 4, 0)), + }), + }), + unitType: LINE_ITEM_NIGHT, + dateType: 'date', + }, + group: 'inbox', +}; + +export const OnlyDateMultipleNights = { + component: BookingTimeInfo, + props: { + isOrder: true, + intl: fakeIntl, + tx: createTransaction({ + customer: createUser('user1'), + provider: createUser('user2'), + listing: createListing('Listing'), + booking: createBooking('example-booking', { + displayStart: new Date(Date.UTC(2019, 8, 28, 3, 0)), + displayEnd: new Date(Date.UTC(2019, 8, 30, 5, 0)), + start: new Date(Date.UTC(2019, 8, 28, 3, 0)), + end: new Date(Date.UTC(2019, 8, 30, 5, 0)), + }), + }), + unitType: LINE_ITEM_NIGHT, + dateType: 'date', + }, + group: 'inbox', +}; diff --git a/src/components/BookingTimeInfo/BookingTimeInfo.js b/src/components/BookingTimeInfo/BookingTimeInfo.js new file mode 100644 index 00000000..b90a4781 --- /dev/null +++ b/src/components/BookingTimeInfo/BookingTimeInfo.js @@ -0,0 +1,98 @@ +import React from 'react'; +import moment from 'moment'; +import { bool, oneOf } from 'prop-types'; +import classNames from 'classnames'; +import { txIsEnquired } from '../../util/transaction'; +import { dateFromAPIToLocalNoon, daysBetween, formatDateToText } from '../../util/dates'; +import { injectIntl, intlShape } from '../../util/reactIntl'; +import { + LINE_ITEM_DAY, + LINE_ITEM_NIGHT, + LINE_ITEM_UNITS, + DATE_TYPE_DATE, + DATE_TYPE_DATETIME, + propTypes, +} from '../../util/types'; + +import css from './BookingTimeInfo.css'; + +const bookingData = (unitType, tx, isOrder, intl) => { + // Attributes: displayStart and displayEnd can be used to differentiate shown time range + // from actual start and end times used for availability reservation. It can help in situations + // where there are preparation time needed between bookings. + // Read more: https://www.sharetribe.com/api-reference/#bookings + const { start, end, displayStart, displayEnd } = tx.booking.attributes; + const startDate = dateFromAPIToLocalNoon(displayStart || start); + const endDateRaw = dateFromAPIToLocalNoon(displayEnd || end); + const isDaily = unitType === LINE_ITEM_DAY; + const isNightly = unitType === LINE_ITEM_NIGHT; + const isUnits = unitType === LINE_ITEM_UNITS; + const isSingleDay = !isNightly && daysBetween(startDate, endDateRaw) <= 1; + const bookingStart = formatDateToText(intl, startDate); + // Shift the exclusive API end date with daily bookings + const endDate = + isDaily || isUnits + ? moment(endDateRaw) + .subtract(1, 'days') + .toDate() + : endDateRaw; + const bookingEnd = formatDateToText(intl, endDate); + return { bookingStart, bookingEnd, isSingleDay }; +}; + +const BookingTimeInfoComponent = props => { + const { bookingClassName, isOrder, intl, tx, unitType, dateType } = props; + const isEnquiry = txIsEnquired(tx); + + if (isEnquiry) { + return null; + } + + const bookingTimes = bookingData(unitType, tx, isOrder, intl); + + const { bookingStart, bookingEnd, isSingleDay } = bookingTimes; + + if (isSingleDay && dateType === DATE_TYPE_DATE) { + return ( +
+ {`${bookingStart.date}`} +
+ ); + } else if (dateType === DATE_TYPE_DATE) { + return ( +
+ {`${bookingStart.date} -`} + {`${bookingEnd.date}`} +
+ ); + } else if (isSingleDay && dateType === DATE_TYPE_DATETIME) { + return ( +
+ + {`${bookingStart.date}, ${bookingStart.time} - ${bookingEnd.time}`} + +
+ ); + } else { + return ( +
+ {`${bookingStart.dateAndTime} - `} + {`${bookingEnd.dateAndTime}`} +
+ ); + } +}; + +BookingTimeInfoComponent.defaultProps = { dateType: null }; + +BookingTimeInfoComponent.propTypes = { + intl: intlShape.isRequired, + isOrder: bool.isRequired, + tx: propTypes.transaction.isRequired, + unitType: propTypes.bookingUnitType.isRequired, + dateType: oneOf(DATE_TYPE_DATE, DATE_TYPE_DATETIME), +}; + +const BookingTimeInfo = injectIntl(BookingTimeInfoComponent); + +export default BookingTimeInfo; diff --git a/src/components/index.js b/src/components/index.js index 7d037632..1254210c 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -116,6 +116,7 @@ export { default as AddImages } from './AddImages/AddImages'; export { default as Avatar, AvatarMedium, AvatarLarge } from './Avatar/Avatar'; export { default as BookingBreakdown } from './BookingBreakdown/BookingBreakdown'; export { default as BookingDateRangeFilter } from './BookingDateRangeFilter/BookingDateRangeFilter'; +export { default as BookingTimeInfo } from './BookingTimeInfo/BookingTimeInfo'; export { default as BookingPanel } from './BookingPanel/BookingPanel'; export { default as Discussion } from './Discussion/Discussion'; export { default as FilterPlain } from './FilterPlain/FilterPlain'; diff --git a/src/examples.js b/src/examples.js index 5af40b8f..1ec5b9ae 100644 --- a/src/examples.js +++ b/src/examples.js @@ -5,6 +5,7 @@ import * as Avatar from './components/Avatar/Avatar.example'; import * as BookingBreakdown from './components/BookingBreakdown/BookingBreakdown.example'; import * as BookingPanel from './components/BookingPanel/BookingPanel.example'; import * as BookingDateRangeFilter from './components/BookingDateRangeFilter/BookingDateRangeFilter.example'; +import * as BookingTimeInfo from './components/BookingTimeInfo/BookingTimeInfo.example'; import * as Button from './components/Button/Button.example'; import * as ExpandingTextarea from './components/ExpandingTextarea/ExpandingTextarea.example'; import * as FieldBirthdayInput from './components/FieldBirthdayInput/FieldBirthdayInput.example'; @@ -101,6 +102,7 @@ export { BookingBreakdown, BookingDateRangeFilter, BookingDatesForm, + BookingTimeInfo, BookingPanel, Button, Colors, diff --git a/src/util/dates.js b/src/util/dates.js index 36072147..17bc71c7 100644 --- a/src/util/dates.js +++ b/src/util/dates.js @@ -257,3 +257,20 @@ export const getExclusiveEndDate = dateString => { .startOf('day') .toDate(); }; + +export const formatDateToText = (intl, date) => { + return { + date: intl.formatDate(date, { + month: 'short', + day: 'numeric', + }), + time: intl.formatDate(date, { + hour: 'numeric', + minute: 'numeric', + }), + dateAndTime: intl.formatTime(date, { + month: 'short', + day: 'numeric', + }), + }; +}; diff --git a/src/util/types.js b/src/util/types.js index 10ee1b83..c8e4133d 100644 --- a/src/util/types.js +++ b/src/util/types.js @@ -497,3 +497,7 @@ propTypes.error = shape({ }); export { propTypes }; + +// Options for showing just date or date and time on BookingTimeInfo and BookingBreakdown +export const DATE_TYPE_DATE = 'date'; +export const DATE_TYPE_DATETIME = 'datetime';