diff --git a/src/Routes.js b/src/Routes.js
index 8b5ade8d..60aa7a06 100644
--- a/src/Routes.js
+++ b/src/Routes.js
@@ -1,4 +1,4 @@
-import React, { PropTypes } from 'react';
+import React, { Component, PropTypes } from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { Switch, Route, withRouter } from 'react-router-dom';
@@ -7,29 +7,80 @@ import { NamedRedirect } from './components';
import { withFlattenedRoutes } from './util/routes';
import * as propTypes from './util/propTypes';
-const Routes = props => {
- const { isAuthenticated, flattenedRoutes, staticContext } = props;
+const { bool, arrayOf, object, func, shape, string, any } = PropTypes;
- const renderComponent = (route, matchProps) => {
- const { auth, component: RouteComponent } = route;
- const { match, location } = matchProps;
- const canShowComponent = !auth || isAuthenticated;
- if (!canShowComponent) {
+class RouteComponentRenderer extends Component {
+ constructor(props) {
+ super(props);
+ this.canShowComponent = this.canShowComponent.bind(this);
+ }
+ componentDidMount() {
+ const { match, route, dispatch } = this.props;
+ const { loadData, name } = route;
+ const shouldLoadData = typeof loadData === 'function' && this.canShowComponent();
+
+ if (shouldLoadData) {
+ dispatch(loadData(match.params, {}))
+ .then(() => {
+ // eslint-disable-next-line no-console
+ console.log(`loadData success for ${name} route`);
+ })
+ .catch(e => {
+ // eslint-disable-next-line no-console
+ console.error(`loadData error for ${name} route`, e);
+ });
+ }
+ }
+ canShowComponent() {
+ const { isAuthenticated, route } = this.props;
+ const { auth } = route;
+ return !auth || isAuthenticated;
+ }
+ render() {
+ const { route, match, location, staticContext } = this.props;
+ const { component: RouteComponent } = route;
+ const canShow = this.canShowComponent();
+ if (!canShow) {
staticContext.forbidden = true;
}
- return canShowComponent
+ return canShow
?
: ;
- };
+ }
+}
- const toRouteComponent = route => (
- renderComponent(route, matchProps)}
- />
- );
+RouteComponentRenderer.propTypes = {
+ isAuthenticated: bool.isRequired,
+ route: propTypes.route.isRequired,
+ match: shape({
+ params: object.isRequired,
+ url: string.isRequired,
+ }).isRequired,
+ location: any.isRequired,
+ staticContext: object.isRequired,
+ dispatch: func.isRequired,
+};
+
+const Routes = props => {
+ const { isAuthenticated, flattenedRoutes, staticContext, dispatch } = props;
+
+ const toRouteComponent = route => {
+ const renderProps = { isAuthenticated, route, staticContext, dispatch };
+ return (
+ (
+
+ )}
+ />
+ );
+ };
return (
@@ -41,12 +92,11 @@ const Routes = props => {
Routes.defaultProps = { staticContext: {} };
-const { bool, arrayOf, object } = PropTypes;
-
Routes.propTypes = {
isAuthenticated: bool.isRequired,
flattenedRoutes: arrayOf(propTypes.route).isRequired,
staticContext: object,
+ dispatch: func.isRequired,
};
const mapStateToProps = state => ({ isAuthenticated: state.Auth.isAuthenticated });
diff --git a/src/containers/ListingPage/ListingPage.duck.js b/src/containers/ListingPage/ListingPage.duck.js
new file mode 100644
index 00000000..121b9bff
--- /dev/null
+++ b/src/containers/ListingPage/ListingPage.duck.js
@@ -0,0 +1,55 @@
+import { showListingsSuccess } from '../../ducks/sdk.duck';
+
+// ================ Action types ================ //
+
+export const SHOW_LISTING_REQUEST = 'app/ListingPage/SHOW_LISTING_REQUEST';
+export const SHOW_LISTING_ERROR = 'app/ListingPage/SHOW_LISTING_ERROR';
+
+// ================ Reducer ================ //
+
+const initialState = {
+ id: null,
+ showListingError: null,
+};
+
+const listingPageReducer = (state = initialState, action = {}) => {
+ const { type, payload } = action;
+ switch (type) {
+ case SHOW_LISTING_REQUEST:
+ return { id: payload.id, showListingError: null };
+ case SHOW_LISTING_ERROR:
+ return { showListingError: payload };
+ default:
+ return state;
+ }
+};
+
+export default listingPageReducer;
+
+// ================ Action creators ================ //
+
+export const showListingRequest = id => ({
+ type: SHOW_LISTING_REQUEST,
+ payload: { id },
+});
+
+export const showListingError = e => ({
+ type: SHOW_LISTING_ERROR,
+ error: true,
+ payload: e,
+});
+
+export const showListing = listingId =>
+ (dispatch, getState, sdk) => {
+ dispatch(showListingRequest(listingId));
+ return sdk.listings
+ .show({ id: listingId, include: ['author', 'images'] })
+ .then(data => {
+ dispatch(showListingsSuccess(data));
+ return data;
+ })
+ .catch(e => {
+ dispatch(showListingError(e));
+ throw e;
+ });
+ };
diff --git a/src/containers/ListingPage/ListingPage.js b/src/containers/ListingPage/ListingPage.js
index e2c180d0..03b2c9f3 100644
--- a/src/containers/ListingPage/ListingPage.js
+++ b/src/containers/ListingPage/ListingPage.js
@@ -3,9 +3,12 @@ import { intlShape, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { types } from 'sharetribe-sdk';
import { NamedLink, PageLayout } from '../../components';
-import { showListings, getListingsById } from '../../ducks/sdk.duck';
+import { getListingsById } from '../../ducks/sdk.duck';
+import { showListing } from './ListingPage.duck';
import css from './ListingPage.css';
+const { UUID } = types;
+
// TODO this hard coded info needs to be removed when API supports these features
const info = {
//title: 'Banyan Studios',
@@ -45,14 +48,10 @@ const info = {
// N.B. All the presentational content needs to be extracted to their own components
export class ListingPageComponent extends Component {
- componentDidMount() {
- ListingPageComponent.loadData(this.props.params.id, this.props.onLoadListing);
- }
-
render() {
- const { entitiesData, intl, params } = this.props;
- const id = new types.UUID(params.id);
- const listingsById = getListingsById(entitiesData, [id]);
+ const { params, marketplaceData, showListingError, intl } = this.props;
+ const id = new UUID(params.id);
+ const listingsById = getListingsById(marketplaceData, [id]);
const currentListing = listingsById.length > 0 ? listingsById[0] : null;
const title = currentListing ? currentListing.attributes.title : '';
@@ -140,40 +139,35 @@ export class ListingPageComponent extends Component {
const noDataMsg = { id: 'ListingPage.noListingData' };
const noDataError = ;
- const loadingOrError = entitiesData.showListingsError ? noDataError : loadingContent;
+ const loadingOrError = showListingError ? noDataError : loadingContent;
return currentListing ? pageContent : loadingOrError;
}
}
-ListingPageComponent.loadData = (id, onLoadListing) => {
- onLoadListing(id);
-};
+ListingPageComponent.defaultProps = { showListingError: null };
-ListingPageComponent.defaultProps = { listing: null };
-
-const { func, object, shape, string } = PropTypes;
+const { shape, string, object, instanceOf } = PropTypes;
ListingPageComponent.propTypes = {
- entitiesData: object.isRequired,
- intl: intlShape.isRequired,
- onLoadListing: func.isRequired,
params: shape({
id: string.isRequired,
slug: string.isRequired,
}).isRequired,
+ marketplaceData: object.isRequired,
+ showListingError: instanceOf(Error),
+ intl: intlShape.isRequired,
};
-const mapStateToProps = state => {
- const { data, ListingPage } = state;
- const entitiesData = data || {};
- return { ListingPage, entitiesData };
+const mapStateToProps = state => ({
+ marketplaceData: state.data,
+ showListingError: state.ListingPage.showListingError,
+});
+
+const ListingPage = connect(mapStateToProps)(injectIntl(ListingPageComponent));
+
+ListingPage.loadData = params => {
+ return showListing(new UUID(params.id));
};
-const mapDispatchToProps = dispatch => {
- return {
- onLoadListing: id => dispatch(showListings({ id, include: ['author', 'images'] })),
- };
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ListingPageComponent));
+export default ListingPage;
diff --git a/src/containers/ListingPage/ListingPage.test.js b/src/containers/ListingPage/ListingPage.test.js
index e9b334b3..796b3afd 100644
--- a/src/containers/ListingPage/ListingPage.test.js
+++ b/src/containers/ListingPage/ListingPage.test.js
@@ -1,19 +1,64 @@
import React from 'react';
+import { types } from 'sharetribe-sdk';
import { createListing } from '../../util/test-data';
import { fakeIntl, renderShallow } from '../../util/test-helpers';
import { ListingPageComponent } from './ListingPage';
+import { showListingsSuccess } from '../../ducks/sdk.duck';
+import { showListingRequest, showListingError, showListing } from './ListingPage.duck';
+
+const { UUID } = types;
describe('ListingPage', () => {
it('matches snapshot', () => {
- const entitiesData = { entities: { listing: { listing1: createListing('listing1') } } };
+ const marketplaceData = { entities: { listing: { listing1: createListing('listing1') } } };
const tree = renderShallow(
l}
/>,
);
expect(tree).toMatchSnapshot();
});
+
+ describe('Duck', () => {
+ it('showListing() success', () => {
+ const id = new UUID('00000000-0000-0000-0000-000000000000');
+ const dispatch = jest.fn(action => action);
+ const response = { status: 200 };
+ const show = jest.fn(() => Promise.resolve(response));
+ const sdk = { listings: { show } };
+
+ return showListing(id)(dispatch, null, sdk).then(data => {
+ expect(data).toEqual(response);
+ expect(show.mock.calls).toEqual([[{ id, include: ['author', 'images'] }]]);
+ expect(dispatch.mock.calls).toEqual([
+ [showListingRequest(id)],
+ [showListingsSuccess(data)],
+ ]);
+ });
+ });
+
+ it('showListing() error', () => {
+ const id = new UUID('00000000-0000-0000-0000-000000000000');
+ const dispatch = jest.fn(action => action);
+ const error = new Error('fail');
+ const show = jest.fn(() => Promise.reject(error));
+ const sdk = { listings: { show } };
+
+ // Calling sdk.listings.show is expected to fail now
+
+ return showListing(id)(dispatch, null, sdk).then(
+ () => {
+ throw new Error('sdk.listings.show was supposed to fail!');
+ },
+ e => {
+ expect(e).toEqual(error);
+ expect(show.mock.calls).toEqual([[{ id, include: ['author', 'images'] }]]);
+ expect(dispatch.mock.calls).toEqual([[showListingRequest(id)], [showListingError(e)]]);
+ },
+ );
+ });
+ });
});
diff --git a/src/containers/reducers.js b/src/containers/reducers.js
index 7518ccab..efeb3e58 100644
--- a/src/containers/reducers.js
+++ b/src/containers/reducers.js
@@ -3,9 +3,8 @@
* We are following Ducks module proposition:
* https://github.com/erikras/ducks-modular-redux
*/
-
-/* eslint-disable import/prefer-default-export */
import EditListingPage from './EditListingPage/EditListingPage.duck';
+import ListingPage from './ListingPage/ListingPage.duck';
import SearchPage from './SearchPage/SearchPage.duck';
-export { EditListingPage, SearchPage };
+export { EditListingPage, ListingPage, SearchPage };
diff --git a/src/routesConfiguration.js b/src/routesConfiguration.js
index c01db109..816c712a 100644
--- a/src/routesConfiguration.js
+++ b/src/routesConfiguration.js
@@ -61,6 +61,7 @@ const routesConfiguration = [
path: '/l/:slug/:id',
exact: true,
name: 'ListingPage',
+ loadData: (params, search) => ListingPage.loadData(params, search),
component: props => ,
},
{