Merge pull request #524 from sharetribe/tos-page

Terms of Service and Privacy Policy
This commit is contained in:
Kimmo Puputti 2017-10-31 12:05:32 +02:00 committed by GitHub
commit 49c922f9e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 932 additions and 357 deletions

View file

@ -15,3 +15,4 @@ Documentation for specific topics can be found in the following files:
- [CI](ci.md)
- [Static pages](static-pages.md)
- [Analytics](analytics.md)
- [Terms of Service and Privacy Policy](terms-of-service-and-privacy-policy.md)

View file

@ -0,0 +1,16 @@
# Terms of Service and Privacy Policy
The Terms of Service and Privacy Policy of a marketplace are static
components that can be edited to suit the marketplace.
## Terms of Service
Edit
[src/components/TermsOfService/TermsOfService.js](../src/components/TermsOfService/TermsOfService.js)
file to customise the default dummy content.
## Privacy Policy
Edit
[src/components/PrivacyPolicy/PrivacyPolicy.js](../src/components/PrivacyPolicy/PrivacyPolicy.js)
file to customise the default dummy content.

View file

@ -240,12 +240,12 @@ const Footer = props => {
<div className={css.legalMatters}>
<ul className={css.tosAndPrivacy}>
<li>
<NamedLink name="LandingPage" className={css.legalLink}>
<NamedLink name="TermsOfServicePage" className={css.legalLink}>
<FormattedMessage id="Footer.termsOfUse" />
</NamedLink>
</li>
<li>
<NamedLink name="LandingPage" className={css.legalLink}>
<NamedLink name="PrivacyPolicyPage" className={css.legalLink}>
<FormattedMessage id="Footer.privacyPolicy" />
</NamedLink>
</li>
@ -261,10 +261,10 @@ const Footer = props => {
<FormattedMessage id="Footer.copyright" />
</ExternalLink>
<div className={css.tosAndPrivacyMobile}>
<NamedLink name="LandingPage" className={css.privacy}>
<NamedLink name="PrivacyPolicyPage" className={css.privacy}>
<FormattedMessage id="Footer.privacy" />
</NamedLink>
<NamedLink name="LandingPage" className={css.terms}>
<NamedLink name="TermsOfServicePage" className={css.terms}>
<FormattedMessage id="Footer.terms" />
</NamedLink>
</div>

View file

@ -26,3 +26,38 @@
padding-left: 25vw;
}
}
.tabs {
display: flex;
flex-direction: row;
overflow-x: auto;
min-height: 48px;
padding-top: 2px;
@media (--viewportMedium) {
min-height: 56px;
padding-top: 10px;
}
@media (--viewportLarge) {
min-height: auto;
flex-direction: column;
margin-top: 28px;
padding-top: 0;
}
}
.tab {
display: flex;
align-items: flex-end;
height: 100%;
margin-left: 16px;
&:first-child {
margin-left: 0;
}
@media (--viewportLarge) {
margin-left: 0;
}
}

View file

@ -5,26 +5,35 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { TabNav } from '../../components';
import css from './LayoutWrapperSideNav.css';
const LayoutWrapperSideNav = props => {
const { className, rootClassName, children } = props;
const { className, rootClassName, children, tabs } = props;
const classes = classNames(rootClassName || css.root, className);
return <aside className={classes}>{children}</aside>;
return (
<aside className={classes}>
{tabs ? <TabNav rootClassName={css.tabs} tabRootClassName={css.tab} tabs={tabs} /> : null}
{children}
</aside>
);
};
LayoutWrapperSideNav.defaultProps = {
className: null,
rootClassName: null,
children: null,
tabs: null,
};
const { node, string } = PropTypes;
const { node, string, array } = PropTypes;
LayoutWrapperSideNav.propTypes = {
children: node.isRequired,
children: node,
className: string,
rootClassName: string,
tabs: array,
};
export default LayoutWrapperSideNav;

View file

@ -0,0 +1,26 @@
@import '../../marketplace.css';
.root {
& p {
@apply --marketplaceH4FontStyles;
}
& h2 {
/* Adjust heading margins to work with the reduced body font size */
margin: 23px 0 19px 0;
@media (--viewportMedium) {
margin: 24px 0 24px 0;
}
}
& .lastUpdated {
@apply --marketplaceBodyFontStyles;
margin-top: 0;
margin-bottom: 55px;
@media (--viewportMedium) {
margin-top: 0;
margin-bottom: 54px;
}
}
}

View file

@ -0,0 +1,73 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import css from './PrivacyPolicy.css';
const PrivacyPolicy = props => {
const { rootClassName, className } = props;
const classes = classNames(rootClassName || css.root, className);
return (
<div className={classes}>
<p className={css.lastUpdated}>Last updated: October 30, 2017</p>
<p>
Thank you for using Saunatime! Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
<h2>1 Lorem ipsum dolor sit amet</h2>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
<h2>2 Sed ut perspiciatis unde</h2>
<p>
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque
laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi
architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit
aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione
voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet,
consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et
dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum
exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi
consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil
molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
</p>
<h2>3 At vero eos et accusamus</h2>
<p>
At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium
voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati
cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id
est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam
libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod
maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus.
Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut
et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a
sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis
doloribus asperiores repellat
</p>
</div>
);
};
PrivacyPolicy.defaultProps = {
rootClassName: null,
className: null,
};
const { string } = PropTypes;
PrivacyPolicy.propTypes = {
rootClassName: string,
className: string,
};
export default PrivacyPolicy;

View file

@ -0,0 +1,26 @@
@import '../../marketplace.css';
.root {
& p {
@apply --marketplaceH4FontStyles;
}
& h2 {
/* Adjust heading margins to work with the reduced body font size */
margin: 23px 0 19px 0;
@media (--viewportMedium) {
margin: 24px 0 24px 0;
}
}
& .lastUpdated {
@apply --marketplaceBodyFontStyles;
margin-top: 0;
margin-bottom: 55px;
@media (--viewportMedium) {
margin-top: 0;
margin-bottom: 54px;
}
}
}

View file

@ -0,0 +1,73 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import css from './TermsOfService.css';
const TermsOfService = props => {
const { rootClassName, className } = props;
const classes = classNames(rootClassName || css.root, className);
return (
<div className={classes}>
<p className={css.lastUpdated}>Last updated: October 30, 2017</p>
<p>
Thank you for using Saunatime! Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
<h2>1 Lorem ipsum dolor sit amet</h2>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
<h2>2 Sed ut perspiciatis unde</h2>
<p>
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque
laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi
architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit
aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione
voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet,
consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et
dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum
exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi
consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil
molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
</p>
<h2>3 At vero eos et accusamus</h2>
<p>
At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium
voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati
cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id
est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam
libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod
maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus.
Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut
et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a
sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis
doloribus asperiores repellat
</p>
</div>
);
};
TermsOfService.defaultProps = {
rootClassName: null,
className: null,
};
const { string } = PropTypes;
TermsOfService.propTypes = {
rootClassName: string,
className: string,
};
export default TermsOfService;

View file

@ -76,6 +76,7 @@ export { default as OrderDetailsPanel } from './OrderDetailsPanel/OrderDetailsPa
export { default as OrderDiscussionPanel } from './OrderDiscussionPanel/OrderDiscussionPanel';
export { default as Page } from './Page/Page';
export { default as PaginationLinks } from './PaginationLinks/PaginationLinks';
export { default as PrivacyPolicy } from './PrivacyPolicy/PrivacyPolicy';
export { default as Promised } from './Promised/Promised';
export { default as ResponsiveImage } from './ResponsiveImage/ResponsiveImage';
export { default as SaleDetailsPanel } from './SaleDetailsPanel/SaleDetailsPanel';
@ -91,6 +92,7 @@ export {
export { default as TabNav } from './TabNav/TabNav';
export { default as TabNavHorizontal } from './TabNavHorizontal/TabNavHorizontal';
export { default as Tabs } from './Tabs/Tabs';
export { default as TermsOfService } from './TermsOfService/TermsOfService';
export { default as TextInputField } from './TextInputField/TextInputField';
export { default as Topbar } from './Topbar/Topbar';
export { default as TopbarDesktop } from './TopbarDesktop/TopbarDesktop';

View file

@ -50,6 +50,27 @@
text-align: left;
}
/* Terms of Service modal*/
.termsWrapper {
width: 100%;
padding-top: 40px;
@media (--viewportMedium) {
width: 571px;
padding-top: 11px;
}
}
.termsHeading {
@apply --marketplaceH1FontStyles;
margin: 0 0 19px 0;
@media (--viewportMedium) {
margin: 0 0 19px 0;
}
}
/* ================ Hide Top bar in screens smaller than 768px ================ */
.hideOnMobile {

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { connect } from 'react-redux';
@ -25,216 +25,244 @@ import {
LayoutWrapperMain,
LayoutWrapperFooter,
Footer,
Modal,
TermsOfService,
} from '../../components';
import { LoginForm, SignupForm, TopbarContainer } from '../../containers';
import { login, authenticationInProgress, signup } from '../../ducks/Auth.duck';
import { isScrollingDisabled } from '../../ducks/UI.duck';
import { sendVerificationEmail } from '../../ducks/user.duck';
import { manageDisableScrolling } from '../../ducks/UI.duck';
import css from './AuthenticationPage.css';
export const AuthenticationPageComponent = props => {
const {
authInProgress,
currentUser,
intl,
isAuthenticated,
location,
loginError,
logoutError,
scrollingDisabled,
signupError,
submitLogin,
submitSignup,
tab,
sendVerificationEmailInProgress,
sendVerificationEmailError,
onResendVerificationEmail,
} = props;
const isLogin = tab === 'login';
const from = location.state && location.state.from ? location.state.from : null;
const user = ensureCurrentUser(currentUser);
const currentUserLoaded = !!user.id;
// We only want to show the email verification dialog in the signup
// tab if the user isn't being redirected somewhere else
// (i.e. `from` is present). We must also check the `emailVerified`
// flag only when the current user is fully loaded.
const showEmailVerification = !isLogin && currentUserLoaded && !user.attributes.emailVerified;
// Already authenticated, redirect away from auth page
if (isAuthenticated && from) {
return <Redirect to={from} />;
} else if (isAuthenticated && currentUserLoaded && !showEmailVerification) {
return <NamedRedirect name="LandingPage" />;
export class AuthenticationPageComponent extends Component {
constructor(props) {
super(props);
this.state = { tosModalOpen: false };
}
render() {
const {
authInProgress,
currentUser,
intl,
isAuthenticated,
location,
loginError,
logoutError,
scrollingDisabled,
signupError,
submitLogin,
submitSignup,
tab,
sendVerificationEmailInProgress,
sendVerificationEmailError,
onResendVerificationEmail,
onManageDisableScrolling,
} = this.props;
const isLogin = tab === 'login';
const from = location.state && location.state.from ? location.state.from : null;
/* eslint-disable no-console */
if (loginError && console && console.error) {
console.error(loginError);
}
if (signupError && console && console.error) {
console.error(signupError);
}
/* eslint-enable no-console */
const user = ensureCurrentUser(currentUser);
const currentUserLoaded = !!user.id;
const loginErrorMessage = (
<div className={css.error}>
<FormattedMessage id="AuthenticationPage.loginFailed" />
</div>
);
// We only want to show the email verification dialog in the signup
// tab if the user isn't being redirected somewhere else
// (i.e. `from` is present). We must also check the `emailVerified`
// flag only when the current user is fully loaded.
const showEmailVerification = !isLogin && currentUserLoaded && !user.attributes.emailVerified;
const signupErrorMessage = (
<div className={css.error}>
{isSignupEmailTakenError(signupError) ? (
<FormattedMessage id="AuthenticationPage.signupFailedEmailAlreadyTaken" />
) : (
<FormattedMessage id="AuthenticationPage.signupFailed" />
)}
</div>
);
// Already authenticated, redirect away from auth page
if (isAuthenticated && from) {
return <Redirect to={from} />;
} else if (isAuthenticated && currentUserLoaded && !showEmailVerification) {
return <NamedRedirect name="LandingPage" />;
}
// eslint-disable-next-line no-confusing-arrow
const errorMessage = (error, message) => (error ? message : null);
const loginOrSignupError = isLogin
? errorMessage(loginError, loginErrorMessage)
: errorMessage(signupError, signupErrorMessage);
/* eslint-disable no-console */
if (loginError && console && console.error) {
console.error(loginError);
}
if (signupError && console && console.error) {
console.error(signupError);
}
/* eslint-enable no-console */
const fromState = { state: from ? { from } : null };
const tabs = [
{
text: (
<h1 className={css.tab}>
<FormattedMessage id="AuthenticationPage.signupLinkText" />
</h1>
),
selected: !isLogin,
linkProps: {
name: 'SignupPage',
to: fromState,
},
},
{
text: (
<h1 className={css.tab}>
<FormattedMessage id="AuthenticationPage.loginLinkText" />
</h1>
),
selected: isLogin,
linkProps: {
name: 'LoginPage',
to: fromState,
},
},
];
const formContent = (
<div className={css.content}>
<TabNavHorizontal className={css.tabs} tabs={tabs} />
{loginOrSignupError}
{isLogin ? (
<LoginForm className={css.form} onSubmit={submitLogin} inProgress={authInProgress} />
) : (
<SignupForm className={css.form} onSubmit={submitSignup} inProgress={authInProgress} />
)}
</div>
);
const name = user.attributes.profile.firstName;
const email = <span className={css.email}>{user.attributes.email}</span>;
const resendEmailLink = (
<InlineTextButton className={css.modalHelperLink} onClick={onResendVerificationEmail}>
<FormattedMessage id="AuthenticationPage.resendEmailLinkText" />
</InlineTextButton>
);
const fixEmailLink = (
<NamedLink className={css.modalHelperLink} name="ContactDetailsPage">
<FormattedMessage id="AuthenticationPage.fixEmailLinkText" />
</NamedLink>
);
const resendErrorTranslationId = isTooManyEmailVerificationRequestsError(
sendVerificationEmailError
)
? 'AuthenticationPage.resendFailedTooManyRequests'
: 'AuthenticationPage.resendFailed';
const resendErrorMessage = sendVerificationEmailError ? (
<p className={css.error}>
<FormattedMessage id={resendErrorTranslationId} />
</p>
) : null;
const emailVerificationContent = (
<div className={css.content}>
<NamedLink className={css.verifyClose} name="ProfileSettingsPage">
<span className={css.closeText}>
<FormattedMessage id="AuthenticationPage.verifyEmailClose" />
</span>
<IconClose rootClassName={css.closeIcon} />
</NamedLink>
<IconEmailSent className={css.modalIcon} />
<h1 className={css.modalTitle}>
<FormattedMessage id="AuthenticationPage.verifyEmailTitle" values={{ name }} />
</h1>
<p className={css.modalMessage}>
<FormattedMessage id="AuthenticationPage.verifyEmailText" values={{ email }} />
</p>
{resendErrorMessage}
<div className={css.bottomWrapper}>
<p className={css.modalHelperText}>
{sendVerificationEmailInProgress ? (
<FormattedMessage id="AuthenticationPage.sendingEmail" />
) : (
<FormattedMessage id="AuthenticationPage.resendEmail" values={{ resendEmailLink }} />
)}
</p>
<p className={css.modalHelperText}>
<FormattedMessage id="AuthenticationPage.fixEmail" values={{ fixEmailLink }} />
</p>
const loginErrorMessage = (
<div className={css.error}>
<FormattedMessage id="AuthenticationPage.loginFailed" />
</div>
</div>
);
);
const siteTitle = config.siteTitle;
const schemaTitle = isLogin
? intl.formatMessage({ id: 'AuthenticationPage.schemaTitleLogin' }, { siteTitle })
: intl.formatMessage({ id: 'AuthenticationPage.schemaTitleSignup' }, { siteTitle });
const signupErrorMessage = (
<div className={css.error}>
{isSignupEmailTakenError(signupError) ? (
<FormattedMessage id="AuthenticationPage.signupFailedEmailAlreadyTaken" />
) : (
<FormattedMessage id="AuthenticationPage.signupFailed" />
)}
</div>
);
const topbarClasses = classNames({
[css.hideOnMobile]: showEmailVerification,
});
// eslint-disable-next-line no-confusing-arrow
const errorMessage = (error, message) => (error ? message : null);
const loginOrSignupError = isLogin
? errorMessage(loginError, loginErrorMessage)
: errorMessage(signupError, signupErrorMessage);
return (
<Page
logoutError={logoutError}
title={schemaTitle}
scrollingDisabled={scrollingDisabled}
schema={{
'@context': 'http://schema.org',
'@type': 'WebPage',
name: schemaTitle,
}}
>
<LayoutSingleColumn>
<LayoutWrapperTopbar>
<TopbarContainer className={topbarClasses} />
</LayoutWrapperTopbar>
<LayoutWrapperMain className={css.layoutWrapperMain}>
<div className={css.root}>
{showEmailVerification ? emailVerificationContent : formContent}
</div>
</LayoutWrapperMain>
<LayoutWrapperFooter>
<Footer />
</LayoutWrapperFooter>
</LayoutSingleColumn>
</Page>
);
};
const fromState = { state: from ? { from } : null };
const tabs = [
{
text: (
<h1 className={css.tab}>
<FormattedMessage id="AuthenticationPage.signupLinkText" />
</h1>
),
selected: !isLogin,
linkProps: {
name: 'SignupPage',
to: fromState,
},
},
{
text: (
<h1 className={css.tab}>
<FormattedMessage id="AuthenticationPage.loginLinkText" />
</h1>
),
selected: isLogin,
linkProps: {
name: 'LoginPage',
to: fromState,
},
},
];
const formContent = (
<div className={css.content}>
<TabNavHorizontal className={css.tabs} tabs={tabs} />
{loginOrSignupError}
{isLogin ? (
<LoginForm className={css.form} onSubmit={submitLogin} inProgress={authInProgress} />
) : (
<SignupForm
className={css.form}
onSubmit={submitSignup}
inProgress={authInProgress}
onOpenTermsOfService={() => this.setState({ tosModalOpen: true })}
/>
)}
</div>
);
const name = user.attributes.profile.firstName;
const email = <span className={css.email}>{user.attributes.email}</span>;
const resendEmailLink = (
<InlineTextButton className={css.modalHelperLink} onClick={onResendVerificationEmail}>
<FormattedMessage id="AuthenticationPage.resendEmailLinkText" />
</InlineTextButton>
);
const fixEmailLink = (
<NamedLink className={css.modalHelperLink} name="ContactDetailsPage">
<FormattedMessage id="AuthenticationPage.fixEmailLinkText" />
</NamedLink>
);
const resendErrorTranslationId = isTooManyEmailVerificationRequestsError(
sendVerificationEmailError
)
? 'AuthenticationPage.resendFailedTooManyRequests'
: 'AuthenticationPage.resendFailed';
const resendErrorMessage = sendVerificationEmailError ? (
<p className={css.error}>
<FormattedMessage id={resendErrorTranslationId} />
</p>
) : null;
const emailVerificationContent = (
<div className={css.content}>
<NamedLink className={css.verifyClose} name="ProfileSettingsPage">
<span className={css.closeText}>
<FormattedMessage id="AuthenticationPage.verifyEmailClose" />
</span>
<IconClose rootClassName={css.closeIcon} />
</NamedLink>
<IconEmailSent className={css.modalIcon} />
<h1 className={css.modalTitle}>
<FormattedMessage id="AuthenticationPage.verifyEmailTitle" values={{ name }} />
</h1>
<p className={css.modalMessage}>
<FormattedMessage id="AuthenticationPage.verifyEmailText" values={{ email }} />
</p>
{resendErrorMessage}
<div className={css.bottomWrapper}>
<p className={css.modalHelperText}>
{sendVerificationEmailInProgress ? (
<FormattedMessage id="AuthenticationPage.sendingEmail" />
) : (
<FormattedMessage id="AuthenticationPage.resendEmail" values={{ resendEmailLink }} />
)}
</p>
<p className={css.modalHelperText}>
<FormattedMessage id="AuthenticationPage.fixEmail" values={{ fixEmailLink }} />
</p>
</div>
</div>
);
const siteTitle = config.siteTitle;
const schemaTitle = isLogin
? intl.formatMessage({ id: 'AuthenticationPage.schemaTitleLogin' }, { siteTitle })
: intl.formatMessage({ id: 'AuthenticationPage.schemaTitleSignup' }, { siteTitle });
const topbarClasses = classNames({
[css.hideOnMobile]: showEmailVerification,
});
return (
<Page
logoutError={logoutError}
title={schemaTitle}
scrollingDisabled={scrollingDisabled}
schema={{
'@context': 'http://schema.org',
'@type': 'WebPage',
name: schemaTitle,
}}
>
<LayoutSingleColumn>
<LayoutWrapperTopbar>
<TopbarContainer className={topbarClasses} />
</LayoutWrapperTopbar>
<LayoutWrapperMain className={css.layoutWrapperMain}>
<div className={css.root}>
{showEmailVerification ? emailVerificationContent : formContent}
</div>
<Modal
id="AuthenticationPage.tos"
isOpen={this.state.tosModalOpen}
onClose={() => this.setState({ tosModalOpen: false })}
onManageDisableScrolling={onManageDisableScrolling}
>
<div className={css.termsWrapper}>
<h2 className={css.termsHeading}>
<FormattedMessage id="AuthenticationPage.termsHeading" />
</h2>
<TermsOfService />
</div>
</Modal>
</LayoutWrapperMain>
<LayoutWrapperFooter>
<Footer />
</LayoutWrapperFooter>
</LayoutSingleColumn>
</Page>
);
}
}
AuthenticationPageComponent.defaultProps = {
currentUser: null,
@ -262,6 +290,7 @@ AuthenticationPageComponent.propTypes = {
sendVerificationEmailInProgress: bool.isRequired,
sendVerificationEmailError: propTypes.error,
onResendVerificationEmail: func.isRequired,
onManageDisableScrolling: func.isRequired,
// from withRouter
location: shape({ state: object }).isRequired,
@ -290,6 +319,8 @@ const mapDispatchToProps = dispatch => ({
submitLogin: ({ email, password }) => dispatch(login(email, password)),
submitSignup: params => dispatch(signup(params)),
onResendVerificationEmail: () => dispatch(sendVerificationEmail()),
onManageDisableScrolling: (componentId, disableScrolling) =>
dispatch(manageDisableScrolling(componentId, disableScrolling)),
});
// Note: it is important that the withRouter HOC is **outside** the

View file

@ -82,6 +82,25 @@ exports[`AuthenticationPageComponent matches snapshot 1`] = `
/>
</div>
</div>
<InjectIntl(ModalComponent)
id="AuthenticationPage.tos"
isOpen={false}
onClose={[Function]}
onManageDisableScrolling={[Function]}
>
<div>
<h2>
<FormattedMessage
id="AuthenticationPage.termsHeading"
values={Object {}}
/>
</h2>
<TermsOfService
className={null}
rootClassName={null}
/>
</div>
</InjectIntl(ModalComponent)>
</LayoutWrapperMain>
<LayoutWrapperFooter
className={null}

View file

@ -16,41 +16,6 @@
box-shadow: none;
}
.tabs {
display: flex;
flex-direction: row;
overflow-x: auto;
min-height: 48px;
padding-top: 2px;
@media (--viewportMedium) {
min-height: 56px;
padding-top: 10px;
}
@media (--viewportLarge) {
min-height: auto;
flex-direction: column;
margin-top: 28px;
padding-top: 0;
}
}
.tab {
display: flex;
align-items: flex-end;
height: 100%;
margin-left: 16px;
&:first-child {
margin-left: 0;
}
@media (--viewportLarge) {
margin-left: 0;
}
}
.title {
margin-top: 4px;
margin-bottom: 19px;

View file

@ -14,7 +14,6 @@ import {
LayoutWrapperFooter,
Footer,
Page,
TabNav,
UserNav,
} from '../../components';
import { ContactDetailsForm, TopbarContainer } from '../../containers';
@ -84,9 +83,7 @@ export const ContactDetailsPageComponent = props => {
/>
<UserNav selectedPageName="ContactDetailsPage" />
</LayoutWrapperTopbar>
<LayoutWrapperSideNav>
<TabNav rootClassName={css.tabs} tabRootClassName={css.tab} tabs={tabs} />
</LayoutWrapperSideNav>
<LayoutWrapperSideNav tabs={tabs} />
<LayoutWrapperMain>
<div className={css.content}>
<h1 className={css.title}>

View file

@ -27,38 +27,31 @@ exports[`ContactDetailsPage matches snapshot 1`] = `
<LayoutWrapperSideNav
className={null}
rootClassName={null}
>
<TabNav
className={null}
rootClassName={null}
tabClassName={null}
tabRootClassName={null}
tabs={
Array [
Object {
"linkProps": Object {
"name": "ContactDetailsPage",
},
"selected": true,
"text": <FormattedMessage
id="ContactDetailsPage.emailTabTitle"
values={Object {}}
/>,
tabs={
Array [
Object {
"linkProps": Object {
"name": "ContactDetailsPage",
},
Object {
"linkProps": Object {
"name": "PasswordChangePage",
},
"selected": false,
"text": <FormattedMessage
id="ContactDetailsPage.passwordTabTitle"
values={Object {}}
/>,
"selected": true,
"text": <FormattedMessage
id="ContactDetailsPage.emailTabTitle"
values={Object {}}
/>,
},
Object {
"linkProps": Object {
"name": "PasswordChangePage",
},
]
}
/>
</LayoutWrapperSideNav>
"selected": false,
"text": <FormattedMessage
id="ContactDetailsPage.passwordTabTitle"
values={Object {}}
/>,
},
]
}
/>
<LayoutWrapperMain
className={null}
rootClassName={null}

View file

@ -22,6 +22,7 @@ exports[`InboxPage matches snapshot 1`] = `
<LayoutWrapperSideNav
className={null}
rootClassName={null}
tabs={null}
>
<h1>
<FormattedMessage
@ -377,6 +378,7 @@ exports[`InboxPage matches snapshot 3`] = `
<LayoutWrapperSideNav
className={null}
rootClassName={null}
tabs={null}
>
<h1>
<FormattedMessage

View file

@ -16,41 +16,6 @@
box-shadow: none;
}
.tabs {
display: flex;
flex-direction: row;
overflow-x: auto;
min-height: 48px;
padding-top: 2px;
@media (--viewportMedium) {
min-height: 56px;
padding-top: 10px;
}
@media (--viewportLarge) {
min-height: auto;
flex-direction: column;
margin-top: 28px;
padding-top: 0;
}
}
.tab {
display: flex;
align-items: flex-end;
height: 100%;
margin-left: 16px;
&:first-child {
margin-left: 0;
}
@media (--viewportLarge) {
margin-left: 0;
}
}
.title {
margin-top: 4px;
margin-bottom: 19px;

View file

@ -13,7 +13,6 @@ import {
LayoutWrapperFooter,
Footer,
Page,
TabNav,
UserNav,
} from '../../components';
import { PasswordChangeForm, TopbarContainer } from '../../containers';
@ -74,9 +73,7 @@ export const PasswordChangePageComponent = props => {
/>
<UserNav selectedPageName="PasswordChangePage" />
</LayoutWrapperTopbar>
<LayoutWrapperSideNav>
<TabNav rootClassName={css.tabs} tabRootClassName={css.tab} tabs={tabs} />
</LayoutWrapperSideNav>
<LayoutWrapperSideNav tabs={tabs} />
<LayoutWrapperMain>
<div className={css.content}>
<h1 className={css.title}>

View file

@ -27,38 +27,31 @@ exports[`PasswordChangePage matches snapshot 1`] = `
<LayoutWrapperSideNav
className={null}
rootClassName={null}
>
<TabNav
className={null}
rootClassName={null}
tabClassName={null}
tabRootClassName={null}
tabs={
Array [
Object {
"linkProps": Object {
"name": "ContactDetailsPage",
},
"selected": false,
"text": <FormattedMessage
id="PasswordChangePage.emailTabTitle"
values={Object {}}
/>,
tabs={
Array [
Object {
"linkProps": Object {
"name": "ContactDetailsPage",
},
Object {
"linkProps": Object {
"name": "PasswordChangePage",
},
"selected": true,
"text": <FormattedMessage
id="PasswordChangePage.passwordTabTitle"
values={Object {}}
/>,
"selected": false,
"text": <FormattedMessage
id="PasswordChangePage.emailTabTitle"
values={Object {}}
/>,
},
Object {
"linkProps": Object {
"name": "PasswordChangePage",
},
]
}
/>
</LayoutWrapperSideNav>
"selected": true,
"text": <FormattedMessage
id="PasswordChangePage.passwordTabTitle"
values={Object {}}
/>,
},
]
}
/>
<LayoutWrapperMain
className={null}
rootClassName={null}

View file

@ -0,0 +1,15 @@
@import '../../marketplace.css';
.heading {
margin: 5px 0 18px 0;
@media (--viewportMedium) {
margin: 2px 0 23px 0;
}
}
.content {
@media (--viewportLarge) {
max-width: 563px;
}
}

View file

@ -0,0 +1,101 @@
import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
import * as propTypes from '../../util/propTypes';
import { isScrollingDisabled } from '../../ducks/UI.duck';
import { TopbarContainer } from '../../containers';
import {
Page,
LayoutSideNavigation,
LayoutWrapperMain,
LayoutWrapperSideNav,
LayoutWrapperTopbar,
LayoutWrapperFooter,
PrivacyPolicy,
Footer,
} from '../../components';
import config from '../../config';
import css from './PrivacyPolicyPage.css';
const PrivacyPolicyPageComponent = props => {
const { logoutError, scrollingDisabled, intl } = props;
const tabs = [
{
text: intl.formatMessage({ id: 'PrivacyPolicyPage.privacyTabTitle' }),
selected: true,
linkProps: {
name: 'PrivacyPolicyPage',
},
},
{
text: intl.formatMessage({ id: 'PrivacyPolicyPage.tosTabTitle' }),
selected: false,
linkProps: {
name: 'TermsOfServicePage',
},
},
];
const siteTitle = config.siteTitle;
const schemaTitle = intl.formatMessage({ id: 'PrivacyPolicyPage.schemaTitle' }, { siteTitle });
const schema = {
'@context': 'http://schema.org',
'@type': 'WebPage',
name: schemaTitle,
};
return (
<Page
title={schemaTitle}
logoutError={logoutError}
scrollingDisabled={scrollingDisabled}
schema={schema}
>
<LayoutSideNavigation>
<LayoutWrapperTopbar>
<TopbarContainer currentPage="PrivacyPolicyPage" />
</LayoutWrapperTopbar>
<LayoutWrapperSideNav tabs={tabs} />
<LayoutWrapperMain>
<div className={css.content}>
<h1 className={css.heading}>
<FormattedMessage id="PrivacyPolicyPage.heading" />
</h1>
<PrivacyPolicy />
</div>
</LayoutWrapperMain>
<LayoutWrapperFooter>
<Footer />
</LayoutWrapperFooter>
</LayoutSideNavigation>
</Page>
);
};
PrivacyPolicyPageComponent.defaultProps = {
logoutError: null,
};
const { bool } = PropTypes;
PrivacyPolicyPageComponent.propTypes = {
logoutError: propTypes.error,
scrollingDisabled: bool.isRequired,
// from injectIntl
intl: intlShape.isRequired,
};
const mapStateToProps = state => {
const { logoutError } = state.Auth;
return {
logoutError,
scrollingDisabled: isScrollingDisabled(state),
};
};
const PrivacyPolicyPage = compose(connect(mapStateToProps), injectIntl)(PrivacyPolicyPageComponent);
export default PrivacyPolicyPage;

View file

@ -29,3 +29,20 @@
.bottomWrapper {
@apply --marketplaceModalBottomWrapper;
}
.bottomWrapperText {
@apply --marketplaceModalBottomWrapperText;
}
.termsText {
@apply --marketplaceModalHelperText;
}
.termsLink {
@apply --marketplaceModalHelperLink;
&:hover {
text-decoration: underline;
cursor: pointer;
}
}

View file

@ -7,6 +7,9 @@ export const Empty = {
onSubmit(values) {
console.log('sign up with form values:', values);
},
onOpenTermsOfService() {
console.log('open terms of service');
},
},
group: 'forms',
};

View file

@ -9,6 +9,8 @@ import * as validators from '../../util/validators';
import css from './SignupForm.css';
const KEY_CODE_ENTER = 13;
const SignupFormComponent = props => {
const {
rootClassName,
@ -19,6 +21,7 @@ const SignupFormComponent = props => {
inProgress,
invalid,
intl,
onOpenTermsOfService,
} = props;
// email
@ -102,6 +105,24 @@ const SignupFormComponent = props => {
const submitInProgress = submitting || inProgress;
const submitDisabled = invalid || submitInProgress;
const handleTermsKeyUp = e => {
// Allow click action with keyboard like with normal links
if (e.keyCode === KEY_CODE_ENTER) {
onOpenTermsOfService();
}
};
const termsLink = (
<span
className={css.termsLink}
onClick={onOpenTermsOfService}
role="button"
tabIndex="0"
onKeyUp={handleTermsKeyUp}
>
<FormattedMessage id="SignupForm.termsAndConditionsLinkText" />
</span>
);
return (
<Form className={classes} onSubmit={handleSubmit}>
<div>
@ -145,6 +166,11 @@ const SignupFormComponent = props => {
</div>
<div className={css.bottomWrapper}>
<p className={css.bottomWrapperText}>
<span className={css.termsText}>
<FormattedMessage id="SignupForm.termsAndConditionsAcceptText" values={{ termsLink }} />
</span>
</p>
<PrimaryButton
className={css.submitButton}
type="submit"
@ -160,11 +186,15 @@ const SignupFormComponent = props => {
SignupFormComponent.defaultProps = { inProgress: false };
const { bool } = PropTypes;
const { bool, func } = PropTypes;
SignupFormComponent.propTypes = {
...formPropTypes,
inProgress: bool,
onOpenTermsOfService: func.isRequired,
// from injectIntl
intl: intlShape.isRequired,
};

View file

@ -2,9 +2,11 @@ import React from 'react';
import { renderDeep } from '../../util/test-helpers';
import SignupForm from './SignupForm';
const noop = () => null;
describe('SignupForm', () => {
it('matches snapshot', () => {
const tree = renderDeep(<SignupForm />);
const tree = renderDeep(<SignupForm onOpenTermsOfService={noop} />);
expect(tree).toMatchSnapshot();
});
});

View file

@ -104,6 +104,28 @@ exports[`SignupForm matches snapshot 1`] = `
<div
className={undefined}
>
<p
className={undefined}
>
<span
className={undefined}
>
<span>
By signing up you accept the
<span
className={undefined}
onClick={[Function]}
onKeyUp={[Function]}
role="button"
tabIndex="0"
>
<span>
terms and conditions
</span>
</span>
</span>
</span>
</p>
<button
className=""
disabled={true}

View file

@ -0,0 +1,15 @@
@import '../../marketplace.css';
.heading {
margin: 5px 0 18px 0;
@media (--viewportMedium) {
margin: 2px 0 23px 0;
}
}
.content {
@media (--viewportLarge) {
max-width: 563px;
}
}

View file

@ -0,0 +1,103 @@
import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
import * as propTypes from '../../util/propTypes';
import { isScrollingDisabled } from '../../ducks/UI.duck';
import { TopbarContainer } from '../../containers';
import {
Page,
LayoutSideNavigation,
LayoutWrapperMain,
LayoutWrapperSideNav,
LayoutWrapperTopbar,
LayoutWrapperFooter,
Footer,
TermsOfService,
} from '../../components';
import config from '../../config';
import css from './TermsOfServicePage.css';
const TermsOfServicePageComponent = props => {
const { logoutError, scrollingDisabled, intl } = props;
const tabs = [
{
text: intl.formatMessage({ id: 'TermsOfServicePage.privacyTabTitle' }),
selected: false,
linkProps: {
name: 'PrivacyPolicyPage',
},
},
{
text: intl.formatMessage({ id: 'TermsOfServicePage.tosTabTitle' }),
selected: true,
linkProps: {
name: 'TermsOfServicePage',
},
},
];
const siteTitle = config.siteTitle;
const schemaTitle = intl.formatMessage({ id: 'TermsOfServicePage.schemaTitle' }, { siteTitle });
const schema = {
'@context': 'http://schema.org',
'@type': 'WebPage',
name: schemaTitle,
};
return (
<Page
title={schemaTitle}
logoutError={logoutError}
scrollingDisabled={scrollingDisabled}
schema={schema}
>
<LayoutSideNavigation>
<LayoutWrapperTopbar>
<TopbarContainer currentPage="TermsOfServicePage" />
</LayoutWrapperTopbar>
<LayoutWrapperSideNav tabs={tabs} />
<LayoutWrapperMain>
<div className={css.content}>
<h1 className={css.heading}>
<FormattedMessage id="TermsOfServicePage.heading" />
</h1>
<TermsOfService />
</div>
</LayoutWrapperMain>
<LayoutWrapperFooter>
<Footer />
</LayoutWrapperFooter>
</LayoutSideNavigation>
</Page>
);
};
TermsOfServicePageComponent.defaultProps = {
logoutError: null,
};
const { bool } = PropTypes;
TermsOfServicePageComponent.propTypes = {
logoutError: propTypes.error,
scrollingDisabled: bool.isRequired,
// from injectIntl
intl: intlShape.isRequired,
};
const mapStateToProps = state => {
const { logoutError } = state.Auth;
return {
logoutError,
scrollingDisabled: isScrollingDisabled(state),
};
};
const TermsOfServicePage = compose(connect(mapStateToProps), injectIntl)(
TermsOfServicePageComponent
);
export default TermsOfServicePage;

View file

@ -31,6 +31,7 @@ export { default as PasswordResetForm } from './PasswordResetForm/PasswordResetF
export { default as PasswordResetPage } from './PasswordResetPage/PasswordResetPage';
export { default as PayoutDetailsForm } from './PayoutDetailsForm/PayoutDetailsForm';
export { default as PayoutPreferencesPage } from './PayoutPreferencesPage/PayoutPreferencesPage';
export { default as PrivacyPolicyPage } from './PrivacyPolicyPage/PrivacyPolicyPage';
export { default as ProfilePage } from './ProfilePage/ProfilePage';
export { default as ProfileSettingsForm } from './ProfileSettingsForm/ProfileSettingsForm';
export { default as ProfileSettingsPage } from './ProfileSettingsPage/ProfileSettingsPage';
@ -40,5 +41,6 @@ export { default as SignupForm } from './SignupForm/SignupForm';
export { default as StaticPage } from './StaticPage/StaticPage';
export { default as StripePaymentForm } from './StripePaymentForm/StripePaymentForm';
export { default as StyleguidePage } from './StyleguidePage/StyleguidePage';
export { default as TermsOfServicePage } from './TermsOfServicePage/TermsOfServicePage';
export { default as TopbarContainer } from './TopbarContainer/TopbarContainer';
export { default as TopbarSearchForm } from './TopbarSearchForm/TopbarSearchForm';

View file

@ -5,6 +5,7 @@ import {
CheckoutPage,
ContactDetailsPage,
EditListingPage,
EmailVerificationPage,
InboxPage,
LandingPage,
ListingPage,
@ -12,15 +13,16 @@ import {
NotFoundPage,
OrderPage,
PasswordChangePage,
PasswordResetPage,
PasswordRecoveryPage,
PasswordResetPage,
PayoutPreferencesPage,
PrivacyPolicyPage,
ProfilePage,
ProfileSettingsPage,
SalePage,
SearchPage,
StyleguidePage,
EmailVerificationPage,
TermsOfServicePage,
} from './containers';
// routeConfiguration needs to initialize containers first
@ -241,6 +243,16 @@ const routeConfiguration = () => {
name: 'PayoutPreferencesPage',
component: props => <PayoutPreferencesPage {...props} />,
},
{
path: '/terms-of-service',
name: 'TermsOfServicePage',
component: props => <TermsOfServicePage {...props} />,
},
{
path: '/privacy-policy',
name: 'PrivacyPolicyPage',
component: props => <PrivacyPolicyPage {...props} />,
},
{
path: '/styleguide',
name: 'Styleguide',

View file

@ -8,12 +8,13 @@
"AuthenticationPage.resendEmailLinkText": "Resend it",
"AuthenticationPage.resendFailed": "Resending verification email failed. Please try again.",
"AuthenticationPage.resendFailedTooManyRequests": "Resending verification email failed, since youve already been sent 5 verification emails. Please try again tomorrow.",
"AuthenticationPage.schemaTitleSignup": "Sign up | {siteTitle}",
"AuthenticationPage.schemaTitleLogin": "Log in | {siteTitle}",
"AuthenticationPage.schemaTitleSignup": "Sign up | {siteTitle}",
"AuthenticationPage.sendingEmail": "Sending verification email…",
"AuthenticationPage.signupFailed": "Signup failed. Please make sure all the information you entered is correct and try again.",
"AuthenticationPage.signupFailedEmailAlreadyTaken": "An account already exists with this email address. Try logging in instead.",
"AuthenticationPage.signupLinkText": "Sign up",
"AuthenticationPage.termsHeading": "Terms of Service",
"AuthenticationPage.verifyEmailClose": "LATER",
"AuthenticationPage.verifyEmailText": "Thanks for signing up! There's one quick step left. To be able to contact you, we need you to verify your email address. Please click the link we sent to {email}.",
"AuthenticationPage.verifyEmailTitle": "{name}, check your inbox to verify your email",
@ -23,11 +24,11 @@
"BookingBreakdown.nightCount": "{count, number} {count, plural, one {night} other {nights}}",
"BookingBreakdown.pricePerNight": "Price per night",
"BookingBreakdown.providerTotalCanceled": "Total price",
"BookingBreakdown.providerTotalDeclined": "You would have made",
"BookingBreakdown.providerTotalDefault": "You'll make",
"BookingBreakdown.providerTotalDelivered": "You made",
"BookingBreakdown.providerTotalDeclined": "You would have made",
"BookingBreakdown.subTotal": "Subtotal",
"BookingBreakdown.refund": "Refund",
"BookingBreakdown.subTotal": "Subtotal",
"BookingBreakdown.total": "Total price",
"BookingDatesForm.bookingEndTitle": "End date",
"BookingDatesForm.bookingStartTitle": "Start date",
@ -169,7 +170,7 @@
"Footer.searchTurku": "Turku",
"Footer.searchVantaa": "Vantaa",
"Footer.terms": "Terms",
"Footer.termsOfUse": "Terms of use",
"Footer.termsOfUse": "Terms of Service",
"Footer.toAboutPage": "About us",
"Footer.toContactPage": "Contact",
"Footer.toFAQPage": "FAQ",
@ -266,13 +267,13 @@
"OrderDetailsPanel.orderAutoDeclinedTitle": "You requested to book {listingLink}",
"OrderDetailsPanel.orderCanceledStatus": "The request was cancelled on {transitionDate}",
"OrderDetailsPanel.orderCanceledTitle": "You booked {listingLink}",
"OrderDetailsPanel.orderDeclinedStatus": "Unfortunately {providerName} declined the request on {transitionDate}.",
"OrderDetailsPanel.orderDeclinedTitle": "You requested to book {listingLink}",
"OrderDetailsPanel.orderDeliveredStatus": " ",
"OrderDetailsPanel.orderDeliveredTitle": "Booking period has ended",
"OrderDetailsPanel.orderPreauthorizedStatus": "{providerName} has been notified about the booking request, so sit back and relax.",
"OrderDetailsPanel.orderPreauthorizedSubtitle": "You have requested to book {listingLink}",
"OrderDetailsPanel.orderPreauthorizedTitle": "Great success, {customerName}!",
"OrderDetailsPanel.orderDeclinedStatus": "Unfortunately {providerName} declined the request on {transitionDate}.",
"OrderDetailsPanel.orderDeclinedTitle": "You requested to book {listingLink}",
"OrderPage.fetchOrderFailed": "Fetching order data failed.",
"OrderPage.loadingData": "Loading order data.",
"OrderPage.title": "Order details: {listingTitle}",
@ -385,6 +386,10 @@
"PayoutDetailsForm.streetAddressRequired": "This field is required",
"PayoutDetailsForm.submitButtonText": "Save details & publish listing",
"PayoutDetailsForm.title": "One more thing: payout preferences",
"PrivacyPolicyPage.heading": "Saunatime Privacy Policy",
"PrivacyPolicyPage.privacyTabTitle": "Privacy Policy",
"PrivacyPolicyPage.schemaTitle": "Privacy Policy | {siteTitle}",
"PrivacyPolicyPage.tosTabTitle": "Terms of Service",
"ProfileSettingsForm.addYourProfilePicture": "+ Add your profile picture…",
"ProfileSettingsForm.addYourProfilePictureMobile": "+ Add",
"ProfileSettingsForm.changeAvatar": "Change",
@ -408,23 +413,23 @@
"SaleDetailsPanel.bannedUserDisplayName": "Banned user",
"SaleDetailsPanel.bookingBreakdownTitle": "Booking breakdown",
"SaleDetailsPanel.customerBannedStatus": "The user made the request, but was later banned.",
"SaleDetailsPanel.declineSaleFailed": "Oops, declining failed. Please try again.",
"SaleDetailsPanel.listingAcceptedTitle": "Woohoo! You accepted a request from {customerName} for {listingLink}",
"SaleDetailsPanel.listingCanceledTitle": "{customerName} booked {listingLink}",
"SaleDetailsPanel.listingDeliveredTitle": "{customerName} booked {listingLink}",
"SaleDetailsPanel.listingDeclinedTitle": "{customerName} requested to book {listingLink}",
"SaleDetailsPanel.listingDeliveredTitle": "{customerName} booked {listingLink}",
"SaleDetailsPanel.listingRequestedTitle": "{customerName} has requested to book {listingLink}",
"SaleDetailsPanel.declineSaleFailed": "Oops, declining failed. Please try again.",
"SaleDetailsPanel.saleAcceptedStatus": "You accepted the request on {formattedDate}.",
"SaleDetailsPanel.saleAutoDeclinedStatus": "You did not react to the request on time. The request expired on {formattedDate}.",
"SaleDetailsPanel.saleCanceledStatus": "The request was cancelled on {formattedDate}.",
"SaleDetailsPanel.saleDeliveredStatus": "You delivered the order on {formattedDate}.",
"SaleDetailsPanel.saleDeclinedStatus": "You declined the request on {formattedDate}.",
"SaleDetailsPanel.saleDeliveredStatus": "You delivered the order on {formattedDate}.",
"SaleDetailsPanel.saleRequestedStatus": "{customerName} is waiting for your response.",
"SalePage.acceptButton": "Accept",
"SalePage.fetchSaleFailed": "Fetching sale data failed.",
"SalePage.loadingData": "Loading sale data.",
"SalePage.declineButton": "Decline",
"SalePage.declineSaleFailed": "Declining sale failed.",
"SalePage.fetchSaleFailed": "Fetching sale data failed.",
"SalePage.loadingData": "Loading sale data.",
"SalePage.title": "Sale details: {title}",
"SearchMapInfoCard.noImage": "No image",
"SearchPage.foundResults": "{count, number} {count, plural, one {sauna} other {saunas}} found",
@ -433,8 +438,8 @@
"SearchPage.loadingResults": "Loading search results…",
"SearchPage.noResults": "Could not find any listings.",
"SearchPage.openMapView": "Map",
"SearchPage.schemaMapSearch": "map search",
"SearchPage.schemaDescription": "Showing search results",
"SearchPage.schemaMapSearch": "map search",
"SearchPage.schemaTitle": "Search results for {searchAddress} | {siteTitle}",
"SearchPage.searchError": "Search failed. Please try again.",
"SearchResultsPanel.nextPage": "Next page",
@ -455,8 +460,8 @@
"SignupForm.passwordTooLong": "Password should be at most {maxLength} characters.",
"SignupForm.passwordTooShort": "Password should be at least {minLength} characters.",
"SignupForm.signUp": "Sign up",
"SignupForm.stripeConnectedAccountAgreementLinkText": "Stripe Connected Account Agreement",
"SignupForm.termsOfService": "By confirming I accept the terms and conditions and the {stripeConnectedAccountAgreementLink}",
"SignupForm.termsAndConditionsAcceptText": "By signing up you accept the {termsLink}",
"SignupForm.termsAndConditionsLinkText": "terms and conditions",
"StripeBankAccountTokenInputField.accountNumber.inline": "account number",
"StripeBankAccountTokenInputField.accountNumber.invalid": "Invalid {country} bank account number",
"StripeBankAccountTokenInputField.accountNumber.label": "Bank account number",
@ -509,6 +514,10 @@
"StripePaymentForm.stripe.validation_error.missing": "There is no card on a customer that is being charged.",
"StripePaymentForm.stripe.validation_error.processing_error": "An error occurred while processing the card.",
"StripePaymentForm.submitPaymentInfo": "Send request",
"TermsOfServicePage.heading": "Terms of Service",
"TermsOfServicePage.privacyTabTitle": "Privacy Policy",
"TermsOfServicePage.schemaTitle": "Terms of Service | {siteTitle}",
"TermsOfServicePage.tosTabTitle": "Terms of Service",
"Topbar.checkInbox": "Please check your inbox and verify your email {email}",
"Topbar.closeVerifyEmailReminder": "Later",
"Topbar.fixEmail": "Whoops, typo in your email? {fixEmailLink}",