From 9d86f365faa18a1406847b00c363becbf54f17af Mon Sep 17 00:00:00 2001 From: Kimmo Puputti Date: Fri, 15 Sep 2017 16:47:46 +0300 Subject: [PATCH] Handle 404 in listing page --- src/app.test.js | 2 +- src/containers/ListingPage/ListingPage.css | 9 +++ .../ListingPage/ListingPage.duck.js | 1 - src/containers/ListingPage/ListingPage.js | 72 +++++++++++++------ .../ListingPage/ListingPage.test.js | 26 +++---- src/containers/NotFoundPage/NotFoundPage.css | 3 + src/containers/NotFoundPage/NotFoundPage.js | 23 ++++-- .../NotFoundPage/NotFoundPage.test.js | 2 + .../__snapshots__/NotFoundPage.test.js.snap | 13 ++-- src/translations/en.json | 54 +++++++------- 10 files changed, 133 insertions(+), 72 deletions(-) create mode 100644 src/containers/NotFoundPage/NotFoundPage.css diff --git a/src/app.test.js b/src/app.test.js index a06a30bd..ecc17904 100644 --- a/src/app.test.js +++ b/src/app.test.js @@ -39,7 +39,7 @@ describe('Application', () => { '/s/listings': 'Search page: listings', '/s/filters': 'Search page: filters', '/s/map': 'Search page: map', - '/l/listing-title-slug/1234': 'Loading listing data', + '/l/listing-title-slug/1234': 'Loading listing…', '/u/1234': 'Profile page with display name: 1234', '/login': 'Login', '/signup': 'Sign up', diff --git a/src/containers/ListingPage/ListingPage.css b/src/containers/ListingPage/ListingPage.css index 06778c26..ad5081ca 100644 --- a/src/containers/ListingPage/ListingPage.css +++ b/src/containers/ListingPage/ListingPage.css @@ -8,6 +8,15 @@ } } +.loadingText { + margin: 24px; +} + +.errorText { + color: var(--failColor); + margin: 24px; +} + .threeToTwoWrapper { /* Layout */ display: block; diff --git a/src/containers/ListingPage/ListingPage.duck.js b/src/containers/ListingPage/ListingPage.duck.js index 7230fa28..96bfea08 100644 --- a/src/containers/ListingPage/ListingPage.duck.js +++ b/src/containers/ListingPage/ListingPage.duck.js @@ -52,6 +52,5 @@ export const showListing = listingId => }) .catch(e => { dispatch(showListingError(e)); - throw e; }); }; diff --git a/src/containers/ListingPage/ListingPage.js b/src/containers/ListingPage/ListingPage.js index 5e6ed1d6..6887f8c5 100644 --- a/src/containers/ListingPage/ListingPage.js +++ b/src/containers/ListingPage/ListingPage.js @@ -22,6 +22,7 @@ import { ResponsiveImage, Topbar, NamedLink, + NamedRedirect, Modal, ImageCarousel, } from '../../components'; @@ -175,12 +176,57 @@ export class ListingPageComponent extends Component { title = '', } = currentListing.attributes; - if (!currentListing.id && showListingError) { - const noDataMsg = { id: 'ListingPage.noListingData' }; - return ; + const topbar = ( + + ); + + const loadingTitle = intl.formatMessage({ + id: 'ListingPage.loadingListingTitle', + }); + const errorTitle = intl.formatMessage({ + id: 'ListingPage.errorLoadingListingTitle', + }); + + if (showListingError && showListingError.status === 404) { + // 404 listing not found + + return ; + } else if (showListingError) { + // Other error in fetching listing + + return ( + + {topbar} +

+ +

+
+ ); } else if (!currentListing.id) { - const loadingPageMsg = { id: 'ListingPage.loadingListingData' }; - return ; + // Still loading the listing + + return ( + + {topbar} +

+ +

+
+ ); } const hasImages = currentListing.images && currentListing.images.length > 0; @@ -291,21 +337,7 @@ export class ListingPageComponent extends Component { title={`${title} ${formattedPrice}`} scrollingDisabled={scrollingDisabled} > - + {topbar}
diff --git a/src/containers/ListingPage/ListingPage.test.js b/src/containers/ListingPage/ListingPage.test.js index 6db933bd..623b028f 100644 --- a/src/containers/ListingPage/ListingPage.test.js +++ b/src/containers/ListingPage/ListingPage.test.js @@ -72,22 +72,16 @@ describe('ListingPage', () => { // 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', 'author.profileImage', 'images'] }], - ]); - expect(dispatch.mock.calls).toEqual([ - [showListingRequest(id)], - [expect.anything()], // fetchCurrentUser() call - [showListingError(e)], - ]); - } - ); + return showListing(id)(dispatch, null, sdk).then(data => { + expect(show.mock.calls).toEqual([ + [{ id, include: ['author', 'author.profileImage', 'images'] }], + ]); + expect(dispatch.mock.calls).toEqual([ + [showListingRequest(id)], + [expect.anything()], // fetchCurrentUser() call + [showListingError(error)], + ]); + }); }); }); diff --git a/src/containers/NotFoundPage/NotFoundPage.css b/src/containers/NotFoundPage/NotFoundPage.css new file mode 100644 index 00000000..704a9004 --- /dev/null +++ b/src/containers/NotFoundPage/NotFoundPage.css @@ -0,0 +1,3 @@ +.root { + padding: 0 24px; +} diff --git a/src/containers/NotFoundPage/NotFoundPage.js b/src/containers/NotFoundPage/NotFoundPage.js index 80cd65fd..c0f2069e 100644 --- a/src/containers/NotFoundPage/NotFoundPage.js +++ b/src/containers/NotFoundPage/NotFoundPage.js @@ -1,4 +1,5 @@ import React, { Component, PropTypes } from 'react'; +import { injectIntl, intlShape, FormattedMessage } from 'react-intl'; import { compose } from 'redux'; import { connect } from 'react-redux'; import { withRouter } from 'react-router-dom'; @@ -6,7 +7,9 @@ import * as propTypes from '../../util/propTypes'; import { sendVerificationEmail } from '../../ducks/user.duck'; import { logout, authenticationInProgress } from '../../ducks/Auth.duck'; import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck'; -import { NamedLink, PageLayout, Topbar } from '../../components'; +import { PageLayout, Topbar } from '../../components'; + +import css from './NotFoundPage.css'; export class NotFoundPageComponent extends Component { componentWillMount() { @@ -34,10 +37,15 @@ export class NotFoundPageComponent extends Component { sendVerificationEmailInProgress, sendVerificationEmailError, onResendVerificationEmail, + intl, } = this.props; + const title = intl.formatMessage({ + id: 'NotFoundPage.title', + }); + return ( - + - Home +
+

+ +

+
); } @@ -89,6 +101,9 @@ NotFoundPageComponent.propTypes = { // context object from StaticRouter, injected by the withRouter wrapper staticContext: object, + // from injectIntl + intl: intlShape.isRequired, + // from withRouter history: shape({ push: func.isRequired, @@ -130,7 +145,7 @@ const mapDispatchToProps = dispatch => ({ onResendVerificationEmail: () => dispatch(sendVerificationEmail()), }); -const NotFoundPage = compose(connect(mapStateToProps, mapDispatchToProps), withRouter)( +const NotFoundPage = compose(connect(mapStateToProps, mapDispatchToProps), withRouter, injectIntl)( NotFoundPageComponent ); diff --git a/src/containers/NotFoundPage/NotFoundPage.test.js b/src/containers/NotFoundPage/NotFoundPage.test.js index d6d99a88..867e5f05 100644 --- a/src/containers/NotFoundPage/NotFoundPage.test.js +++ b/src/containers/NotFoundPage/NotFoundPage.test.js @@ -1,5 +1,6 @@ import React from 'react'; import { renderShallow } from '../../util/test-helpers'; +import { fakeIntl } from '../../util/test-data'; import { NotFoundPageComponent } from './NotFoundPage'; const noop = () => null; @@ -19,6 +20,7 @@ describe('NotFoundPageComponent', () => { onManageDisableScrolling={noop} sendVerificationEmailInProgress={false} onResendVerificationEmail={noop} + intl={fakeIntl} /> ); expect(tree).toMatchSnapshot(); diff --git a/src/containers/NotFoundPage/__snapshots__/NotFoundPage.test.js.snap b/src/containers/NotFoundPage/__snapshots__/NotFoundPage.test.js.snap index 8078c745..e87253ea 100644 --- a/src/containers/NotFoundPage/__snapshots__/NotFoundPage.test.js.snap +++ b/src/containers/NotFoundPage/__snapshots__/NotFoundPage.test.js.snap @@ -2,7 +2,7 @@ exports[`NotFoundPageComponent matches snapshot 1`] = ` + title="NotFoundPage.title"> - - Home - +
+

+ +

+
`; diff --git a/src/translations/en.json b/src/translations/en.json index 153d4680..773c8847 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -157,10 +157,12 @@ "ListingPage.ctaButtonMessage": "Request to book", "ListingPage.descriptionTitle": "About this sauna", "ListingPage.editListing": "Edit listing", + "ListingPage.errorLoadingListingMessage": "Could not load listing. Please try again.", + "ListingPage.errorLoadingListingTitle": "Error in loading listing", "ListingPage.hostedBy": "Hosted by {name}", - "ListingPage.loadingListingData": "Loading listing data", + "ListingPage.loadingListingMessage": "Loading listing…", + "ListingPage.loadingListingTitle": "Loading listing…", "ListingPage.locationTitle": "Location", - "ListingPage.noListingData": "Could not find listing data", "ListingPage.ownClosedListing": "Your listing has been closed and can't be booked.", "ListingPage.ownListing": "This is your own listing.", "ListingPage.perNight": "per night", @@ -169,8 +171,8 @@ "LoginForm.emailPlaceholder": "john.doe@example.com", "LoginForm.emailRequired": "This field is required", "LoginForm.forgotPassword": "Forgot your password?", - "LoginForm.logIn": "Log in", "LoginForm.forgotPasswordHelp": "No problem.", + "LoginForm.logIn": "Log in", "LoginForm.passwordLabel": "Password", "LoginForm.passwordPlaceholder": "Enter your password…", "LoginForm.passwordRequired": "This field is required", @@ -193,6 +195,8 @@ "MapPriceMarker.unsupportedPrice": "({currency})", "Modal.close": "CLOSE", "Modal.closeModal": "Close modal", + "NotFoundPage.heading": "Oops, could not find the page you were looking for.", + "NotFoundPage.title": "Page not found", "OrderDetailsPanel.bannedUserDisplayName": "Banned user", "OrderDetailsPanel.bookingBreakdownTitle": "Booking breakdown", "OrderDetailsPanel.deletedListingOrderTitle": "a listing", @@ -219,6 +223,28 @@ "PaginationLinks.next": "Next page", "PaginationLinks.previous": "Previous page", "PaginationLinks.toPage": "Go to page {page}", + "PasswordRecoveryForm.emailLabel": "Email", + "PasswordRecoveryForm.emailNotFound": "Hmm. We didn't find an account with that email. Please double-check your email and try again.", + "PasswordRecoveryForm.emailPlaceholder": "john.doe@example.com", + "PasswordRecoveryForm.emailRequired": "This field is required", + "PasswordRecoveryForm.loginLinkInfo": "Suddenly remembered your password? {loginLink}", + "PasswordRecoveryForm.loginLinkText": "Go log in.", + "PasswordRecoveryForm.sendInstructions": "Send instructions", + "PasswordRecoveryPage.actionFailedMessage": "Something went wrong. Please refresh the page and try again.", + "PasswordRecoveryPage.actionFailedTitle": "Whoops!", + "PasswordRecoveryPage.emailNotVerifiedContactAdmin": "To resolve the issue, please contact Saunatime administrators.", + "PasswordRecoveryPage.emailNotVerifiedMessage": "Unfortunately, we cannot recover your password, because your email {initialEmailText} hasn't been verified.", + "PasswordRecoveryPage.emailNotVerifiedTitle": "We have bad news", + "PasswordRecoveryPage.emailSubmittedMessage": "We have sent the instructions for resetting your password to {submittedEmailText}.", + "PasswordRecoveryPage.emailSubmittedTitle": "Check your inbox", + "PasswordRecoveryPage.fixEmailInfo": "Whoops, typo in your email? {fixEmailLink}", + "PasswordRecoveryPage.fixEmailLinkText": "Fix it.", + "PasswordRecoveryPage.forgotPasswordMessage": "No worries! Please enter the email address you used when signing up and we'll send you instructions on how to set a new password.", + "PasswordRecoveryPage.forgotPasswordTitle": "Forgot your password?", + "PasswordRecoveryPage.resendEmailInfo": "Didn't get the email? {resendEmailLink}", + "PasswordRecoveryPage.resendEmailLinkText": "Send another email.", + "PasswordRecoveryPage.resendingEmailInfo": "Resending instructions...", + "PasswordRecoveryPage.title": "Request a new password", "PasswordResetForm.passwordLabel": "Your new password", "PasswordResetForm.passwordPlaceholder": "Enter your new password...", "PasswordResetForm.passwordRequired": "This field is required", @@ -231,28 +257,6 @@ "PasswordResetPage.passwordChangedHelpText": "Your password has been changed successfully.", "PasswordResetPage.resetFailed": "Reset failed. Please try again.", "PasswordResetPage.title": "Reset password", - "PasswordRecoveryForm.emailLabel": "Email", - "PasswordRecoveryForm.emailNotFound": "Hmm. We didn't find an account with that email. Please double-check your email and try again.", - "PasswordRecoveryForm.emailPlaceholder": "john.doe@example.com", - "PasswordRecoveryForm.emailRequired": "This field is required", - "PasswordRecoveryForm.loginLinkText": "Go log in.", - "PasswordRecoveryForm.loginLinkInfo": "Suddenly remembered your password? {loginLink}", - "PasswordRecoveryForm.sendInstructions": "Send instructions", - "PasswordRecoveryPage.actionFailedTitle": "Whoops!", - "PasswordRecoveryPage.actionFailedMessage": "Something went wrong. Please refresh the page and try again.", - "PasswordRecoveryPage.emailNotVerifiedTitle": "We have bad news", - "PasswordRecoveryPage.emailNotVerifiedMessage": "Unfortunately, we cannot recover your password, because your email {initialEmailText} hasn't been verified.", - "PasswordRecoveryPage.emailNotVerifiedContactAdmin": "To resolve the issue, please contact Saunatime administrators.", - "PasswordRecoveryPage.emailSubmittedTitle": "Check your inbox", - "PasswordRecoveryPage.emailSubmittedMessage": "We have sent the instructions for resetting your password to {submittedEmailText}.", - "PasswordRecoveryPage.fixEmailLinkText": "Fix it.", - "PasswordRecoveryPage.fixEmailInfo": "Whoops, typo in your email? {fixEmailLink}", - "PasswordRecoveryPage.forgotPasswordTitle": "Forgot your password?", - "PasswordRecoveryPage.forgotPasswordMessage": "No worries! Please enter the email address you used when signing up and we'll send you instructions on how to set a new password.", - "PasswordRecoveryPage.resendEmailLinkText": "Send another email.", - "PasswordRecoveryPage.resendEmailInfo": "Didn't get the email? {resendEmailLink}", - "PasswordRecoveryPage.resendingEmailInfo": "Resending instructions...", - "PasswordRecoveryPage.title": "Request a new password", "PayoutDetailsForm.addressTitle": "Address", "PayoutDetailsForm.bankDetails": "Bank details", "PayoutDetailsForm.birthdayDatePlaceholder": "dd",