diff --git a/src/app.test.js b/src/app.test.js index 4e67a4ac..95cc1e08 100644 --- a/src/app.test.js +++ b/src/app.test.js @@ -25,7 +25,7 @@ describe('Application', () => { const urlTitles = { '/': 'Landing page', '/s': 'Search page', - '/l/listing-title-slug/1234': 'Banyan Studios 55€ / day', + '/l/listing-title-slug/1234': 'Loading listing data', '/u/1234': 'Profile page with display name: 1234', '/checkout/1234': 'Book Banyan Studios (1234)', '/login': 'Authentication page: login tab', diff --git a/src/containers/ListingPage/ListingPage.js b/src/containers/ListingPage/ListingPage.js index 4fdfcb5b..32955485 100644 --- a/src/containers/ListingPage/ListingPage.js +++ b/src/containers/ListingPage/ListingPage.js @@ -1,9 +1,14 @@ -import React from 'react'; +import React, { Component, PropTypes } from 'react'; +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 css from './ListingPage.css'; +// TODO this hard coded info needs to be removed when API supports these features const info = { - title: 'Banyan Studios', + //title: 'Banyan Studios', price: '55\u20AC / day', images: [ { id: 1, title: 'img1', imageUrl: 'http://placehold.it/750x470' }, @@ -12,18 +17,18 @@ const info = { { id: 4, title: 'img4', imageUrl: 'http://placehold.it/750x470' }, { id: 5, title: 'img5', imageUrl: 'http://placehold.it/750x470' }, ], - description: ` -

- Organic Music Production in a Sustainable, Ethical and Professional Studio. -

-

- Social Permaculture focuses on nourishing our environment with abundance instead of depleting it. -

-

- Banyan Studios is a comfortable, conscious, inspiring and creative retreat for musicians trying to convey their message through state of the art Audio & Video. -

- https://vimeo.com/168106603 - `, + // description: ` + //

+ // Organic Music Production in a Sustainable, Ethical and Professional Studio. + //

+ //

+ // Social Permaculture focuses on nourishing our environment with abundance instead of depleting it. + //

+ //

+ // Banyan Studios is a comfortable, conscious, inspiring and creative retreat for musicians trying to convey their message through state of the art Audio & Video. + //

+ // https://vimeo.com/168106603 + // `, reviews: [ { id: 1, @@ -39,67 +44,122 @@ const info = { }; // N.B. All the presentational content needs to be extracted to their own components -const ListingPage = () => ( - -
- {info.images[0].title} -
- {info.images.slice(1).map(image => ( -
-
- {image.title} -
-
- ))} -
-
- {/* eslint-disable react/no-danger */} -
- {/* eslint-enable react/no-danger */} -
-

Here will be filters (or dragons)

-

Studio type

-

Amenities

-

Additional Services Available

-

Studio hours: 10am - 6pm

-
- -

Contact studio

-
-
-

Studio reviews (1)

-
- {info.reviews.map(review => ( -
-

{review.review}

-
-
- {review.reviewer.name} -
-
- {review.reviewer.name} - {review.reviewer.date} - -
-
- review: {review.rating}/5 -
-
-
- ))} -
-
-
Map
- - {`Book ${info.title}`} - - -); +export class ListingPageComponent extends Component { + componentDidMount() { + ListingPageComponent.loadData(this.props.params.id, this.props.onLoadListing); + } -export default ListingPage; + render() { + const { entitiesData, intl, params } = this.props; + const id = new types.UUID(params.id); + const listingsById = getListingsById(entitiesData, [id]); + const currentListing = listingsById.length > 0 ? listingsById[0] : null; + + const title = currentListing ? currentListing.attributes.title : ''; + const description = currentListing ? currentListing.attributes.description : ''; + + const pageContent = ( + +
+ {info.images[0].title} +
+ {info.images.slice(1).map(image => ( +
+
+ {image.title} +
+
+ ))} +
+
+ {/* eslint-disable react/no-danger */} +
+ {/* eslint-enable react/no-danger */} +
+

Here will be filters (or dragons)

+

Studio type

+

Amenities

+

Additional Services Available

+

Studio hours: 10am - 6pm

+
+ +

Contact studio

+
+
+

Studio reviews (1)

+
+ {info.reviews.map(review => ( +
+

{review.review}

+
+
+ {review.reviewer.name} +
+
+ {review.reviewer.name} + {review.reviewer.date} + +
+
+ review: {review.rating}/5 +
+
+
+ ))} +
+
+
Map
+ + {`Book ${title}`} + + + ); + + const loadingPageMsg = { + id: 'ListingPage.loadingListingData', + defaultMessage: 'Loading listing data', + }; + + const loadingContent = ; + + return currentListing ? pageContent : loadingContent; + } +} + +ListingPageComponent.loadData = (id, onLoadListing) => { + onLoadListing(id); +}; + +ListingPageComponent.defaultProps = { listing: null }; + +const { func, object, shape, string } = PropTypes; + +ListingPageComponent.propTypes = { + entitiesData: object.isRequired, + intl: intlShape.isRequired, + onLoadListing: func.isRequired, + params: shape({ + id: string.isRequired, + slug: string.isRequired, + }).isRequired, +}; + +const mapStateToProps = state => { + const { data, ListingPage } = state; + const entitiesData = data || {}; + return { ListingPage, entitiesData }; +}; + +const mapDispatchToProps = dispatch => { + return { + onLoadListing: id => dispatch(showListings({ id, include: ['author'] })), + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ListingPageComponent)); diff --git a/src/containers/ListingPage/ListingPage.test.js b/src/containers/ListingPage/ListingPage.test.js index 9b7c1e52..e9b334b3 100644 --- a/src/containers/ListingPage/ListingPage.test.js +++ b/src/containers/ListingPage/ListingPage.test.js @@ -1,10 +1,19 @@ import React from 'react'; -import { renderShallow } from '../../util/test-helpers'; -import ListingPage from './ListingPage'; +import { createListing } from '../../util/test-data'; +import { fakeIntl, renderShallow } from '../../util/test-helpers'; +import { ListingPageComponent } from './ListingPage'; describe('ListingPage', () => { it('matches snapshot', () => { - const tree = renderShallow(); + const entitiesData = { entities: { listing: { listing1: createListing('listing1') } } }; + const tree = renderShallow( + l} + />, + ); expect(tree).toMatchSnapshot(); }); }); diff --git a/src/containers/ListingPage/__snapshots__/ListingPage.test.js.snap b/src/containers/ListingPage/__snapshots__/ListingPage.test.js.snap index f70e5731..8601c5e5 100644 --- a/src/containers/ListingPage/__snapshots__/ListingPage.test.js.snap +++ b/src/containers/ListingPage/__snapshots__/ListingPage.test.js.snap @@ -1,6 +1,6 @@ exports[`ListingPage matches snapshot 1`] = ` + title="listing1 title 55€ / day">
img1 - Organic Music Production in a Sustainable, Ethical and Professional Studio. -

-

- Social Permaculture focuses on nourishing our environment with abundance instead of depleting it. -

-

- Banyan Studios is a comfortable, conscious, inspiring and creative retreat for musicians trying to convey their message through state of the art Audio & Video. -

- https://vimeo.com/168106603 - ", + "__html": "listing1 description", } } />
@@ -129,7 +118,7 @@ exports[`ListingPage matches snapshot 1`] = ` "id": 12345, } }> - Book Banyan Studios + Book listing1 title `; diff --git a/src/ducks/sdk.duck.js b/src/ducks/sdk.duck.js index 89adb04c..491fa213 100644 --- a/src/ducks/sdk.duck.js +++ b/src/ducks/sdk.duck.js @@ -107,8 +107,14 @@ export default function sdkReducer(state = initialState, action = {}) { * @param {Object} data the state part of the Redux store for this SDK reducer * @param {Array} listingIds listing IDs to select from the store */ -export const getListingsById = (data, listingIds) => - denormalisedEntities(data.entities, 'listing', listingIds); +export const getListingsById = (data, listingIds) => { + try { + return denormalisedEntities(data.entities, 'listing', listingIds); + } catch (e) { + console.error(`Could not denormalise entities with given ids (${listingIds.map(id => id.uuid)}). Error: ${e}`); + return []; + } +}; // ================ Action creators ================ // diff --git a/src/translations/en.json b/src/translations/en.json index 5ce5a195..858a8517 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3,5 +3,6 @@ "HeroSearchForm.placeholder": "Location search (soon)", "HeroSearchForm.search": "Search", "HeroSection.title": "Book Studiotime anywhere", - "HeroSection.subTitle": "The largest online community to rent music studios" + "HeroSection.subTitle": "The largest online community to rent music studios", + "ListingPage.loadingListingData": "Loading listing data" } diff --git a/src/util/test-helpers.js b/src/util/test-helpers.js index e2021dac..ef40478e 100644 --- a/src/util/test-helpers.js +++ b/src/util/test-helpers.js @@ -46,3 +46,15 @@ export const renderDeep = component => { ); return comp.toJSON(); }; + +// Create fake Internalization object to help with shallow rendering. +export const fakeIntl = { + formatDate: d => d, + formatHTMLMessage: d => d, + formatMessage: msg => msg.defaultMessage, + formatNumber: d => d, + formatPlural: d => d, + formatRelative: d => d, + formatTime: d => d, + now: d => d, +};