mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-29 05:00:43 +10:00
Merge pull request #118 from sharetribe/submit-booking-dates-branchfix
Submit booking dates branchfix
This commit is contained in:
commit
a444e94827
19 changed files with 331 additions and 70 deletions
|
|
@ -63,8 +63,8 @@ describe('Application', () => {
|
|||
const urlRedirects = {
|
||||
'/l/new': '/login',
|
||||
'/l/listing-title-slug/1234/edit': '/login',
|
||||
'/l/listing-title-slug/1234/checkout': '/login',
|
||||
'/u/1234/edit': '/login',
|
||||
'/checkout': '/login',
|
||||
'/orders': '/login',
|
||||
'/sales': '/login',
|
||||
'/order/1234': '/login',
|
||||
|
|
|
|||
21
src/components/AuthorInfo/AuthorInfo.css
Normal file
21
src/components/AuthorInfo/AuthorInfo.css
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
.root {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.authorDetails {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.avatarWrapper {
|
||||
display: block;
|
||||
flex-basis: 44px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
.avatar {
|
||||
border-radius: 22px;
|
||||
}
|
||||
35
src/components/AuthorInfo/AuthorInfo.js
Normal file
35
src/components/AuthorInfo/AuthorInfo.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import css from './AuthorInfo.css';
|
||||
|
||||
const AuthorInfo = props => {
|
||||
const { className, author } = props;
|
||||
const classes = classNames(css.root, className);
|
||||
const currentAuthor = { id: null, type: 'user', attributes: {}, ...author };
|
||||
|
||||
const authorName = currentAuthor.attributes.profile
|
||||
? `${currentAuthor.attributes.profile.firstName} ${currentAuthor.attributes.profile.lastName}`
|
||||
: '';
|
||||
const authorAvatar = 'https://placehold.it/44x44';
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className={css.avatarWrapper}>
|
||||
<img className={css.avatar} src={authorAvatar} alt={authorName} />
|
||||
</div>
|
||||
<div className={css.authorDetails}>
|
||||
<span className={css.authorName}>
|
||||
<FormattedMessage id="AuthorInfo.host" values={{ authorName }} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const { string } = PropTypes;
|
||||
AuthorInfo.defaultProps = { className: null };
|
||||
AuthorInfo.propTypes = { author: propTypes.user.isRequired, className: string };
|
||||
|
||||
export default AuthorInfo;
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import AddImages from './AddImages/AddImages';
|
||||
import AuthorInfo from './AuthorInfo/AuthorInfo';
|
||||
import BookingInfo from './BookingInfo/BookingInfo';
|
||||
import Button from './Button/Button';
|
||||
import CurrencyInput from './CurrencyInput/CurrencyInput';
|
||||
|
|
@ -28,6 +29,7 @@ import StripeBankAccountToken from './StripeBankAccountToken/StripeBankAccountTo
|
|||
|
||||
export {
|
||||
AddImages,
|
||||
AuthorInfo,
|
||||
BookingInfo,
|
||||
Button,
|
||||
CurrencyInput,
|
||||
|
|
|
|||
|
|
@ -58,8 +58,9 @@ export const BookingDatesFormComponent = props => {
|
|||
submitting,
|
||||
} = props;
|
||||
const placeholderText = intl.formatMessage({ id: 'BookingDatesForm.placeholder' });
|
||||
const bookingStartLabel = intl.formatMessage({ id: 'BookingDatesForm.bookingStartTitle'});
|
||||
const bookingEndLabel = intl.formatMessage({ id: 'BookingDatesForm.bookingEndTitle'});
|
||||
const bookingStartLabel = intl.formatMessage({ id: 'BookingDatesForm.bookingStartTitle' });
|
||||
const bookingEndLabel = intl.formatMessage({ id: 'BookingDatesForm.bookingEndTitle' });
|
||||
const priceRequiredMessage = intl.formatMessage({ id: 'BookingDatesForm.priceRequired' });
|
||||
const requiredMessage = intl.formatMessage({ id: 'BookingDatesForm.requiredDate' });
|
||||
|
||||
// A day is outside range if it is between today and booking end date (if end date has been chosen)
|
||||
|
|
@ -72,7 +73,9 @@ export const BookingDatesFormComponent = props => {
|
|||
: {};
|
||||
|
||||
// A day is outside range if it is after booking start date (or today if none is chosen)
|
||||
const startOfBookingEndRange = bookingStart ? moment(bookingStart) : moment();
|
||||
const startOfBookingEndRange = bookingStart
|
||||
? moment(bookingStart).add(1, 'days')
|
||||
: moment().add(1, 'days');
|
||||
const isOutsideRangeEnd = bookingStart
|
||||
? { isOutsideRange: day => !isInclusivelyAfterDay(day, startOfBookingEndRange) }
|
||||
: {};
|
||||
|
|
@ -84,9 +87,9 @@ export const BookingDatesFormComponent = props => {
|
|||
bookingEnd={bookingEnd}
|
||||
unitPrice={price}
|
||||
/>
|
||||
: null;
|
||||
: <p className={css.error}>{priceRequiredMessage}</p>;
|
||||
|
||||
const invalid = !(bookingStart && bookingEnd);
|
||||
const invalid = !(bookingStart && bookingEnd && price);
|
||||
|
||||
return (
|
||||
<form className={className} onSubmit={handleSubmit}>
|
||||
|
|
@ -119,10 +122,12 @@ export const BookingDatesFormComponent = props => {
|
|||
);
|
||||
};
|
||||
|
||||
BookingDatesFormComponent.defaultProps = { price: null };
|
||||
|
||||
BookingDatesFormComponent.propTypes = {
|
||||
...formPropTypes,
|
||||
intl: intlShape.isRequired,
|
||||
price: instanceOf(types.Money).isRequired,
|
||||
price: instanceOf(types.Money),
|
||||
};
|
||||
|
||||
const formName = 'bookingDates';
|
||||
|
|
|
|||
|
|
@ -2,6 +2,14 @@
|
|||
margin: 1rem;
|
||||
}
|
||||
|
||||
.authorContainer {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.receipt {
|
||||
margin: 1rem 1rem 2rem 1rem;
|
||||
}
|
||||
|
||||
.payment {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { pick } from 'lodash';
|
||||
|
||||
// ================ Action types ================ //
|
||||
|
||||
export const SET_INITAL_VALUES = 'app/CheckoutPage/SET_INITIAL_VALUES';
|
||||
|
||||
export const INITIATE_ORDER_REQUEST = 'app/CheckoutPage/INITIATE_ORDER_REQUEST';
|
||||
export const INITIATE_ORDER_SUCCESS = 'app/CheckoutPage/INITIATE_ORDER_SUCCESS';
|
||||
export const INITIATE_ORDER_ERROR = 'app/CheckoutPage/INITIATE_ORDER_ERROR';
|
||||
|
|
@ -7,12 +11,16 @@ export const INITIATE_ORDER_ERROR = 'app/CheckoutPage/INITIATE_ORDER_ERROR';
|
|||
// ================ Reducer ================ //
|
||||
|
||||
const initialState = {
|
||||
bookingDates: null,
|
||||
initiateOrderError: null,
|
||||
listing: null,
|
||||
};
|
||||
|
||||
export default function checkoutPageReducer(state = initialState, action = {}) {
|
||||
const { type, payload } = action;
|
||||
switch (type) {
|
||||
case SET_INITAL_VALUES:
|
||||
return { ...state, ...payload };
|
||||
case INITIATE_ORDER_REQUEST:
|
||||
return { ...state, initiateOrderError: null };
|
||||
case INITIATE_ORDER_SUCCESS:
|
||||
|
|
@ -25,8 +33,15 @@ export default function checkoutPageReducer(state = initialState, action = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
// ================ Selectors ================ //
|
||||
|
||||
// ================ Action creators ================ //
|
||||
|
||||
export const setInitialValues = initialValues => ({
|
||||
type: SET_INITAL_VALUES,
|
||||
payload: pick(initialValues, Object.keys(initialState)),
|
||||
});
|
||||
|
||||
const initiateOrderRequest = () => ({ type: INITIATE_ORDER_REQUEST });
|
||||
|
||||
const initiateOrderSuccess = orderId => ({
|
||||
|
|
@ -59,3 +74,5 @@ export const initiateOrder = params =>
|
|||
throw e;
|
||||
});
|
||||
};
|
||||
|
||||
// ================ Thunk ================ //
|
||||
|
|
|
|||
|
|
@ -3,32 +3,20 @@ import { compose } from 'redux';
|
|||
import { connect } from 'react-redux';
|
||||
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { types } from '../../util/sdkLoader';
|
||||
import { pathByRouteName } from '../../util/routes';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import { withFlattenedRoutes } from '../../util/contextHelpers';
|
||||
import { PageLayout } from '../../components';
|
||||
import { AuthorInfo, BookingInfo, NamedRedirect, PageLayout } from '../../components';
|
||||
import { StripePaymentForm } from '../../containers';
|
||||
import { initiateOrder } from './CheckoutPage.duck';
|
||||
import { initiateOrder, setInitialValues } from './CheckoutPage.duck';
|
||||
|
||||
import css from './CheckoutPage.css';
|
||||
|
||||
const { UUID, LatLng } = types;
|
||||
|
||||
const bookingStart = new Date(2017, 4, 18);
|
||||
const bookingEnd = new Date(2017, 4, 19);
|
||||
|
||||
const listing = {
|
||||
id: new UUID('927a30a2-3a69-4b0d-9c2e-a41744488703'),
|
||||
type: 'listing',
|
||||
attributes: {
|
||||
title: 'Example listing',
|
||||
description: 'Listing description here.',
|
||||
address: 'Helsinki, Finland',
|
||||
geolocation: new LatLng(60.16985569999999, 24.93837899999994),
|
||||
},
|
||||
const ensureListingProperties = listing => {
|
||||
const empty = { id: null, type: 'listing', attributes: {}, author: {}, images: [] };
|
||||
// assume own properties: id, type, attributes etc.
|
||||
return { ...empty, ...listing };
|
||||
};
|
||||
const imageUrl = 'https://placehold.it/750x470';
|
||||
|
||||
export class CheckoutPageComponent extends Component {
|
||||
constructor(props) {
|
||||
|
|
@ -36,18 +24,15 @@ export class CheckoutPageComponent extends Component {
|
|||
this.state = { submitting: false };
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
handleSubmit(cardToken) {
|
||||
if (this.state.submitting) {
|
||||
return;
|
||||
}
|
||||
this.setState({ submitting: true });
|
||||
const { sendOrderRequest, history, flattenedRoutes } = this.props;
|
||||
const params = {
|
||||
listingId: listing.id,
|
||||
bookingStart,
|
||||
bookingEnd,
|
||||
cardToken,
|
||||
};
|
||||
const { bookingDates, flattenedRoutes, history, sendOrderRequest, listing } = this.props;
|
||||
const params = { listingId: listing.id, cardToken, ...bookingDates };
|
||||
|
||||
sendOrderRequest(params)
|
||||
.then(orderId => {
|
||||
this.setState({ submitting: false });
|
||||
|
|
@ -60,15 +45,27 @@ export class CheckoutPageComponent extends Component {
|
|||
this.setState({ submitting: false });
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { initiateOrderError, intl } = this.props;
|
||||
const { bookingDates, initiateOrderError, intl, listing, params } = this.props;
|
||||
const { bookingStart, bookingEnd } = bookingDates || {};
|
||||
const currentListing = ensureListingProperties(listing);
|
||||
const price = currentListing.attributes.price;
|
||||
|
||||
if (!listing || !price) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Listing price is undefined for listing (${currentListing.id}). Redirecting to SearchPage.`
|
||||
);
|
||||
return <NamedRedirect name="ListingPage" params={params} />;
|
||||
}
|
||||
|
||||
const title = intl.formatMessage(
|
||||
{
|
||||
id: 'CheckoutPage.title',
|
||||
},
|
||||
{
|
||||
listingTitle: listing.attributes.title,
|
||||
listingTitle: currentListing.attributes.title,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -81,7 +78,13 @@ export class CheckoutPageComponent extends Component {
|
|||
return (
|
||||
<PageLayout title={title}>
|
||||
<h1 className={css.title}>{title}</h1>
|
||||
<img alt={listing.attributes.title} src={imageUrl} style={{ width: '100%' }} />
|
||||
<AuthorInfo author={currentListing.author} className={css.authorContainer} />
|
||||
<BookingInfo
|
||||
className={css.receipt}
|
||||
bookingStart={bookingStart}
|
||||
bookingEnd={bookingEnd}
|
||||
unitPrice={price}
|
||||
/>
|
||||
<section className={css.payment}>
|
||||
{errorMessage}
|
||||
<h2 className={css.paymentTitle}>
|
||||
|
|
@ -97,12 +100,25 @@ export class CheckoutPageComponent extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
CheckoutPageComponent.defaultProps = { initiateOrderError: null };
|
||||
CheckoutPageComponent.defaultProps = {
|
||||
bookingDates: null,
|
||||
initiateOrderError: null,
|
||||
listing: null,
|
||||
};
|
||||
|
||||
const { func, shape, arrayOf, instanceOf } = PropTypes;
|
||||
const { arrayOf, func, instanceOf, shape, string } = PropTypes;
|
||||
|
||||
CheckoutPageComponent.propTypes = {
|
||||
bookingDates: shape({
|
||||
bookingStart: instanceOf(Date).isRequired,
|
||||
bookingEnd: instanceOf(Date).isRequired,
|
||||
}),
|
||||
initiateOrderError: instanceOf(Error),
|
||||
listing: propTypes.listing,
|
||||
params: shape({
|
||||
id: string,
|
||||
slug: string,
|
||||
}).isRequired,
|
||||
sendOrderRequest: func.isRequired,
|
||||
|
||||
// from injectIntl
|
||||
|
|
@ -117,9 +133,10 @@ CheckoutPageComponent.propTypes = {
|
|||
flattenedRoutes: arrayOf(propTypes.route).isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
initiateOrderError: state.CheckoutPage.initiateOrderError,
|
||||
});
|
||||
const mapStateToProps = state => {
|
||||
const { initiateOrderError, listing, bookingDates } = state.CheckoutPage;
|
||||
return { initiateOrderError, listing, bookingDates };
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
sendOrderRequest: params => dispatch(initiateOrder(params)),
|
||||
|
|
@ -132,4 +149,8 @@ const CheckoutPage = compose(
|
|||
injectIntl
|
||||
)(CheckoutPageComponent);
|
||||
|
||||
CheckoutPage.setInitialValues = initialValues => setInitialValues(initialValues);
|
||||
|
||||
CheckoutPage.displayName = 'CheckoutPage';
|
||||
|
||||
export default CheckoutPage;
|
||||
|
|
|
|||
|
|
@ -1,19 +1,65 @@
|
|||
import React from 'react';
|
||||
import { renderShallow } from '../../util/test-helpers';
|
||||
import { fakeIntl } from '../../util/test-data';
|
||||
import { createUser, createListing, fakeIntl } from '../../util/test-data';
|
||||
import { CheckoutPageComponent } from './CheckoutPage';
|
||||
import checkoutPageReducer, { SET_INITAL_VALUES, setInitialValues } from './CheckoutPage.duck';
|
||||
|
||||
const noop = () => null;
|
||||
|
||||
describe('CheckoutPage', () => {
|
||||
it('matches snapshot', () => {
|
||||
const props = {
|
||||
intl: fakeIntl,
|
||||
sendOrderRequest: noop,
|
||||
history: { push: noop },
|
||||
bookingDates: {
|
||||
bookingStart: new Date('Fri, 14 Apr 2017 GMT'),
|
||||
bookingEnd: new Date('Sun, 16 Apr 2017 GMT'),
|
||||
},
|
||||
flattenedRoutes: [],
|
||||
history: { push: noop },
|
||||
intl: fakeIntl,
|
||||
listing: { ...createListing('listing1'), author: createUser('author') },
|
||||
params: { id: 'listing1', slug: 'listing1' },
|
||||
sendOrderRequest: noop,
|
||||
};
|
||||
const tree = renderShallow(<CheckoutPageComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('Duck', () => {
|
||||
it('ActionCreator: setInitialValues(initialValues)', () => {
|
||||
const author = createUser('author1');
|
||||
const listing = { ...createListing('00000000-0000-0000-0000-000000000000'), author };
|
||||
const bookingDates = {
|
||||
bookingStart: new Date('Fri, 14 Apr 2017 GMT'),
|
||||
bookingEnd: new Date('Sun, 16 Apr 2017 GMT'),
|
||||
};
|
||||
const expectedAction = {
|
||||
type: SET_INITAL_VALUES,
|
||||
payload: { listing, bookingDates },
|
||||
};
|
||||
|
||||
expect(setInitialValues({ listing, bookingDates })).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
describe('Reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
const initialValues = {
|
||||
initiateOrderError: null,
|
||||
listing: null,
|
||||
bookingDates: null,
|
||||
};
|
||||
expect(checkoutPageReducer(undefined, {})).toEqual(initialValues);
|
||||
});
|
||||
|
||||
it('should handle SET_INITAL_VALUES', () => {
|
||||
const author = createUser('author1');
|
||||
const listing = { ...createListing('00000000-0000-0000-0000-000000000000'), author };
|
||||
const bookingDates = {
|
||||
bookingStart: new Date('Fri, 14 Apr 2017 GMT'),
|
||||
bookingEnd: new Date('Sun, 16 Apr 2017 GMT'),
|
||||
};
|
||||
const payload = { listing, bookingDates };
|
||||
expect(checkoutPageReducer({}, { type: SET_INITAL_VALUES, payload })).toEqual(payload);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,12 +4,31 @@ exports[`CheckoutPage matches snapshot 1`] = `
|
|||
<h1>
|
||||
CheckoutPage.title
|
||||
</h1>
|
||||
<img
|
||||
alt="Example listing"
|
||||
src="https://placehold.it/750x470"
|
||||
style={
|
||||
<AuthorInfo
|
||||
author={
|
||||
Object {
|
||||
"width": "100%",
|
||||
"attributes": Object {
|
||||
"email": "author@example.com",
|
||||
"profile": Object {
|
||||
"firstName": "author first name",
|
||||
"lastName": "author last name",
|
||||
"slug": "author-slug",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "author",
|
||||
},
|
||||
"type": "user",
|
||||
}
|
||||
}
|
||||
className={null} />
|
||||
<BookingInfo
|
||||
bookingEnd={2017-04-16T00:00:00.000Z}
|
||||
bookingStart={2017-04-14T00:00:00.000Z}
|
||||
unitPrice={
|
||||
Money {
|
||||
"amount": 5500,
|
||||
"currency": "USD",
|
||||
}
|
||||
} />
|
||||
<section>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import { intlShape, injectIntl } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { union, without } from 'lodash';
|
||||
import config from '../../config';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import { types } from '../../util/sdkLoader';
|
||||
import { createSlug } from '../../util/urlHelpers';
|
||||
import { convertMoneyToNumber } from '../../util/currency';
|
||||
import { createResourceLocatorString, findRouteByRouteName } from '../../util/routes';
|
||||
import { Button, Map, ModalInMobile, PageLayout } from '../../components';
|
||||
import { BookingDatesForm } from '../../containers';
|
||||
import { getListingsById } from '../../ducks/sdk.duck';
|
||||
|
|
@ -16,6 +20,12 @@ const MODAL_BREAKPOINT = 2500;
|
|||
|
||||
const { UUID } = types;
|
||||
|
||||
const denormaliseListing = (marketplaceData, params) => {
|
||||
const id = new UUID(params.id);
|
||||
const listingsById = getListingsById(marketplaceData, [id]);
|
||||
return listingsById.length > 0 ? listingsById[0] : null;
|
||||
};
|
||||
|
||||
const priceData = (price, currencyConfig, intl) => {
|
||||
if (price && price.currency === currencyConfig.currency) {
|
||||
const priceAsNumber = convertMoneyToNumber(price, currencyConfig.subUnitDivisor);
|
||||
|
|
@ -46,14 +56,24 @@ export class ListingPageComponent extends Component {
|
|||
}
|
||||
|
||||
onSubmit(values) {
|
||||
const { marketplaceData, params } = this.props;
|
||||
const id = new UUID(params.id);
|
||||
const listingsById = getListingsById(marketplaceData, [id]);
|
||||
const currentListing = listingsById.length > 0 ? listingsById[0] : null;
|
||||
const { dispatch, flattenedRoutes, history, marketplaceData, params } = this.props;
|
||||
const listing = denormaliseListing(marketplaceData, params);
|
||||
|
||||
this.setState({ isBookingModalOpenOnMobile: false });
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Submitting: bookedDates', values, 'and price', currentListing.attributes.price);
|
||||
|
||||
// Customize checkout page state with current listing and selected bookingDates
|
||||
const { setInitialValues } = findRouteByRouteName('CheckoutPage', flattenedRoutes);
|
||||
dispatch(setInitialValues({ listing, bookingDates: values }));
|
||||
|
||||
// Redirect to CheckoutPage
|
||||
history.push(
|
||||
createResourceLocatorString(
|
||||
'CheckoutPage',
|
||||
flattenedRoutes,
|
||||
{ id: listing.id.uuid, slug: createSlug(listing.attributes.title) },
|
||||
{}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
togglePageClassNames(className, addClass = true) {
|
||||
|
|
@ -70,9 +90,7 @@ export class ListingPageComponent extends Component {
|
|||
render() {
|
||||
const { params, marketplaceData, showListingError, intl } = this.props;
|
||||
const currencyConfig = config.currencyConfig;
|
||||
const id = new UUID(params.id);
|
||||
const listingsById = getListingsById(marketplaceData, [id]);
|
||||
const currentListing = listingsById.length > 0 ? listingsById[0] : null;
|
||||
const currentListing = denormaliseListing(marketplaceData, params);
|
||||
|
||||
const attributes = currentListing ? currentListing.attributes : {};
|
||||
const {
|
||||
|
|
@ -158,9 +176,17 @@ ListingPageComponent.defaultProps = {
|
|||
tab: 'listing',
|
||||
};
|
||||
|
||||
const { instanceOf, object, oneOf, shape, string } = PropTypes;
|
||||
const { arrayOf, func, instanceOf, object, oneOf, shape, string } = PropTypes;
|
||||
|
||||
ListingPageComponent.propTypes = {
|
||||
// from connect
|
||||
dispatch: func.isRequired,
|
||||
flattenedRoutes: arrayOf(propTypes.route).isRequired,
|
||||
// from withRouter
|
||||
history: shape({
|
||||
push: func.isRequired,
|
||||
}).isRequired,
|
||||
// from injectIntl
|
||||
intl: intlShape.isRequired,
|
||||
marketplaceData: object.isRequired,
|
||||
params: shape({
|
||||
|
|
@ -176,7 +202,7 @@ const mapStateToProps = state => ({
|
|||
showListingError: state.ListingPage.showListingError,
|
||||
});
|
||||
|
||||
const ListingPage = connect(mapStateToProps)(injectIntl(ListingPageComponent));
|
||||
const ListingPage = connect(mapStateToProps)(withRouter(injectIntl(ListingPageComponent)));
|
||||
|
||||
ListingPage.loadData = params => {
|
||||
const id = new UUID(params.id);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ describe('ListingPage', () => {
|
|||
it('matches snapshot', () => {
|
||||
const marketplaceData = { entities: { listing: { listing1: createListing('listing1') } } };
|
||||
const props = {
|
||||
dispatch: () => console.log('Dispatch called'),
|
||||
flattenedRoutes: [],
|
||||
location: { search: '' },
|
||||
history: {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
* We are following Ducks module proposition:
|
||||
* https://github.com/erikras/ducks-modular-redux
|
||||
*/
|
||||
import CheckoutPage from './CheckoutPage/CheckoutPage.duck';
|
||||
import EditListingPage from './EditListingPage/EditListingPage.duck';
|
||||
import ListingPage from './ListingPage/ListingPage.duck';
|
||||
import SearchPage from './SearchPage/SearchPage.duck';
|
||||
import CheckoutPage from './CheckoutPage/CheckoutPage.duck';
|
||||
|
||||
export { EditListingPage, ListingPage, SearchPage, CheckoutPage };
|
||||
export { CheckoutPage, EditListingPage, ListingPage, SearchPage };
|
||||
|
|
|
|||
|
|
@ -75,6 +75,14 @@ const routesConfiguration = [
|
|||
loadData: (params, search) => ListingPage.loadData(params, search),
|
||||
component: props => <ListingPage {...props} tab="book" />,
|
||||
},
|
||||
{
|
||||
path: '/l/:slug/:id/checkout',
|
||||
auth: true,
|
||||
exact: true,
|
||||
name: 'CheckoutPage',
|
||||
setInitialValues: initialValues => CheckoutPage.setInitialValues(initialValues),
|
||||
component: props => <CheckoutPage {...props} />,
|
||||
},
|
||||
{
|
||||
auth: true,
|
||||
path: '/l/new',
|
||||
|
|
@ -115,13 +123,6 @@ const routesConfiguration = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/checkout',
|
||||
auth: true,
|
||||
exact: true,
|
||||
name: 'CheckoutPage',
|
||||
component: props => <CheckoutPage {...props} />,
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
exact: true,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
{
|
||||
"AuthenticationPage.loginFailed": "The email and password you entered did not match our records. Please double-check and try again.",
|
||||
"AuthenticationPage.loginRequiredFor": "You must log in to view the page at",
|
||||
"AuthorInfo.host": "Hosted by {authorName}",
|
||||
"BookingDatesForm.bookingEndTitle": "End date",
|
||||
"BookingDatesForm.bookingStartTitle": "Start date",
|
||||
"BookingDatesForm.placeholder": "mm/dd/yyyy",
|
||||
"BookingDatesForm.priceRequired": "Oops, this listing has no price!",
|
||||
"BookingDatesForm.requiredDate": "Oops, make sure your date is correct!",
|
||||
"BookingDatesForm.requestToBook": "Request to book",
|
||||
"BookingDatesForm.youWontBeChargedInfo": "You won't be charged yet",
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ export const listing = shape({
|
|||
description: string.isRequired,
|
||||
address: string.isRequired,
|
||||
geolocation: latlng.isRequired,
|
||||
price: money,
|
||||
}),
|
||||
author: user,
|
||||
images: arrayOf(image),
|
||||
|
|
|
|||
|
|
@ -83,3 +83,22 @@ export const createResourceLocatorString = (
|
|||
const path = pathByRouteName(routeName, flattenedRoutes, pathParams);
|
||||
return `${path}${includeSearchQuery}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find component related to route name
|
||||
* E.g. `const PageComponent = findComponentByRouteName('CheckoutPage', routes);`
|
||||
* Then we can call static methods of given component:
|
||||
* `dispatch(PageComponent.setInitialValues({ listing, bookingDates }));`
|
||||
*
|
||||
* @param {String} nameToFind - Route name
|
||||
* @param {Array<{ route }>} flattenedRoutes - Route configuration as flattened array.
|
||||
*
|
||||
* @return {Route} - Route that matches the given route name.
|
||||
*/
|
||||
export const findRouteByRouteName = (nameToFind, flattenedRoutes) => {
|
||||
const route = findRouteByName(nameToFind, flattenedRoutes);
|
||||
if (!route) {
|
||||
throw new Error(`Component "${nameToFind}" was not found.`);
|
||||
}
|
||||
return route;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { RoutesProvider } from '../components';
|
|||
import routesConfiguration from '../routesConfiguration';
|
||||
import { renderDeep, renderShallow } from './test-helpers';
|
||||
import * as propTypes from './propTypes';
|
||||
import { createResourceLocatorString, flattenRoutes } from './routes';
|
||||
import { createResourceLocatorString, flattenRoutes, findRouteByRouteName } from './routes';
|
||||
|
||||
const { arrayOf } = PropTypes;
|
||||
|
||||
|
|
@ -73,4 +73,19 @@ describe('util/routes.js', () => {
|
|||
).toEqual('/l/nice-listing/1234?extrainfo=true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findRouteByRouteName', () => {
|
||||
const flattenedRoutes = flattenRoutes(routesConfiguration);
|
||||
it('should return CheckoutPage route', () => {
|
||||
const foundRoute = findRouteByRouteName('CheckoutPage', flattenedRoutes);
|
||||
expect(foundRoute.name).toEqual('CheckoutPage');
|
||||
expect(typeof foundRoute.setInitialValues).toEqual('function');
|
||||
});
|
||||
|
||||
it('should throw exception for non-existing route (BlaaBlaaPage)', () => {
|
||||
expect(() => findRouteByRouteName('BlaaBlaaPage', flattenedRoutes)).toThrowError(
|
||||
'Component "BlaaBlaaPage" was not found.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,28 @@ export const createUser = id => ({
|
|||
},
|
||||
});
|
||||
|
||||
// Create a user that conforms to the util/propTypes user schema
|
||||
export const createImage = id => ({
|
||||
id: new UUID(id),
|
||||
type: 'image',
|
||||
attributes: {
|
||||
sizes: [
|
||||
{
|
||||
name: 'square',
|
||||
height: 408,
|
||||
width: 408,
|
||||
url: 'https://placehold.it/408x408',
|
||||
},
|
||||
{
|
||||
name: 'square2x',
|
||||
height: 816,
|
||||
width: 816,
|
||||
url: 'https://placehold.it/816x816',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// Create a user that conforms to the util/propTypes listing schema
|
||||
export const createListing = id => ({
|
||||
id: new UUID(id),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue