diff --git a/src/components/Avatar/Avatar.css b/src/components/Avatar/Avatar.css new file mode 100644 index 00000000..6cf812bf --- /dev/null +++ b/src/components/Avatar/Avatar.css @@ -0,0 +1,3 @@ +.avatar { + border-radius: 50%; +} diff --git a/src/components/Avatar/Avatar.js b/src/components/Avatar/Avatar.js new file mode 100644 index 00000000..ae94ca83 --- /dev/null +++ b/src/components/Avatar/Avatar.js @@ -0,0 +1,27 @@ +import React, { PropTypes } from 'react'; +import classNames from 'classnames'; + +import css from './Avatar.css'; + +const Avatar = props => { + const { className, name } = props; + + // TODO hard coded placeholder image need to be changed to something else + const avatarImageURL = 'http://placehold.it/44x44'; + const classes = classNames(css.avatar, className); + + return {name}; +}; + +const { string } = PropTypes; + +Avatar.defaultProps = { + className: null, +}; + +Avatar.propTypes = { + className: string, + name: string.isRequired, +}; + +export default Avatar; diff --git a/src/components/BookingInfo/BookingInfo.js b/src/components/BookingInfo/BookingInfo.js index 6b9a8ff7..4daaddcf 100644 --- a/src/components/BookingInfo/BookingInfo.js +++ b/src/components/BookingInfo/BookingInfo.js @@ -13,7 +13,7 @@ import { types } from '../../util/sdkLoader'; import css from './BookingInfo.css'; const BookingInfoComponent = props => { - const { bookingStart, bookingEnd, className, intl, unitPrice } = props; + const { bookingStart, bookingEnd, className, intl, totalPrice, unitPrice } = props; const hasSelectedDays = bookingStart && bookingEnd; const bookingPeriod = hasSelectedDays @@ -30,13 +30,20 @@ const BookingInfoComponent = props => { ? : null; - const priceAsNumber = convertMoneyToNumber(unitPrice, config.currencyConfig.subUnitDivisor); - const formattedPrice = intl.formatNumber(priceAsNumber, config.currencyConfig); - const totalPriceAsNumber = hasSelectedDays - ? new Decimal(priceAsNumber).times(nightCount).toNumber() + const currencyConfig = config.currencyConfig; + const subUnitDivisor = currencyConfig.subUnitDivisor; + const unitPriceAsNumber = convertMoneyToNumber(unitPrice, subUnitDivisor); + const formattedUnitPrice = intl.formatNumber(unitPriceAsNumber, currencyConfig); + const calculatedTotalPriceAsNumber = !totalPrice && hasSelectedDays + ? new Decimal(unitPriceAsNumber).times(nightCount).toNumber() : null; - const formattedTotalPrice = hasSelectedDays - ? intl.formatNumber(totalPriceAsNumber, config.currencyConfig) + + // Total price can be given (when it comes from API) or calculated based on unit price and nights + const totalPriceAsNumber = totalPrice + ? convertMoneyToNumber(totalPrice, subUnitDivisor) + : calculatedTotalPriceAsNumber; + const formattedTotalPrice = totalPriceAsNumber + ? intl.formatNumber(totalPriceAsNumber, currencyConfig) : null; return ( @@ -46,7 +53,7 @@ const BookingInfoComponent = props => {
- {formattedPrice} + {formattedUnitPrice}
@@ -72,7 +79,12 @@ const BookingInfoComponent = props => { ); }; -BookingInfoComponent.defaultProps = { bookingStart: null, bookingEnd: null, className: '' }; +BookingInfoComponent.defaultProps = { + bookingStart: null, + bookingEnd: null, + className: '', + totalPrice: null, +}; const { instanceOf, string } = PropTypes; @@ -81,6 +93,7 @@ BookingInfoComponent.propTypes = { bookingEnd: instanceOf(Date), className: string, intl: intlShape.isRequired, + totalPrice: instanceOf(types.Money), unitPrice: instanceOf(types.Money).isRequired, }; diff --git a/src/components/BookingInfo/__snapshots__/BookingInfo.test.js.snap b/src/components/BookingInfo/__snapshots__/BookingInfo.test.js.snap index fba864c1..13494205 100644 --- a/src/components/BookingInfo/__snapshots__/BookingInfo.test.js.snap +++ b/src/components/BookingInfo/__snapshots__/BookingInfo.test.js.snap @@ -6,7 +6,7 @@ exports[`BookingInfo matches snapshot 1`] = `
- Price per day: + Price per night:
- 2 days + 2 nights
diff --git a/src/components/OrderDetailsPanel/OrderDetailsPanel.css b/src/components/OrderDetailsPanel/OrderDetailsPanel.css index bd0370c6..2c0ff102 100644 --- a/src/components/OrderDetailsPanel/OrderDetailsPanel.css +++ b/src/components/OrderDetailsPanel/OrderDetailsPanel.css @@ -1,21 +1,17 @@ -.buttonLink { - display: block; - width: 100%; - font-size: 1.4rem; - padding: 0.5rem; - margin: 1rem 0; - background-color: #eee; - border: 1px solid #ddd; - cursor: pointer; - - text-align: center; - text-decoration: none; - color: #000; - - &:hover { - background-color: #ddd; - } - &:active { - background-color: #ccc; - } +.title, +.receipt { + margin: 1rem 1rem 2rem 1rem; +} + +.message { + display: flex; + margin: 1rem 1rem 2rem 1rem; +} + +.avatarWrapper { + display: block; + flex-basis: 44px; + width: 44px; + height: 44px; + margin-right: 1rem; } diff --git a/src/components/OrderDetailsPanel/OrderDetailsPanel.js b/src/components/OrderDetailsPanel/OrderDetailsPanel.js index 1823babb..28e8de87 100644 --- a/src/components/OrderDetailsPanel/OrderDetailsPanel.js +++ b/src/components/OrderDetailsPanel/OrderDetailsPanel.js @@ -1,53 +1,76 @@ import React, { PropTypes } from 'react'; -import { NamedLink } from '../../components'; +import { FormattedMessage } from 'react-intl'; +import * as propTypes from '../../util/propTypes'; +import { createSlug } from '../../util/urlHelpers'; +import { types } from '../../util/sdkLoader'; +import { Avatar, BookingInfo, NamedLink } from '../../components'; import css from './OrderDetailsPanel.css'; -const ContactInfo = props => { - const { addressLine1, addressLine2, phoneNumber } = props; - return ( -
-

{addressLine1}

-

{addressLine2}

-

{phoneNumber}

-

Get directions

-
- ); -}; - -const { number, oneOfType, string, object } = PropTypes; - -ContactInfo.propTypes = { - addressLine1: string.isRequired, - addressLine2: string.isRequired, - phoneNumber: string.isRequired, -}; - const OrderDetailsPanel = props => { - const { className, orderId, title, imageUrl, contact, confirmationCode } = props; + const { className, totalPrice, orderState, booking, listing, provider } = props; + const { firstName, lastName } = provider.attributes.profile; + const providerName = firstName ? `${firstName} ${lastName}` : ''; + + const listingLinkParams = { id: listing.id.uuid, slug: createSlug(listing.attributes.title) }; + const listingLink = ( + + {listing.attributes.title} + + ); + + const title = orderState === propTypes.TX_STATE_PREAUTHORIZED + ? + : null; + + const message = orderState === propTypes.TX_STATE_PREAUTHORIZED + ?
+
+ +
+
+ +
+
+ : null; + + // TODO We can't use price from listing, since that might have changed. + // When API includes unit price and possible additional fees, we need to change this. + const unitPrice = listing.attributes.price; + + const bookingInfo = unitPrice + ? + :

{'priceRequiredMessage'}

; + return (
- {title} -

{title}

- -

Confirmation code {confirmationCode}

-

Cancel booking

- - You have a new message! - +

{title}

+ {message} + {bookingInfo}
); }; OrderDetailsPanel.defaultProps = { className: null }; +const { instanceOf, string } = PropTypes; + OrderDetailsPanel.propTypes = { className: string, - orderId: oneOfType([string, number]).isRequired, - title: string.isRequired, - imageUrl: string.isRequired, - contact: object.isRequired, - confirmationCode: string.isRequired, + totalPrice: instanceOf(types.Money).isRequired, + orderState: string.isRequired, + booking: propTypes.booking.isRequired, + listing: propTypes.listing.isRequired, + provider: propTypes.user.isRequired, }; export default OrderDetailsPanel; diff --git a/src/components/OrderDetailsPanel/OrderDetailsPanel.test.js b/src/components/OrderDetailsPanel/OrderDetailsPanel.test.js index ee0ae9e4..46a1300f 100644 --- a/src/components/OrderDetailsPanel/OrderDetailsPanel.test.js +++ b/src/components/OrderDetailsPanel/OrderDetailsPanel.test.js @@ -1,25 +1,23 @@ import React from 'react'; +import { types } from '../../util/sdkLoader'; +import { createBooking, createListing, createUser } from '../../util/test-data'; import { renderShallow } from '../../util/test-helpers'; import OrderDetailsPanel from './OrderDetailsPanel.js'; describe('OrderDetailsPanel', () => { it('matches snapshot', () => { + const { Money } = types; const props = { - orderId: 'some-test-order-id', - title: 'Test order', - imageUrl: 'http://example.com/img', - info: { - pricePerDay: '10$', - bookingPeriod: 'some booking period', - bookingDuration: 'some booking duration', - total: '100$', - }, - contact: { - addressLine1: 'Some road 1', - addressLine2: 'Some city, somewhere', - phoneNumber: 'Some phone number', - }, - confirmationCode: 'some-test-confirmation-code', + commission: null, + totalPrice: new Money(16500, 'USD'), + orderState: 'state/preauthorized', + booking: createBooking( + 'booking1', + new Date(Date.UTC(2017, 5, 10)), + new Date(Date.UTC(2017, 5, 13)) + ), + listing: createListing('listing1'), + provider: createUser('provider'), }; const tree = renderShallow(); expect(tree).toMatchSnapshot(); diff --git a/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap b/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap index ccbf920c..bcac3fb9 100644 --- a/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap +++ b/src/components/OrderDetailsPanel/__snapshots__/OrderDetailsPanel.test.js.snap @@ -1,36 +1,45 @@ exports[`OrderDetailsPanel matches snapshot 1`] = `
- Test order + + +
+
+ +
+
+ +
+
+ -

- Test order -

- -

- Confirmation code - some-test-confirmation-code -

-

- Cancel booking -

- - You have a new message! -
`; diff --git a/src/components/index.js b/src/components/index.js index ad6dab7b..7f4b322b 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,5 +1,6 @@ import AddImages from './AddImages/AddImages'; import AuthorInfo from './AuthorInfo/AuthorInfo'; +import Avatar from './Avatar/Avatar'; import BookingInfo from './BookingInfo/BookingInfo'; import Button from './Button/Button'; import CurrencyInput from './CurrencyInput/CurrencyInput'; @@ -31,6 +32,7 @@ import StripeBankAccountToken from './StripeBankAccountToken/StripeBankAccountTo export { AddImages, AuthorInfo, + Avatar, BookingInfo, Button, CurrencyInput, diff --git a/src/translations/en.json b/src/translations/en.json index e1919915..cbc36530 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -12,8 +12,9 @@ "BookingDatesForm.youWontBeChargedInfo": "You won't be charged yet", "BookingInfo.bookingPeriod": "{bookingStart} - {bookingEnd}", "BookingInfo.bookingPeriodLabel": "Booking period:", - "BookingInfo.nightCount": "{count, number} {count, plural, one {day} other {days}}", - "BookingInfo.pricePerDay":"Price per day:", + "BookingInfo.commission": "Charged commission:", + "BookingInfo.nightCount": "{count, number} {count, plural, one {night} other {nights}}", + "BookingInfo.pricePerDay":"Price per night:", "BookingInfo.total": "Total:", "CheckoutPage.initiateOrderError": "Payment request failed. Please try again.", "CheckoutPage.paymentInfo": "You'll only be charged if your request is accepted by the provider.", @@ -54,6 +55,8 @@ "ListingPage.loadingListingData": "Loading listing data", "ListingPage.noListingData": "Could not find listing data", "ModalInMobile.closeModal": "Close modal", + "OrderDetailsPanel.listingTitle": "You have requested to book {title}", + "OrderDetailsPanel.orderStatusMessage": "{providerName} has been notified about the booking request, so sit back and relax", "PageLayout.authInfoFailed": "Could not get authentication information.", "PageLayout.logoutFailed": "Logout failed. Please try again.", "PaginationLinks.previous": "Previous page", diff --git a/src/util/propTypes.js b/src/util/propTypes.js index 2ea7a658..acff214c 100644 --- a/src/util/propTypes.js +++ b/src/util/propTypes.js @@ -118,6 +118,16 @@ export const listing = shape({ images: arrayOf(image), }); +// Denormalised booking object +export const booking = shape({ + id: uuid.isRequired, + type: value('booking').isRequired, + attributes: shape({ + end: instanceOf(Date).isRequired, + start: instanceOf(Date).isRequired, + }), +}); + export const TX_STATE_ACCEPTED = 'state/accepted'; export const TX_STATE_REJECTED = 'state/rejected'; export const TX_STATE_PREAUTHORIZED = 'state/preauthorized'; @@ -135,6 +145,8 @@ export const transaction = shape({ state: oneOf(TX_STATES).isRequired, total: any, // ??? }), + booking, + listing, customer: user, provider: user, }); diff --git a/src/util/test-data.js b/src/util/test-data.js index 8dacf15e..31bf44c2 100644 --- a/src/util/test-data.js +++ b/src/util/test-data.js @@ -2,6 +2,16 @@ import { types } from './sdkLoader'; const { UUID, LatLng, Money } = types; +// Create a booking that conforms to the util/propTypes booking schema +export const createBooking = (id, startDateInUTC, endDateInUTC) => ({ + id: new UUID(id), + type: 'booking', + attributes: { + start: startDateInUTC, + end: endDateInUTC, + }, +}); + // Create a user that conforms to the util/propTypes user schema export const createUser = id => ({ id: new UUID(id), @@ -62,6 +72,9 @@ export const createTransaction = options => { const { id, state = 'state/preauthorized', + total = new Money(1000, 'USD'), + booking = null, + listing = null, customer = null, provider = null, lastTransitionedAt = new Date(Date.UTC(2017, 5, 1)), @@ -74,8 +87,10 @@ export const createTransaction = options => { createdAt: new Date(Date.UTC(2017, 4, 1)), lastTransitionedAt, state, - total: null, + total, }, + booking, + listing, customer, provider, };