CheckoutPage: desktop layout and listing image to mobile layout

This commit is contained in:
Vesa Luusua 2017-08-08 17:46:21 +03:00
parent 03cd105314
commit 48c47633ab
3 changed files with 570 additions and 170 deletions

View file

@ -2,80 +2,7 @@
:root {
--logoHeight: 25px;
}
.heading {
margin-top: 29px;
margin-bottom: 34px;
padding: 0 24px;
}
.title {
/* Font */
@apply --marketplaceH1FontStyles;
color: var(--matterColor);
/* Layout */
width: 100%;
margin-top: 0;
margin-bottom: 7px;
@media (--viewportMedium) {
margin-top: 0;
margin-bottom: 7px;
}
}
.author {
width: 100%;
@apply --marketplaceH4FontStyles;
margin-top: 0;
margin-bottom: 0;
}
.priceBreakdownContainer {
padding: 0 24px;
margin-bottom: 40px;
}
.priceBreakdownTitle {
/* Font */
color: var(--matterColorAnti);
margin-top: 0;
margin-bottom: 14px;
@media (--viewportMedium) {
margin-top: 0;
margin-bottom: 14px;
}
}
.paymentContainer {
flex-grow: 1;
display: flex;
flex-direction: column;
padding: 0 24px;
margin-bottom: 35px;
}
.paymentTitle {
/* Font */
color: var(--matterColorAnti);
margin-top: 0;
margin-bottom: 14px;
@media (--viewportMedium) {
margin-top: 0;
margin-bottom: 14px;
}
}
.orderError {
/* Font */
color: var(--failColor);
}
.paymentForm {
flex-grow: 1;
--logoHeightDesktop: 27px;
}
/* Dummy Topbar */
@ -97,12 +24,311 @@
/* shadows */
box-shadow: var(--boxShadow);
@media (--viewportMedium) {
display: none;
@media (--viewportLarge) {
height: var(--topbarHeightDesktop);
justify-content: flex-start;
}
}
.home {
display: flex;
padding: calc((var(--topbarHeight) - var(--logoHeight)) / 2) 24px;
@media (--viewportLarge) {
padding: calc((var(--topbarHeightDesktop) - var(--logoHeightDesktop)) / 2) 36px;
}
}
.logoMobile {
display: block;
@media (--viewportLarge) {
display: none;
}
}
.logoDesktop {
display: none;
width: 155px;
height: 27px;
@media (--viewportLarge) {
display: block;
}
}
/* /Dummy Topbar */
/**
* Content: mobile flow vs desktop 2 columns
* desktop has reduced version of bookListingContainer (incl. title and Stripe paymentForm).
* priceBreakdown is inside separate column (detailsContainerDesktop)
*/
.contentContainer {
flex-grow: 1;
display: flex;
flex-direction: column;
@media (--viewportLarge) {
flex-direction: row;
justify-content: center;
align-items: flex-start;
padding: 0 48px;
}
}
.bookListingContainer {
flex-grow: 1;
display: flex;
flex-direction: column;
@media (--viewportLarge) {
flex-grow: 0;
flex-basis: 516px;
margin-top: 113px;
margin-right: 130px;
}
}
.aspectWrapper {
position: relative;
padding-bottom: 66.6667%; /* 3:2 Aspect Ratio */
background-color: var(--matterColorNegative); /* Loading BG color */
@media (--viewportLarge) {
display: none;
}
}
.rootForImage {
/* Layout - image will take space defined by aspect ratio wrapper */
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
@media (--viewportLarge) {
border-radius: 2px 2px 0 0;
}
}
.avatarWrapper {
/* Position (over the listing image)*/
margin-left: 24px;
margin-top: -31px;
/* Rendering context to the same lavel as listing image */
position: relative;
/* Layout */
display: block;
width: 61px;
height: 61px;
@media (--viewportLarge) {
margin-left: 48px;
}
}
.avatarMobile {
@media (--viewportLarge) {
display: none;
}
}
.heading {
margin-top: 29px;
margin-bottom: 34px;
padding: 0 24px;
@media (--viewportMedium) {
margin-top: 27px;
margin-bottom: 30px;
}
@media (--viewportLarge) {
margin-top: 0px;
margin-bottom: 71px;
padding: 0;
}
}
.title {
/* Font */
@apply --marketplaceH1FontStyles;
color: var(--matterColor);
/* Layout */
width: 100%;
margin-top: 0;
margin-bottom: 7px;
@media (--viewportMedium) {
margin-top: 0;
margin-bottom: 5px;
}
}
.author {
width: 100%;
@apply --marketplaceH4FontStyles;
/* Reset margins from font styles */
margin-top: 0;
margin-bottom: 0;
@media (--viewportMedium) {
margin-top: 0;
margin-bottom: 0;
}
}
.priceBreakdownContainer {
padding: 0 24px;
margin-bottom: 40px;
@media (--viewportMedium) {
margin-bottom: 38px;
}
@media (--viewportLarge) {
/* we don't show price breakdown since it will be visible within 2nd column (detailsContainerDesktop) */
display: none;
}
}
.priceBreakdownTitle {
/* Font */
color: var(--matterColorAnti);
margin-top: 0;
margin-bottom: 14px;
@media (--viewportMedium) {
margin-top: 0;
margin-bottom: 18px;
}
@media (--viewportLarge) {
margin-top: 0;
margin-bottom: 14px;
}
}
.paymentContainer {
flex-grow: 1;
display: flex;
flex-direction: column;
padding: 0 24px;
margin-bottom: 35px;
@media (--viewportLarge) {
padding: 0;
}
}
.paymentTitle {
/* Font */
color: var(--matterColorAnti);
margin-top: 0;
margin-bottom: 14px;
@media (--viewportMedium) {
margin-top: 0;
margin-bottom: 14px;
}
@media (--viewportMedium) {
margin-top: 0;
margin-bottom: 18px;
}
}
.orderError {
/* Font */
color: var(--failColor);
}
.paymentForm {
flex-grow: 1;
}
/* Details column is visible only on desktop */
.detailsContainerDesktop {
display: none;
@media (--viewportLarge) {
flex-shrink: 0;
display: block;
/* Dimensions */
width: 409px;
padding-bottom: 55px;
/* Position */
margin-top: 120px;
margin-left: 0;
margin-right: 0;
/* Coloring */
background-color: var(--matterColorLight);
border: 1px solid var(--matterColorNegative);
border-radius: 2px;
}
}
.detailsAspectWrapper {
position: relative;
padding-bottom: 66.6667%; /* 3:2 Aspect Ratio */
background-color: var(--matterColorNegative); /* Loading BG color */
}
.detailsHeadings {
display: none;
@media (--viewportLarge) {
display: block;
margin: 0 48px;
}
}
.detailsTitle {
margin-bottom: 10px;
@media (--viewportLarge) {
margin-top: 17px;
margin-bottom: 0;
}
}
.detailsSubtitle {
@apply --marketplaceH5FontStyles;
color: var(--matterColorAnti);
/* Reset margins from font styles */
margin-top: 0;
margin-bottom: 0;
@media (--viewportLarge) {
margin-top: 0;
margin-bottom: 0;
}
}
.bookingBreakdownTitle {
/* Font */
color: var(--matterColorAnti);
/* Layout spacing */
margin: 5px 24px 25px 24px;
@media (--viewportLarge) {
margin: 38px 48px 25px 48px;
}
}
.bookingBreakdown {
@media (--viewportLarge) {
margin: 0 48px;
}
}

View file

@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { withRouter } from 'react-router-dom';
import Decimal from 'decimal.js';
import classNames from 'classnames';
import config from '../../config';
import { types } from '../../util/sdkLoader';
import { pathByRouteName } from '../../util/routes';
@ -12,7 +13,14 @@ import { ensureListing, ensureUser } from '../../util/data';
import { withFlattenedRoutes } from '../../util/contextHelpers';
import { nightsBetween } from '../../util/dates';
import { convertMoneyToNumber, convertUnitToSubUnit } from '../../util/currency';
import { BookingBreakdown, NamedLink, NamedRedirect, PageLayout } from '../../components';
import {
AvatarMedium,
BookingBreakdown,
NamedLink,
NamedRedirect,
PageLayout,
ResponsiveImage,
} from '../../components';
import { StripePaymentForm } from '../../containers';
import { initiateOrder, setInitialValues } from './CheckoutPage.duck';
@ -45,7 +53,7 @@ const breakdown = (bookingStart, bookingEnd, unitPrice) => {
return (
<BookingBreakdown
className={css.receipt}
className={css.bookingBreakdown}
bookingStart={bookingStart}
bookingEnd={bookingEnd}
userRole="customer"
@ -116,6 +124,9 @@ export class CheckoutPageComponent extends Component {
const { bookingStart, bookingEnd } = pageData.bookingDates || {};
const currentListing = ensureListing(pageData.listing);
const currentAuthor = ensureUser(currentListing.author);
const authorProfile = currentAuthor.attributes.profile;
const authorFirstName = authorProfile.firstName;
const authorLastName = authorProfile.lastName;
const isOwnListing = currentListing.id &&
currentUser &&
@ -155,16 +166,10 @@ export class CheckoutPageComponent extends Component {
const listingTitle = currentListing.attributes.title;
const title = intl.formatMessage({ id: 'CheckoutPage.title' }, { listingTitle });
const authorFirstName = currentAuthor.attributes.profile.firstName;
const priceBreakdown = (
<div className={css.priceBreakdownContainer}>
<h3 className={css.priceBreakdownTitle}>
<FormattedMessage id="CheckoutPage.priceBreakdownTitle" />
</h3>
{breakdown(bookingStart, bookingEnd, unitPrice)}
</div>
);
const firstImage = currentListing.images && currentListing.images.length > 0
? currentListing.images[0]
: null;
const errorMessage = initiateOrderError
? <p className={css.orderError}>
@ -172,43 +177,104 @@ export class CheckoutPageComponent extends Component {
</p>
: null;
const topbar = (
<div className={css.topbar}>
<NamedLink className={css.home} name="LandingPage">
<LogoIcon title={intl.formatMessage({ id: 'CheckoutPage.goToLandingPage' })} />
</NamedLink>
</div>
);
return (
<PageLayout authInfoError={authInfoError} logoutError={logoutError} title={title}>
{topbar}
<div className={css.heading}>
<h1 className={css.title}>{title}</h1>
<div className={css.author}>
<span className={css.authorName}>
<FormattedMessage id="ListingPage.hostedBy" values={{ name: authorFirstName }} />
</span>
</div>
<div className={css.topbar}>
<NamedLink className={css.home} name="LandingPage">
<LogoIcon
className={css.logoMobile}
title={intl.formatMessage({ id: 'CheckoutPage.goToLandingPage' })}
/>
<LogoIcon
className={css.logoDesktop}
alt={intl.formatMessage({ id: 'CheckoutPage.goToLandingPage' })}
isMobile={false}
/>
</NamedLink>
</div>
{priceBreakdown}
<div className={css.contentContainer}>
<div className={css.aspectWrapper}>
<ResponsiveImage
rootClassName={css.rootForImage}
alt={listingTitle}
image={firstImage}
nameSet={[
{ name: 'landscape-crop', size: '400w' },
{ name: 'landscape-crop2x', size: '800w' },
]}
sizes="100vw"
/>
</div>
<div className={classNames(css.avatarWrapper, css.avatarMobile)}>
<AvatarMedium firstName={authorFirstName} lastName={authorLastName} />
</div>
<div className={css.bookListingContainer}>
<div className={css.heading}>
<h1 className={css.title}>{title}</h1>
<div className={css.author}>
<span className={css.authorName}>
<FormattedMessage id="ListingPage.hostedBy" values={{ name: authorFirstName }} />
</span>
</div>
</div>
<section className={css.paymentContainer}>
<h3 className={css.paymentTitle}>
<FormattedMessage id="CheckoutPage.paymentTitle" />
</h3>
{errorMessage}
{showPaymentForm
? <StripePaymentForm
className={css.paymentForm}
onSubmit={this.handleSubmit}
disableSubmit={this.state.submitting}
formId="CheckoutPagePaymentForm"
paymentInfo={intl.formatMessage({ id: 'CheckoutPage.paymentInfo' })}
<div className={css.priceBreakdownContainer}>
<h3 className={css.priceBreakdownTitle}>
<FormattedMessage id="CheckoutPage.priceBreakdownTitle" />
</h3>
{breakdown(bookingStart, bookingEnd, unitPrice)}
</div>
<section className={css.paymentContainer}>
<h3 className={css.paymentTitle}>
<FormattedMessage id="CheckoutPage.paymentTitle" />
</h3>
{errorMessage}
{showPaymentForm
? <StripePaymentForm
className={css.paymentForm}
onSubmit={this.handleSubmit}
disableSubmit={this.state.submitting}
formId="CheckoutPagePaymentForm"
paymentInfo={intl.formatMessage({ id: 'CheckoutPage.paymentInfo' })}
/>
: null}
</section>
</div>
<div className={css.detailsContainerDesktop}>
<div className={css.detailsAspectWrapper}>
<ResponsiveImage
rootClassName={css.rootForImage}
alt={listingTitle}
image={firstImage}
nameSet={[
{ name: 'landscape-crop', size: '400w' },
{ name: 'landscape-crop2x', size: '800w' },
]}
sizes="100%"
/>
: null}
</section>
</div>
<div className={css.avatarWrapper}>
<AvatarMedium firstName={authorFirstName} lastName={authorLastName} />
</div>
<div className={css.detailsHeadings}>
<h2 className={css.detailsTitle}>{listingTitle}</h2>
<p className={css.detailsSubtitle}>
<FormattedMessage
id="CheckoutPage.hostedBy"
values={{ name: authorFirstName }}
/>
</p>
</div>
<h3 className={css.bookingBreakdownTitle}>
<FormattedMessage id="CheckoutPage.priceBreakdownTitle" />
</h3>
{breakdown(bookingStart, bookingEnd, unitPrice)}
</div>
</div>
</PageLayout>
);
}

View file

@ -7,70 +7,178 @@ exports[`CheckoutPage matches snapshot 1`] = `
<withFlattenedRoutes(withRouter(NamedLink))
name="LandingPage">
<LogoIcon
alt="Logo"
className={null}
isMobile={true}
title="CheckoutPage.goToLandingPage" />
<LogoIcon
alt="CheckoutPage.goToLandingPage"
className={null}
isMobile={false} />
</withFlattenedRoutes(withRouter(NamedLink))>
</div>
<div>
<h1>
CheckoutPage.title
</h1>
<div>
<span>
<FormattedMessage
id="ListingPage.hostedBy"
values={
<ResponsiveImage
alt="listing1 title"
className={null}
image={null}
nameSet={
Array [
Object {
"name": "author first name",
}
} />
</span>
"name": "landscape-crop",
"size": "400w",
},
Object {
"name": "landscape-crop2x",
"size": "800w",
},
]
}
rootClassName={null}
sizes="100vw" />
</div>
</div>
<div>
<h3>
<FormattedMessage
id="CheckoutPage.priceBreakdownTitle"
values={Object {}} />
</h3>
<BookingBreakdown
bookingEnd={2017-04-16T00:00:00.000Z}
bookingStart={2017-04-14T00:00:00.000Z}
lineItems={
Array [
Object {
"code": "line-item/night",
"lineTotal": Money {
<div
className="">
<AvatarMedium
firstName="author first name"
lastName="author last name" />
</div>
<div>
<div>
<h1>
CheckoutPage.title
</h1>
<div>
<span>
<FormattedMessage
id="ListingPage.hostedBy"
values={
Object {
"name": "author first name",
}
} />
</span>
</div>
</div>
<div>
<h3>
<FormattedMessage
id="CheckoutPage.priceBreakdownTitle"
values={Object {}} />
</h3>
<BookingBreakdown
bookingEnd={2017-04-16T00:00:00.000Z}
bookingStart={2017-04-14T00:00:00.000Z}
lineItems={
Array [
Object {
"code": "line-item/night",
"lineTotal": Money {
"amount": 11000,
"currency": "USD",
},
"quantity": "2",
"unitPrice": Money {
"amount": 5500,
"currency": "USD",
},
},
]
}
payinTotal={
Money {
"amount": 11000,
"currency": "USD",
}
}
userRole="customer" />
</div>
<section>
<h3>
<FormattedMessage
id="CheckoutPage.paymentTitle"
values={Object {}} />
</h3>
<InjectIntl(StripePaymentForm)
disableSubmit={false}
formId="CheckoutPagePaymentForm"
onSubmit={[Function]}
paymentInfo="CheckoutPage.paymentInfo" />
</section>
</div>
<div>
<div>
<ResponsiveImage
alt="listing1 title"
className={null}
image={null}
nameSet={
Array [
Object {
"name": "landscape-crop",
"size": "400w",
},
Object {
"name": "landscape-crop2x",
"size": "800w",
},
]
}
rootClassName={null}
sizes="100%" />
</div>
<div>
<AvatarMedium
firstName="author first name"
lastName="author last name" />
</div>
<div>
<h2>
listing1 title
</h2>
<p>
<FormattedMessage
id="CheckoutPage.hostedBy"
values={
Object {
"name": "author first name",
}
} />
</p>
</div>
<h3>
<FormattedMessage
id="CheckoutPage.priceBreakdownTitle"
values={Object {}} />
</h3>
<BookingBreakdown
bookingEnd={2017-04-16T00:00:00.000Z}
bookingStart={2017-04-14T00:00:00.000Z}
lineItems={
Array [
Object {
"code": "line-item/night",
"lineTotal": Money {
"amount": 11000,
"currency": "USD",
},
"quantity": "2",
"unitPrice": Money {
"amount": 5500,
"currency": "USD",
},
},
"quantity": "2",
"unitPrice": Money {
"amount": 5500,
"currency": "USD",
},
},
]
}
payinTotal={
Money {
"amount": 11000,
"currency": "USD",
]
}
}
userRole="customer" />
payinTotal={
Money {
"amount": 11000,
"currency": "USD",
}
}
userRole="customer" />
</div>
</div>
<section>
<h3>
<FormattedMessage
id="CheckoutPage.paymentTitle"
values={Object {}} />
</h3>
<InjectIntl(StripePaymentForm)
disableSubmit={false}
formId="CheckoutPagePaymentForm"
onSubmit={[Function]}
paymentInfo="CheckoutPage.paymentInfo" />
</section>
</withRouter(PageLayout)>
`;