diff --git a/CHANGELOG.md b/CHANGELOG.md index d9092b53db..658a164a73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ way to update this template, but currently, we follow a pattern: ## Upcoming version 2019-XX-XX +- [change] Move `BookingTimeInfo` to separate component from `InboxPage`. Add options to show only + booking dates or booking dates and times. + [#1194](https://github.com/sharetribe/flex-template-web/pull/1194) - [add] Add new Spanish translations related to storing payment card. [#1193](https://github.com/sharetribe/flex-template-web/pull/1193) - [fix] Update yarn.lock (there was Lodash version resolution missing) diff --git a/src/components/BookingTimeInfo/BookingTimeInfo.css b/src/components/BookingTimeInfo/BookingTimeInfo.css new file mode 100644 index 0000000000..0fff0e5e8b --- /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 0000000000..41f292f46e --- /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 0000000000..b90a47816a --- /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 7d037632dd..1254210cdd 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/containers/InboxPage/InboxPage.css b/src/containers/InboxPage/InboxPage.css index c18a8d2233..817ba7d04e 100644 --- a/src/containers/InboxPage/InboxPage.css +++ b/src/containers/InboxPage/InboxPage.css @@ -273,29 +273,23 @@ } } -.bookingInfo { +.bookingInfoWrapper { + display: flex; + align-items: center; + flex-wrap: wrap; + font-size: 14px; line-height: 14px; - margin-top: 3px; + margin-top: 2px; + padding-top: 2px; @media (--viewportMedium) { + padding-top: 0px; margin-top: 8px; line-height: 16px; } } -.itemTimestamp { - /* Font */ - @apply --marketplaceH5FontStyles; - - margin-top: 0px; - margin-bottom: 0px; - @media (--viewportMedium) { - margin-top: 4px; - margin-bottom: 0; - } -} - .itemPrice { &::before { font-size: 10px; diff --git a/src/containers/InboxPage/InboxPage.js b/src/containers/InboxPage/InboxPage.js index 4207296422..26d43ab5c9 100644 --- a/src/containers/InboxPage/InboxPage.js +++ b/src/containers/InboxPage/InboxPage.js @@ -1,9 +1,8 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import { arrayOf, bool, number, oneOf, shape, string } from 'prop-types'; import { compose } from 'redux'; import { connect } from 'react-redux'; import { FormattedMessage, injectIntl, intlShape } from '../../util/reactIntl'; -import moment from 'moment'; import classNames from 'classnames'; import { txIsAccepted, @@ -15,14 +14,13 @@ import { txIsPaymentExpired, txIsPaymentPending, } from '../../util/transaction'; -import { LINE_ITEM_DAY, LINE_ITEM_UNITS, propTypes } from '../../util/types'; -import { formatMoney } from '../../util/currency'; +import { propTypes, DATE_TYPE_DATE } from '../../util/types'; import { ensureCurrentUser } from '../../util/data'; -import { dateFromAPIToLocalNoon, daysBetween } from '../../util/dates'; import { getMarketplaceEntities } from '../../ducks/marketplaceData.duck'; import { isScrollingDisabled } from '../../ducks/UI.duck'; import { Avatar, + BookingTimeInfo, NamedLink, NotificationBadge, Page, @@ -43,8 +41,6 @@ import config from '../../config'; import { loadData } from './InboxPage.duck'; import css from './InboxPage.css'; -const { arrayOf, bool, number, oneOf, shape, string } = PropTypes; - const formatDate = (intl, date) => { return { short: intl.formatDate(date, { @@ -157,33 +153,7 @@ export const txState = (intl, tx, type) => { } }; -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 isUnits = unitType === LINE_ITEM_UNITS; - const isSingleDay = isDaily && daysBetween(startDate, endDateRaw) === 1; - const bookingStart = formatDate(intl, startDate); - - // Shift the exclusive API end date with daily bookings - const endDate = - isDaily || isUnits - ? moment(endDateRaw) - .subtract(1, 'days') - .toDate() - : endDateRaw; - const bookingEnd = formatDate(intl, endDate); - const bookingPrice = isOrder ? tx.attributes.payinTotal : tx.attributes.payoutTotal; - const price = formatMoney(intl, bookingPrice); - return { bookingStart, bookingEnd, price, isSingleDay }; -}; - -// Functional component as internal helper to print BookingInfo if that is needed +// Functional component as internal helper to print BookingTimeInfo if that is needed const BookingInfoMaybe = props => { const { bookingClassName, isOrder, intl, tx, unitType } = props; const isEnquiry = txIsEnquired(tx); @@ -192,13 +162,26 @@ const BookingInfoMaybe = props => { return null; } - const { bookingStart, bookingEnd, price, isSingleDay } = bookingData(unitType, tx, isOrder, intl); - const dateInfo = isSingleDay ? bookingStart.short : `${bookingStart.short} - ${bookingEnd.short}`; + // If you want to show the booking price after the booking time on InboxPage you can + // add the price after the BookingTimeInfo component. You can get the price by uncommenting + // sthe following lines: + + // const bookingPrice = isOrder ? tx.attributes.payinTotal : tx.attributes.payoutTotal; + // const price = bookingPrice ? formatMoney(intl, bookingPrice) : null; + + // Remember to also add formatMoney function from 'util/currency.js' and add this after BookingTimeInfo: + //
{price}
return ( -
- {dateInfo} - {price} +
+
); }; diff --git a/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap b/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap index 10fbe7cf07..5f70f293b2 100644 --- a/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap +++ b/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap @@ -429,10 +429,16 @@ exports[`InboxPage matches snapshot 2`] = `
- 2017-02-15 - 2017-02-16 - - 10 - +
+ + Feb 15 - + + + Feb 16 + +
@@ -883,10 +889,16 @@ exports[`InboxPage matches snapshot 4`] = `
- 2017-02-15 - 2017-02-16 - - 9 - +
+ + Feb 15 - + + + Feb 16 + +
diff --git a/src/examples.js b/src/examples.js index 5af40b8fa8..1ec5b9ae8c 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 360721471b..17bc71c760 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 10ee1b8363..c8e4133d61 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';