mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-30 18:16:48 +10:00
Merge pull request #124 from sharetribe/orderpage-real-data
OrderPage: fetching actual transaction data with SDK
This commit is contained in:
commit
fb9197499a
19 changed files with 464 additions and 189 deletions
3
src/components/Avatar/Avatar.css
Normal file
3
src/components/Avatar/Avatar.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.avatar {
|
||||
border-radius: 50%;
|
||||
}
|
||||
27
src/components/Avatar/Avatar.js
Normal file
27
src/components/Avatar/Avatar.js
Normal 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;
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -1,36 +1,54 @@
|
|||
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": <withFlattenedRoutes(withRouter(NamedLink))
|
||||
name="ListingPage"
|
||||
params={
|
||||
Object {
|
||||
"id": "listing1",
|
||||
"slug": "listing1-title",
|
||||
}
|
||||
}>
|
||||
listing1 title
|
||||
</withFlattenedRoutes(withRouter(NamedLink))>,
|
||||
}
|
||||
} />
|
||||
</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>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
.title {
|
||||
margin: 1rem 1rem 2rem 1rem;
|
||||
}
|
||||
|
||||
.tabContent {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
73
src/containers/OrderPage/OrderPage.duck.js
Normal file
73
src/containers/OrderPage/OrderPage.duck.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { types } from '../../util/sdkLoader';
|
||||
import { addEntities } from '../../ducks/sdk.duck';
|
||||
import { fetchCurrentUser } from '../../ducks/user.duck';
|
||||
|
||||
// ================ Action types ================ //
|
||||
|
||||
export const FETCH_ORDER_REQUEST = 'app/InboxPage/FETCH_ORDER_REQUEST';
|
||||
export const FETCH_ORDER_SUCCESS = 'app/InboxPage/FETCH_ORDER_SUCCESS';
|
||||
export const FETCH_ORDER_ERROR = 'app/InboxPage/FETCH_ORDER_ERROR';
|
||||
|
||||
// ================ Reducer ================ //
|
||||
|
||||
const initialState = {
|
||||
fetchInProgress: false,
|
||||
fetchOrderError: null,
|
||||
transactionRef: null,
|
||||
};
|
||||
|
||||
export default function checkoutPageReducer(state = initialState, action = {}) {
|
||||
const { type, payload } = action;
|
||||
switch (type) {
|
||||
case FETCH_ORDER_REQUEST:
|
||||
return { ...state, fetchInProgress: true, fetchOrderError: null };
|
||||
case FETCH_ORDER_SUCCESS: {
|
||||
const transactionRef = { id: payload.data.data.id, type: 'transaction' };
|
||||
return { ...state, fetchInProgress: false, transactionRef };
|
||||
}
|
||||
case FETCH_ORDER_ERROR:
|
||||
console.error(payload); // eslint-disable-line
|
||||
return { ...state, fetchInProgress: false, fetchOrderError: payload };
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// ================ Action creators ================ //
|
||||
|
||||
const fetchOrderRequest = () => ({ type: FETCH_ORDER_REQUEST });
|
||||
const fetchOrderSuccess = response => ({ type: FETCH_ORDER_SUCCESS, payload: response });
|
||||
const fetchOrderError = e => ({ type: FETCH_ORDER_ERROR, error: true, payload: e });
|
||||
|
||||
// ================ Thunks ================ //
|
||||
|
||||
export const fetchOrder = id =>
|
||||
(dispatch, getState, sdk) => {
|
||||
dispatch(fetchOrderRequest());
|
||||
|
||||
return sdk.transactions
|
||||
.show({ id, include: ['provider', 'customer', 'listing', 'booking'] }, { expand: true })
|
||||
.then(response => {
|
||||
dispatch(addEntities(response));
|
||||
dispatch(fetchOrderSuccess(response));
|
||||
return response;
|
||||
})
|
||||
.catch(e => {
|
||||
dispatch(fetchOrderError(e));
|
||||
throw e;
|
||||
});
|
||||
};
|
||||
|
||||
// loadData is a collection of async calls that need to be made
|
||||
// before page has all the info it needs to render itself
|
||||
export const loadData = params =>
|
||||
dispatch => {
|
||||
const orderId = new types.UUID(params.id);
|
||||
|
||||
// Current user is needed to render Topbar
|
||||
dispatch(fetchCurrentUser());
|
||||
|
||||
// Order (i.e. transaction entity in API, but from buyers perspective) contains order details
|
||||
return dispatch(fetchOrder(orderId));
|
||||
};
|
||||
|
|
@ -1,62 +1,100 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { OrderDetailsPanel, OrderDiscussionPanel, NamedLink, PageLayout } from '../../components';
|
||||
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { OrderDetailsPanel, PageLayout } from '../../components';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import { getEntities } from '../../ducks/sdk.duck';
|
||||
import { loadData } from './OrderPage.duck';
|
||||
|
||||
import css from './OrderPage.css';
|
||||
|
||||
const OrderPage = props => {
|
||||
const { params } = props;
|
||||
const orderId = params.id;
|
||||
// Create shell objects to ensure that attributes etc. exists.
|
||||
// TODO: these could be moved to separate util file, if needed elsewhere
|
||||
const ensureTransaction = transaction => {
|
||||
const empty = {
|
||||
id: null,
|
||||
type: 'transaction',
|
||||
attributes: {},
|
||||
booking: {},
|
||||
listing: {},
|
||||
provider: {},
|
||||
};
|
||||
// assume own properties: id, type, attributes etc.
|
||||
return { ...empty, ...transaction };
|
||||
};
|
||||
|
||||
const title = 'Banyan Studios';
|
||||
const ensureBooking = booking => {
|
||||
const empty = { id: null, type: 'booking', attributes: {} };
|
||||
return { ...empty, ...booking };
|
||||
};
|
||||
|
||||
const ensureListing = listing => {
|
||||
const empty = { id: null, type: 'listing', attributes: {}, images: [] };
|
||||
return { ...empty, ...listing };
|
||||
};
|
||||
|
||||
const ensureUser = user => {
|
||||
const empty = { id: null, type: 'user', attributes: { profile: {} } };
|
||||
return { ...empty, ...user };
|
||||
};
|
||||
|
||||
// OrderPage handles data loading
|
||||
// It show loading data text or OrderDetailsPanel (and later also another panel for messages).
|
||||
export const OrderPageComponent = props => {
|
||||
const { intl, transaction } = props;
|
||||
const currentTransaction = ensureTransaction(transaction);
|
||||
const currentListing = ensureListing(currentTransaction.listing);
|
||||
const title = currentListing.attributes.title;
|
||||
|
||||
const detailsProps = {
|
||||
title,
|
||||
orderId,
|
||||
imageUrl: 'http://placehold.it/750x470',
|
||||
contact: {
|
||||
addressLine1: '350 5th Avenue',
|
||||
addressLine2: 'New York, NY 10118',
|
||||
phoneNumber: '+1 432 43184910',
|
||||
},
|
||||
confirmationCode: 'X2587X',
|
||||
totalPrice: currentTransaction.attributes.total,
|
||||
orderState: currentTransaction.attributes.state,
|
||||
listing: currentListing,
|
||||
booking: ensureBooking(currentTransaction.booking),
|
||||
provider: ensureUser(currentTransaction.provider),
|
||||
};
|
||||
|
||||
const detailsClassName = classNames(css.tabContent, {
|
||||
[css.tabContentVisible]: props.tab === 'details',
|
||||
});
|
||||
const discussionClassName = classNames(css.tabContent, {
|
||||
[css.tabContentVisible]: props.tab === 'discussion',
|
||||
});
|
||||
|
||||
const panel = currentTransaction.id
|
||||
? <OrderDetailsPanel className={detailsClassName} {...detailsProps} />
|
||||
: <h1 className={css.title}><FormattedMessage id="OrderPage.loadingData" /></h1>;
|
||||
|
||||
return (
|
||||
<PageLayout title={`Your ${title} booking is confirmed!`}>
|
||||
<NamedLink
|
||||
name="OrderDetailsPage"
|
||||
params={{ id: orderId }}
|
||||
activeClassName={css.activeTab}
|
||||
style={{ marginRight: '2rem' }}
|
||||
>
|
||||
Booking details
|
||||
</NamedLink>
|
||||
<NamedLink
|
||||
name="OrderDiscussionPage"
|
||||
activeClassName={css.activeTab}
|
||||
params={{ id: orderId }}
|
||||
>
|
||||
Discussion
|
||||
</NamedLink>
|
||||
<OrderDetailsPanel className={detailsClassName} {...detailsProps} />
|
||||
<OrderDiscussionPanel className={discussionClassName} />
|
||||
<PageLayout title={intl.formatMessage({ id: 'OrderPage.title' }, { title })}>
|
||||
{panel}
|
||||
</PageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
const { string, shape, oneOfType, number, oneOf } = PropTypes;
|
||||
OrderPageComponent.defaultProps = { transaction: null };
|
||||
|
||||
OrderPage.propTypes = {
|
||||
params: shape({ id: oneOfType([number, string]).isRequired }).isRequired,
|
||||
const { oneOf } = PropTypes;
|
||||
|
||||
OrderPageComponent.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
tab: oneOf(['details', 'discussion']).isRequired,
|
||||
transaction: propTypes.transaction,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const transactionRef = state.OrderPage.transactionRef;
|
||||
const transactions = getEntities(state.data, transactionRef ? [transactionRef] : []);
|
||||
const transaction = transactions.length > 0 ? transactions[0] : null;
|
||||
return {
|
||||
transaction,
|
||||
showOrderError: state.ListingPage.showListingError,
|
||||
currentUser: state.user.currentUser,
|
||||
};
|
||||
};
|
||||
|
||||
const OrderPage = connect(mapStateToProps)(injectIntl(OrderPageComponent));
|
||||
|
||||
OrderPage.loadData = params => {
|
||||
return loadData(params);
|
||||
};
|
||||
|
||||
export default OrderPage;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,35 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
createBooking,
|
||||
createListing,
|
||||
createTransaction,
|
||||
createUser,
|
||||
fakeIntl,
|
||||
} from '../../util/test-data';
|
||||
import { renderShallow } from '../../util/test-helpers';
|
||||
import OrderPage from './OrderPage';
|
||||
import { OrderPageComponent } from './OrderPage';
|
||||
|
||||
describe('OrderPage', () => {
|
||||
it('matches snapshot', () => {
|
||||
const tree = renderShallow(<OrderPage params={{ id: 1234 }} tab="details" />);
|
||||
const transaction = createTransaction({
|
||||
id: 'tx-order-1',
|
||||
state: 'state/preauthorized',
|
||||
booking: createBooking(
|
||||
'booking1',
|
||||
new Date(Date.UTC(2017, 5, 10)),
|
||||
new Date(Date.UTC(2017, 5, 13))
|
||||
),
|
||||
listing: createListing('listing1'),
|
||||
provider: createUser('provider1'),
|
||||
});
|
||||
|
||||
const props = {
|
||||
transaction,
|
||||
tab: 'details',
|
||||
intl: fakeIntl,
|
||||
};
|
||||
|
||||
const tree = renderShallow(<OrderPageComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,43 +1,63 @@
|
|||
exports[`OrderPage matches snapshot 1`] = `
|
||||
<Connect(withRouter(PageLayout))
|
||||
title="Your Banyan Studios booking is confirmed!">
|
||||
<withFlattenedRoutes(withRouter(NamedLink))
|
||||
name="OrderDetailsPage"
|
||||
params={
|
||||
Object {
|
||||
"id": 1234,
|
||||
}
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"marginRight": "2rem",
|
||||
}
|
||||
}>
|
||||
Booking details
|
||||
</withFlattenedRoutes(withRouter(NamedLink))>
|
||||
<withFlattenedRoutes(withRouter(NamedLink))
|
||||
name="OrderDiscussionPage"
|
||||
params={
|
||||
Object {
|
||||
"id": 1234,
|
||||
}
|
||||
}>
|
||||
Discussion
|
||||
</withFlattenedRoutes(withRouter(NamedLink))>
|
||||
title="OrderPage.title">
|
||||
<OrderDetailsPanel
|
||||
className="undefined"
|
||||
confirmationCode="X2587X"
|
||||
contact={
|
||||
booking={
|
||||
Object {
|
||||
"addressLine1": "350 5th Avenue",
|
||||
"addressLine2": "New York, NY 10118",
|
||||
"phoneNumber": "+1 432 43184910",
|
||||
"attributes": Object {
|
||||
"end": 2017-06-13T00:00:00.000Z,
|
||||
"start": 2017-06-10T00:00:00.000Z,
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "booking1",
|
||||
},
|
||||
"type": "booking",
|
||||
}
|
||||
}
|
||||
imageUrl="http://placehold.it/750x470"
|
||||
orderId={1234}
|
||||
title="Banyan Studios" />
|
||||
<OrderDiscussionPanel
|
||||
className="" />
|
||||
className="undefined"
|
||||
listing={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"address": "listing1 address",
|
||||
"description": "listing1 description",
|
||||
"geolocation": LatLng {
|
||||
"lat": 40,
|
||||
"lng": 60,
|
||||
},
|
||||
"price": Money {
|
||||
"amount": 5500,
|
||||
"currency": "USD",
|
||||
},
|
||||
"title": "listing1 title",
|
||||
},
|
||||
"author": null,
|
||||
"id": UUID {
|
||||
"uuid": "listing1",
|
||||
},
|
||||
"images": Array [],
|
||||
"type": "listing",
|
||||
}
|
||||
}
|
||||
orderState="state/preauthorized"
|
||||
provider={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"profile": Object {
|
||||
"firstName": "provider1 first name",
|
||||
"lastName": "provider1 last name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "provider1",
|
||||
},
|
||||
"type": "user",
|
||||
}
|
||||
}
|
||||
totalPrice={
|
||||
Money {
|
||||
"amount": 1000,
|
||||
"currency": "USD",
|
||||
}
|
||||
} />
|
||||
</Connect(withRouter(PageLayout))>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import CheckoutPage from './CheckoutPage/CheckoutPage.duck';
|
|||
import EditListingPage from './EditListingPage/EditListingPage.duck';
|
||||
import InboxPage from './InboxPage/InboxPage.duck';
|
||||
import ListingPage from './ListingPage/ListingPage.duck';
|
||||
import OrderPage from './OrderPage/OrderPage.duck';
|
||||
import SearchPage from './SearchPage/SearchPage.duck';
|
||||
|
||||
export { CheckoutPage, EditListingPage, InboxPage, ListingPage, SearchPage };
|
||||
export { CheckoutPage, EditListingPage, InboxPage, ListingPage, OrderPage, SearchPage };
|
||||
|
|
|
|||
|
|
@ -183,6 +183,7 @@ const routesConfiguration = [
|
|||
exact: true,
|
||||
name: 'OrderDetailsPage',
|
||||
component: props => <OrderPage {...props} tab="details" />,
|
||||
loadData: params => OrderPage.loadData(params),
|
||||
},
|
||||
{
|
||||
path: '/order/:id/discussion',
|
||||
|
|
@ -190,6 +191,7 @@ const routesConfiguration = [
|
|||
exact: true,
|
||||
name: 'OrderDiscussionPage',
|
||||
component: props => <OrderPage {...props} tab="discussion" />,
|
||||
loadData: params => OrderPage.loadData(params),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,10 @@
|
|||
"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",
|
||||
"OrderPage.loadingData": "Loading order data",
|
||||
"OrderPage.title": "Order details for ${title}",
|
||||
"PageLayout.authInfoFailed": "Could not get authentication information.",
|
||||
"PageLayout.logoutFailed": "Logout failed. Please try again.",
|
||||
"PaginationLinks.previous": "Previous page",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue