Sale page responsive styles

This commit is contained in:
Kimmo Puputti 2017-08-03 15:53:37 +03:00
parent 215c2b0b5e
commit 6bf7a5af9a
7 changed files with 474 additions and 189 deletions

View file

@ -1,53 +1,166 @@
@import '../../marketplace.css';
.root {
padding: 0 24px;
}
.container {
display: flex;
flex-direction: column;
@media (--viewportMedium) {
flex-direction: row;
justify-content: center;
}
}
.nowrap {
white-space: nowrap;
}
.header {
display: flex;
margin-top: 28px;
margin-bottom: 24px;
.aspectWrapperMobile,
.aspectWrapperDesktop {
position: relative;
padding-bottom: 66.6667%; /* 3:2 Aspect Ratio */
background-color: var(--matterColorNegative); /* Loading BG color */
}
.aspectWrapperMobile {
@media (--viewportMedium) {
display: none;
}
}
.aspectWrapperDesktop {
display: none;
@media (--viewportMedium) {
display: block;
}
}
.rootForImage {
/* Layout - image will take space defined by aspect ratio wrapper */
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
}
.info {
@media (--viewportMedium) {
margin-top: 100px;
max-width: 517px;
}
}
.title {
flex: 1;
margin: 0 12px 0 0;
margin: 22px 24px 0 24px;
@media (--viewportMedium) {
margin: 0 12px 0 0;
margin: 16px 24px 0 24px;
}
}
.message {
margin: 0 0 47px 0;
margin: 24px 24px 0 24px;
@media (--viewportMedium) {
margin: 23px 24px 0 24px;
}
}
.avatarWrapper {
width: auto;
margin-top: 7px;
.avatarWrapperMobile {
/* Position (over the listing image)*/
margin-top: -31px;
/* Bring on top of image */
position: relative;
/* Layout */
display: flex;
justify-content: center;
width: 100%;
@media (--viewportMedium) {
display: none;
}
}
.avatar span {
margin-bottom: 5px;
.avatarWrapperDesktop {
display: none;
@media (--viewportMedium) {
display: block;
margin-left: 24px;
}
}
.bookingBreakdownContainer {
margin-bottom: 40px;
.breakdownContainerMobile {
margin: 0 24px 122px 24px;
@media (--viewportMedium) {
display: none;
}
}
.bookingBreakdownTitle {
.breakdownContainerDesktop {
display: none;
margin: 119px 24px 24px 24px;
box-shadow: var(--boxShadowBreakdown);
@media (--viewportMedium) {
display: block;
}
}
.breakdownTitleMobile {
/* Font */
color: var(--matterColorAnti);
margin-top: 5px;
margin-bottom: 14px;
margin: 47px 0 14px 0;
}
.breakdownTitleDesktop {
margin: 48px 48px 50px 48px;
color: var(--matterColorAnti);
}
.breakdownDesktop {
margin: 48px;
}
.actionButtons {
position: fixed;
bottom: 0;
width: 100%;
padding: 19px 24px 18px 24px;
display: flex;
/* Contain repainting to this component only */
/* 3D painting container helps scrolling */
transform: translate3d(0,0,0);
box-shadow: var(--boxShadowTop);
background-color: white;
@media (--viewportMedium) {
margin-top: 5px;
margin-bottom: 14px;
position: static;
box-shadow: none;
width: auto;
margin: 100px 24px 0 24px;
padding: 0;
}
}
.rejectButton {
margin-right: 6.5px;
}
.acceptButton {
margin-left: 6.5px;
}

View file

@ -4,7 +4,14 @@ import classNames from 'classnames';
import * as propTypes from '../../util/propTypes';
import { createSlug } from '../../util/urlHelpers';
import { ensureListing, ensureTransaction, ensureBooking, ensureUser } from '../../util/data';
import { AvatarMedium, BookingBreakdown, NamedLink } from '../../components';
import {
AvatarMedium,
BookingBreakdown,
NamedLink,
ResponsiveImage,
PrimaryButton,
SecondaryButton,
} from '../../components';
import css from './SaleDetailsPanel.css';
@ -23,7 +30,6 @@ const breakdown = (transaction, totalLabelMessage) => {
return (
<BookingBreakdown
className={css.breakdown}
bookingStart={bookingStart}
bookingEnd={bookingEnd}
payinTotal={payinTotal}
@ -111,6 +117,9 @@ const SaleDetailsPanel = props => {
rootClassName,
className,
transaction,
onAcceptSale,
onRejectSale,
acceptOrRejectInProgress,
} = props;
const currentTransaction = ensureTransaction(transaction);
const currentListing = ensureListing(currentTransaction.listing);
@ -149,24 +158,86 @@ const SaleDetailsPanel = props => {
lastTransition
);
const listingTitle = currentListing.attributes.title;
const firstImage = currentListing.images && currentListing.images.length > 0
? currentListing.images[0]
: null;
const isPreauthorizedState = currentTransaction.attributes.state ===
propTypes.TX_STATE_PREAUTHORIZED;
const actionButtons = isPreauthorizedState
? <div className={css.actionButtons}>
<SecondaryButton
className={css.rejectButton}
disabled={acceptOrRejectInProgress}
onClick={() => onRejectSale(currentTransaction.id)}
>
<FormattedMessage id="SalePage.rejectButton" />
</SecondaryButton>
<PrimaryButton
className={css.acceptButton}
disabled={acceptOrRejectInProgress}
onClick={() => onAcceptSale(currentTransaction.id)}
>
<FormattedMessage id="SalePage.acceptButton" />
</PrimaryButton>
</div>
: null;
const classes = classNames(rootClassName || css.root, className);
return (
<div className={classes}>
<div className={css.header}>
<h1 className={css.title}>
{title}
</h1>
<div className={css.avatarWrapper}>
<div className={css.container}>
<div className={css.aspectWrapperMobile}>
<ResponsiveImage
rootClassName={css.rootForImage}
alt={listingTitle}
image={firstImage}
nameSet={[
{ name: 'landscape-crop', size: '400w' },
{ name: 'landscape-crop2x', size: '800w' },
]}
sizes="100vw"
/>
</div>
<div className={css.avatarWrapperMobile}>
<AvatarMedium firstName={customerFirstName} lastName={customerLastName} />
</div>
</div>
<p className={css.message}>{message}</p>
<div className={css.bookingBreakdownContainer}>
<h3 className={css.bookingBreakdownTitle}>
<FormattedMessage id="SaleDetailsPanel.bookingBreakdownTitle" />
</h3>
{bookingInfo}
<div className={css.info}>
<div className={css.avatarWrapperDesktop}>
<AvatarMedium firstName={customerFirstName} lastName={customerLastName} />
</div>
<h1 className={css.title}>{title}</h1>
<p className={css.message}>{message}</p>
{actionButtons}
</div>
<div className={css.breakdownContainerMobile}>
<h3 className={css.breakdownTitleMobile}>
<FormattedMessage id="SaleDetailsPanel.bookingBreakdownTitle" />
</h3>
{bookingInfo}
</div>
<div className={css.breakdownContainerDesktop}>
<div className={css.aspectWrapperDesktop}>
<ResponsiveImage
rootClassName={css.rootForImage}
alt={listingTitle}
image={firstImage}
nameSet={[
{ name: 'landscape-crop', size: '400w' },
{ name: 'landscape-crop2x', size: '800w' },
]}
sizes="100%"
/>
</div>
<h3 className={css.breakdownTitleDesktop}>
<FormattedMessage id="SaleDetailsPanel.bookingBreakdownTitle" />
</h3>
<div className={css.breakdownDesktop}>
{bookingInfo}
</div>
</div>
</div>
</div>
);
@ -178,12 +249,15 @@ SaleDetailsPanel.defaultProps = {
lastTransition: null,
};
const { string } = PropTypes;
const { string, func, bool } = PropTypes;
SaleDetailsPanel.propTypes = {
rootClassName: string,
className: string,
transaction: propTypes.transaction.isRequired,
onAcceptSale: func.isRequired,
onRejectSale: func.isRequired,
acceptOrRejectInProgress: bool,
};
export default SaleDetailsPanel;

View file

@ -2,94 +2,212 @@ exports[`SaleDetailsPanel matches snapshot 1`] = `
<div
className="">
<div>
<h1>
<FormattedMessage
id="SaleDetailsPanel.listingRequestedTitle"
values={
Object {
"customerName": "customer1 first name",
"listingLink": <withFlattenedRoutes(withRouter(NamedLink))
name="ListingPage"
params={
Object {
"id": "listing1",
"slug": "listing1-title",
}
}>
listing1 title
</withFlattenedRoutes(withRouter(NamedLink))>,
}
} />
</h1>
<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="100vw" />
</div>
<div>
<AvatarMedium
firstName="customer1 first name"
lastName="customer1 last name" />
</div>
</div>
<p>
<FormattedMessage
id="SaleDetailsPanel.saleRequestedStatus"
values={
Object {
"customerName": "customer1 first name",
<div>
<div>
<AvatarMedium
firstName="customer1 first name"
lastName="customer1 last name" />
</div>
<h1>
<FormattedMessage
id="SaleDetailsPanel.listingRequestedTitle"
values={
Object {
"customerName": "customer1 first name",
"listingLink": <withFlattenedRoutes(withRouter(NamedLink))
name="ListingPage"
params={
Object {
"id": "listing1",
"slug": "listing1-title",
}
}>
listing1 title
</withFlattenedRoutes(withRouter(NamedLink))>,
}
} />
</h1>
<p>
<FormattedMessage
id="SaleDetailsPanel.saleRequestedStatus"
values={
Object {
"customerName": "customer1 first name",
}
} />
</p>
<div>
<SecondaryButton
onClick={[Function]}>
<FormattedMessage
id="SalePage.rejectButton"
values={Object {}} />
</SecondaryButton>
<PrimaryButton
onClick={[Function]}>
<FormattedMessage
id="SalePage.acceptButton"
values={Object {}} />
</PrimaryButton>
</div>
</div>
<div>
<h3>
<FormattedMessage
id="SaleDetailsPanel.bookingBreakdownTitle"
values={Object {}} />
</h3>
<BookingBreakdown
bookingEnd={2017-06-13T00:00:00.000Z}
bookingStart={2017-06-10T00:00:00.000Z}
lineItems={
Array [
Object {
"code": "line-item/night",
"lineTotal": Money {
"amount": 16500,
"currency": "USD",
},
"quantity": "3",
"unitPrice": Money {
"amount": 5500,
"currency": "USD",
},
},
Object {
"code": "line-item/provider-commission",
"lineTotal": Money {
"amount": -1000,
"currency": "USD",
},
"unitPrice": Money {
"amount": -1000,
"currency": "USD",
},
},
]
}
} />
</p>
<div>
<h3>
<FormattedMessage
id="SaleDetailsPanel.bookingBreakdownTitle"
values={Object {}} />
</h3>
<BookingBreakdown
bookingEnd={2017-06-13T00:00:00.000Z}
bookingStart={2017-06-10T00:00:00.000Z}
lineItems={
Array [
Object {
"code": "line-item/night",
"lineTotal": Money {
payinTotal={
Money {
"amount": 16500,
"currency": "USD",
}
}
payoutTotal={
Money {
"amount": 15500,
"currency": "USD",
}
}
totalLabelMessage={
<FormattedMessage
id="SaleDetailsPanel.providerTotal"
values={Object {}} />
}
userRole="provider" />
</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>
<h3>
<FormattedMessage
id="SaleDetailsPanel.bookingBreakdownTitle"
values={Object {}} />
</h3>
<div>
<BookingBreakdown
bookingEnd={2017-06-13T00:00:00.000Z}
bookingStart={2017-06-10T00:00:00.000Z}
lineItems={
Array [
Object {
"code": "line-item/night",
"lineTotal": Money {
"amount": 16500,
"currency": "USD",
},
"quantity": "3",
"unitPrice": Money {
"amount": 5500,
"currency": "USD",
},
},
Object {
"code": "line-item/provider-commission",
"lineTotal": Money {
"amount": -1000,
"currency": "USD",
},
"unitPrice": Money {
"amount": -1000,
"currency": "USD",
},
},
]
}
payinTotal={
Money {
"amount": 16500,
"currency": "USD",
},
"quantity": "3",
"unitPrice": Money {
"amount": 5500,
}
}
payoutTotal={
Money {
"amount": 15500,
"currency": "USD",
},
},
Object {
"code": "line-item/provider-commission",
"lineTotal": Money {
"amount": -1000,
"currency": "USD",
},
"unitPrice": Money {
"amount": -1000,
"currency": "USD",
},
},
]
}
payinTotal={
Money {
"amount": 16500,
"currency": "USD",
}
}
payoutTotal={
Money {
"amount": 15500,
"currency": "USD",
}
}
totalLabelMessage={
<FormattedMessage
id="SaleDetailsPanel.providerTotal"
values={Object {}} />
}
userRole="provider" />
}
}
totalLabelMessage={
<FormattedMessage
id="SaleDetailsPanel.providerTotal"
values={Object {}} />
}
userRole="provider" />
</div>
</div>
</div>
</div>
`;

View file

@ -17,21 +17,6 @@
color: var(--failColor);
}
.actionButtons {
display: flex;
justify-content: space-between;
margin: auto 24px 96px 24px;
padding-top: 24px;
}
.rejectButton {
margin-right: 6.5px;
}
.acceptButton {
margin-left: 6.5px;
}
.tabContent {
display: none;
}

View file

@ -6,14 +6,7 @@ import classNames from 'classnames';
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
import * as propTypes from '../../util/propTypes';
import { ensureListing, ensureTransaction } from '../../util/data';
import {
PrimaryButton,
SecondaryButton,
NamedRedirect,
SaleDetailsPanel,
PageLayout,
Topbar,
} from '../../components';
import { NamedRedirect, SaleDetailsPanel, PageLayout, Topbar } from '../../components';
import { getMarketplaceEntities } from '../../ducks/marketplaceData.duck';
import { logout, authenticationInProgress } from '../../ducks/Auth.duck';
import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck';
@ -30,6 +23,9 @@ export const SalePageComponent = props => {
currentUser,
currentUserHasListings,
fetchSaleError,
acceptSaleError,
rejectSaleError,
acceptOrRejectInProgress,
history,
intl,
isAuthenticated,
@ -68,28 +64,22 @@ export const SalePageComponent = props => {
? <p className={css.error}><FormattedMessage id="SalePage.fetchSaleFailed" /></p>
: <p className={css.loading}><FormattedMessage id="SalePage.loadingData" /></p>;
const panel = isDataAvailable && currentTransaction.id
? <SaleDetailsPanel className={detailsClassName} transaction={currentTransaction} />
: loadingOrFailedFetching;
const isPreauthorizedState = currentTransaction.attributes.state ===
propTypes.TX_STATE_PREAUTHORIZED;
const actionButtons = isDataAvailable && isPreauthorizedState
? <div className={css.actionButtons}>
<SecondaryButton
className={css.rejectButton}
onClick={() => onRejectSale(currentTransaction.id)}
>
<FormattedMessage id="SalePage.rejectButton" />
</SecondaryButton>
<PrimaryButton
className={css.acceptButton}
onClick={() => onAcceptSale(currentTransaction.id)}
>
<FormattedMessage id="SalePage.acceptButton" />
</PrimaryButton>
</div>
const acceptError = acceptSaleError
? <p className={css.error}><FormattedMessage id="SalePage.acceptSaleFailed" /></p>
: null;
const rejectError = rejectSaleError
? <p className={css.error}><FormattedMessage id="SalePage.rejectSaleFailed" /></p>
: null;
const panel = isDataAvailable && currentTransaction.id
? <SaleDetailsPanel
className={detailsClassName}
transaction={currentTransaction}
onAcceptSale={onAcceptSale}
onRejectSale={onRejectSale}
acceptOrRejectInProgress={acceptOrRejectInProgress}
/>
: loadingOrFailedFetching;
return (
<PageLayout
@ -109,9 +99,10 @@ export const SalePageComponent = props => {
onLogout={onLogout}
onManageDisableScrolling={onManageDisableScrolling}
/>
{acceptError}
{rejectError}
<div className={css.root}>
{panel}
{actionButtons}
</div>
</PageLayout>
);
@ -121,6 +112,8 @@ SalePageComponent.defaultProps = {
authInfoError: null,
currentUser: null,
fetchSaleError: null,
acceptSaleError: null,
rejectSaleError: null,
logoutError: null,
notificationCount: 0,
transaction: null,
@ -134,6 +127,9 @@ SalePageComponent.propTypes = {
currentUser: propTypes.currentUser,
currentUserHasListings: bool.isRequired,
fetchSaleError: instanceOf(Error),
acceptSaleError: instanceOf(Error),
rejectSaleError: instanceOf(Error),
acceptOrRejectInProgress: bool.isRequired,
intl: intlShape.isRequired,
isAuthenticated: bool.isRequired,
logoutError: instanceOf(Error),
@ -157,7 +153,13 @@ SalePageComponent.propTypes = {
};
const mapStateToProps = state => {
const { fetchSaleError, transactionRef } = state.SalePage;
const {
fetchSaleError,
acceptSaleError,
rejectSaleError,
acceptOrRejectInProgress,
transactionRef,
} = state.SalePage;
const { authInfoError, isAuthenticated, logoutError } = state.Auth;
const {
currentUser,
@ -174,6 +176,9 @@ const mapStateToProps = state => {
currentUser,
currentUserHasListings,
fetchSaleError,
acceptSaleError,
rejectSaleError,
acceptOrRejectInProgress,
isAuthenticated,
logoutError,
notificationCount,

View file

@ -41,6 +41,8 @@ exports[`SalePage matches snapshot 1`] = `
<SaleDetailsPanel
className="undefined"
lastTransition={null}
onAcceptSale={[Function]}
onRejectSale={[Function]}
rootClassName={null}
transaction={
Object {
@ -142,20 +144,6 @@ exports[`SalePage matches snapshot 1`] = `
"type": "transaction",
}
} />
<div>
<SecondaryButton
onClick={[Function]}>
<FormattedMessage
id="SalePage.rejectButton"
values={Object {}} />
</SecondaryButton>
<PrimaryButton
onClick={[Function]}>
<FormattedMessage
id="SalePage.acceptButton"
values={Object {}} />
</PrimaryButton>
</div>
</div>
</withRouter(PageLayout)>
`;

View file

@ -1,6 +1,6 @@
{
"AddImages.upload": "Uploading",
"AddImages.couldNotReadFile": "Could not read file",
"AddImages.upload": "Uploading",
"AuthenticationPage.emailAlreadyInUse": "An account already exists with this email address. Try logging in instead.",
"AuthenticationPage.loginFailed": "The email and password you entered did not match our records. Please double-check and try again.",
"AuthenticationPage.loginLinkText": "Login",
@ -26,9 +26,9 @@
"BookingDatesForm.requiredDate": "Oops, make sure your date is correct!",
"BookingDatesForm.requiredDate": "Oops, make sure your date is correct!",
"BookingDatesForm.youWontBeChargedInfo": "You won't be charged yet",
"CheckoutPage.goToLandingPage": "Go to homepage",
"CheckoutPage.hostedBy": "Hosted by {name}",
"CheckoutPage.initiateOrderError": "Payment request failed. Please try again.",
"CheckoutPage.goToLandingPage": "Go to homepage",
"CheckoutPage.paymentInfo": "You'll only be charged if your request is accepted by the provider.",
"CheckoutPage.paymentTitle": "Payment",
"CheckoutPage.priceBreakdownTitle": "Booking breakdown",
@ -89,25 +89,25 @@
"InboxPage.salesTabTitle": "Hosting",
"InboxPage.salesTitle": "Inbox: Hosting",
"InboxPage.stateAccepted": "Accepted",
"InboxPage.stateDeclined": "Declined",
"InboxPage.stateDelivered": "Delivered",
"InboxPage.statePending": "Pending",
"InboxPage.stateRequested": "Requested",
"InboxPage.stateDeclined": "Declined",
"InboxPage.title": "Inbox",
"ListingCard.hostedBy": "Hosted by {authorName}.",
"ListingCard.perNight": "per night",
"ListingCard.unsupportedPrice": "({currency})",
"ListingCard.unsupportedPriceTitle": "Unsupported currency ({currency})",
"ListingPage.bookingTitle": "Book {title}",
"ListingPage.bookingHelp": "Start by choosing your dates.",
"ListingPage.bookingTitle": "Book {title}",
"ListingPage.ctaButtonMessage": "Request to book",
"ListingPage.descriptionTitle": "About this sauna",
"ListingPage.hostedBy": "Hosted by {name}",
"ListingPage.loadingListingData": "Loading listing data",
"ListingPage.locationTitle": "Location",
"ListingPage.noListingData": "Could not find listing data",
"ListingPage.perNight": "per night",
"ListingPage.ownListing": "This is your own listing.",
"ListingPage.perNight": "per night",
"LoginForm.emailLabel": "Email",
"LoginForm.emailPlaceholder": "john.doe@example.com",
"LoginForm.emailRequired": "This field is required",
@ -119,6 +119,7 @@
"Modal.close": "CLOSE",
"Modal.closeModal": "Close modal",
"OrderDetailsPanel.bookingBreakdownTitle": "Booking breakdown",
"OrderDetailsPanel.hostedBy": "Hosted by {name}",
"OrderDetailsPanel.orderAcceptedStatus": "{providerName} accepted the request on {transitionDate}.",
"OrderDetailsPanel.orderAcceptedSubtitle": "You have booked {listingLink}",
"OrderDetailsPanel.orderAcceptedTitle": "Woohoo {customerName}!",
@ -131,7 +132,6 @@
"OrderDetailsPanel.orderPreauthorizedTitle": "Great success, {customerName}!",
"OrderDetailsPanel.orderRejectedStatus": "Unfortunately {providerName} declined the request on {transitionDate}.",
"OrderDetailsPanel.orderRejectedTitle": "You requested to book {listingLink}",
"OrderDetailsPanel.hostedBy": "Hosted by {name}",
"OrderPage.fetchOrderFailed": "Fetching order data failed.",
"OrderPage.loadingData": "Loading order data.",
"OrderPage.title": "Order details: {listingTitle}",
@ -142,35 +142,35 @@
"PaginationLinks.toPage": "Go to page {page}",
"PayoutDetailsForm.addressTitle": "Address",
"PayoutDetailsForm.bankDetails": "Bank details",
"PayoutDetailsForm.birthdayDatePlaceholder": "dd",
"PayoutDetailsForm.birthdayLabel": "Birth date",
"PayoutDetailsForm.birthdayLabelMonth": "Month",
"PayoutDetailsForm.birthdayLabelYear": "Year",
"PayoutDetailsForm.birthdayDatePlaceholder": "dd",
"PayoutDetailsForm.birthdayMonthPlaceholder": "mm",
"PayoutDetailsForm.birthdayYearPlaceholder": "yyyy",
"PayoutDetailsForm.birthdayRequired": "Birthday is required and must be a valid date.",
"PayoutDetailsForm.birthdayYearPlaceholder": "yyyy",
"PayoutDetailsForm.cityLabel": "City",
"PayoutDetailsForm.cityPlaceholder": "Helsinki",
"PayoutDetailsForm.cityRequired": "This field is required",
"PayoutDetailsForm.countryLabel": "Country",
"PayoutDetailsForm.countryPlaceholder": "Select your country…",
"PayoutDetailsForm.countryRequired": "This field is required",
"PayoutDetailsForm.countryNames.AU": "Australia",
"PayoutDetailsForm.countryNames.AT": "Austria",
"PayoutDetailsForm.countryNames.AU": "Australia",
"PayoutDetailsForm.countryNames.BE": "Belgium",
"PayoutDetailsForm.countryNames.DE": "Germany",
"PayoutDetailsForm.countryNames.DK": "Denmark",
"PayoutDetailsForm.countryNames.ES": "Spain",
"PayoutDetailsForm.countryNames.FI": "Finland",
"PayoutDetailsForm.countryNames.FR": "France",
"PayoutDetailsForm.countryNames.DE": "Germany",
"PayoutDetailsForm.countryNames.GB": "United Kingdom",
"PayoutDetailsForm.countryNames.IE": "Ireland",
"PayoutDetailsForm.countryNames.IT": "Italy",
"PayoutDetailsForm.countryNames.LU": "Luxembourg",
"PayoutDetailsForm.countryNames.NL": "Netherlands",
"PayoutDetailsForm.countryNames.PT": "Portugal",
"PayoutDetailsForm.countryNames.ES": "Spain",
"PayoutDetailsForm.countryNames.SE": "Sweden",
"PayoutDetailsForm.countryNames.GB": "United Kingdom",
"PayoutDetailsForm.countryNames.US": "United States",
"PayoutDetailsForm.countryPlaceholder": "Select your country…",
"PayoutDetailsForm.countryRequired": "This field is required",
"PayoutDetailsForm.firstNameLabel": "First name",
"PayoutDetailsForm.firstNamePlaceholder": "John",
"PayoutDetailsForm.firstNameRequired": "This field is required",
@ -201,9 +201,11 @@
"SaleDetailsPanel.saleRejectedStatus": "You declined the request on {formattedDate}.",
"SaleDetailsPanel.saleRequestedStatus": "{customerName} is waiting for your response.",
"SalePage.acceptButton": "Accept",
"SalePage.acceptSaleFailed": "Accepting sale failed.",
"SalePage.fetchSaleFailed": "Fetching sale data failed.",
"SalePage.loadingData": "Loading sale data.",
"SalePage.rejectButton": "Decline",
"SalePage.rejectSaleFailed": "Rejecting sale failed.",
"SalePage.title": "Sale details: {title}",
"SearchPage.foundResults": "{count, number} {count, plural, one {sauna} other {saunas}} found",
"SearchPage.foundResultsWithAddress": "{count, number} {count, plural, one {sauna} other {saunas}} around {address}",