OrderDetailsPanel updated and related child components too

This commit is contained in:
Vesa Luusua 2017-04-26 16:25:37 +03:00
parent 403f097ceb
commit b0f06f4b9a
12 changed files with 214 additions and 113 deletions

View file

@ -0,0 +1,3 @@
.avatar {
border-radius: 50%;
}

View file

@ -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 <img className={classes} src={avatarImageURL} alt={name} />;
};
const { string } = PropTypes;
Avatar.defaultProps = {
className: null,
};
Avatar.propTypes = {
className: string,
name: string.isRequired,
};
export default Avatar;

View file

@ -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 => {
? <FormattedMessage id="BookingInfo.nightCount" values={{ count: nightCount }} />
: 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 => {
<FormattedMessage id="BookingInfo.pricePerDay" />
</div>
<div className={css.priceUnitPrice}>
{formattedPrice}
{formattedUnitPrice}
</div>
</div>
<div className={css.row}>
@ -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,
};

View file

@ -6,7 +6,7 @@ exports[`BookingInfo matches snapshot 1`] = `
<div
className={undefined}>
<span>
Price per day:
Price per night:
</span>
</div>
<div
@ -29,7 +29,7 @@ exports[`BookingInfo matches snapshot 1`] = `
<div
className={undefined}>
<span>
2 days
2 nights
</span>
</div>
</div>

View file

@ -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;
}

View file

@ -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 (
<div>
<p>{addressLine1}</p>
<p>{addressLine2}</p>
<p>{phoneNumber}</p>
<p>Get directions</p>
</div>
);
};
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 = (
<NamedLink name="ListingPage" params={listingLinkParams}>
{listing.attributes.title}
</NamedLink>
);
const title = orderState === propTypes.TX_STATE_PREAUTHORIZED
? <FormattedMessage
id="OrderDetailsPanel.listingTitle"
values={{ title: listingLink }}
/>
: null;
const message = orderState === propTypes.TX_STATE_PREAUTHORIZED
? <div className={css.message}>
<div className={css.avatarWrapper}>
<Avatar name={providerName} />
</div>
<div>
<FormattedMessage id="OrderDetailsPanel.orderStatusMessage" values={{ providerName }} />
</div>
</div>
: 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
? <BookingInfo
className={css.receipt}
bookingStart={booking.attributes.start}
bookingEnd={booking.attributes.end}
unitPrice={unitPrice}
totalPrice={totalPrice}
/>
: <p className={css.error}>{'priceRequiredMessage'}</p>;
return (
<div className={className}>
<img alt={title} src={imageUrl} style={{ width: '100%' }} />
<h3>{title}</h3>
<ContactInfo {...contact} />
<p>Confirmation code {confirmationCode}</p>
<p>Cancel booking</p>
<NamedLink className={css.buttonLink} name="OrderDiscussionPage" params={{ id: orderId }}>
You have a new message!
</NamedLink>
<h1 className={css.title}>{title}</h1>
{message}
{bookingInfo}
</div>
);
};
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;

View file

@ -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(<OrderDetailsPanel {...props} />);
expect(tree).toMatchSnapshot();

View file

@ -1,36 +1,45 @@
exports[`OrderDetailsPanel matches snapshot 1`] = `
<div
className={null}>
<img
alt="Test order"
src="http://example.com/img"
style={
Object {
"width": "100%",
<h1>
<FormattedMessage
id="OrderDetailsPanel.listingTitle"
values={
Object {
"title": "listing1 title",
}
} />
</h1>
<div>
<div>
<Avatar
className={null}
name="provider first name provider last name" />
</div>
<div>
<FormattedMessage
id="OrderDetailsPanel.orderStatusMessage"
values={
Object {
"providerName": "provider first name provider last name",
}
} />
</div>
</div>
<BookingInfo
bookingEnd={2017-06-13T00:00:00.000Z}
bookingStart={2017-06-10T00:00:00.000Z}
totalPrice={
Money {
"amount": 16500,
"currency": "USD",
}
}
unitPrice={
Money {
"amount": 5500,
"currency": "USD",
}
} />
<h3>
Test order
</h3>
<ContactInfo
addressLine1="Some road 1"
addressLine2="Some city, somewhere"
phoneNumber="Some phone number" />
<p>
Confirmation code
some-test-confirmation-code
</p>
<p>
Cancel booking
</p>
<withFlattenedRoutes(withRouter(NamedLink))
name="OrderDiscussionPage"
params={
Object {
"id": "some-test-order-id",
}
}>
You have a new message!
</withFlattenedRoutes(withRouter(NamedLink))>
</div>
`;

View file

@ -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,

View file

@ -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",

View file

@ -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,
});

View file

@ -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,
};