LandingPage mobile and desktop styles and components

This commit is contained in:
Kimmo Puputti 2017-06-08 10:13:28 +03:00
parent 9ae4fd86c2
commit 9a5552abbc
29 changed files with 440 additions and 307 deletions

View file

@ -1,55 +1,70 @@
.section {
width: 100%;
min-height: calc(100vh - 60px);
background-image: url("data:image/svg+xml;utf8,<svg width='375' height='240' preserveAspectRatio='none' xmlns='http://www.w3.org/2000/svg'><g fill='none' fill-rule='evenodd'><path fill='#D8D8D8' d='M0 0h375v240H0z' vector-effect='non-scaling-stroke'/><path d='M375 0l-375 240M.5.5l375 240' stroke='#979797' stroke-linecap='square' vector-effect='non-scaling-stroke'/></g></svg>");
background-size: 100% 100%;
background-position: center;
@import '../../marketplace.css';
display: flex;
justify-content: center;
:root {
--desktopTitleMaxWidth: 625px;
}
.content {
.root {
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--matterColor);
background-image: url(./background.jpg);
background-size: cover;
background-position-y: center;
/*
This value is specific to the given image. The value is the
relative position of the interesting area in the x axis, counted
from the left side of the image.
In the default image, this means the person sitting in the
sauna. This value ensures the person is approximately centered in
the viewport when the browser width changes.
*/
background-position-x: 80%;
}
.titleWrapper {
text-align: center;
.heroMainTitle {
composes: heroTitle from '../../marketplace.css';
color: var(--matterColorLight);
margin-top: auto;
margin-bottom: 12px;
/* Mobile padding */
margin-left: 24px; /* Number out of my head, not sure if it's the right one */
margin-right: 24px; /* Number out of my head, not sure if it's the right one */
@media (--desktopViewport) {
max-width: var(--desktopTitleMaxWidth);
}
}
.title {
/* Font */
font-size: 24px;
letter-spacing: -0.6px;
font-weight: bold;
.heroSubTitle {
color: var(--matterColorLight);
margin-bottom: 47px;
/* Mobile padding */
margin-top: 64px;
margin-bottom: 4px;
}
.subTitle {
/* Font */
font-size: 20px;
letter-spacing: -0.5px;
font-weight: bold;
/* Mobile padding */
margin-bottom: 48px;
@media (--desktopViewport) {
max-width: var(--desktopTitleMaxWidth);
}
}
.ctaWrapper {
display: flex;
flex-direction: column;
flex-grow: 1;
align-items: center;
.mobileSearchButton {
border-radius: 4px;
/* Mobile dimensions */
width: 235px;
@media (--desktopViewport) {
display: none;
}
}
.searchIcon {
stroke: var(--matterColorLight);
width: 16px;
height: 16px;
margin-right: 10px;
}
.desktopSearchForm {
display: none;
background-color: var(--matterColorLight);
@media (--desktopViewport) {
display: block;
width: 400px;
}
}

View file

@ -1,29 +1,64 @@
import React, { PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { SearchIcon } from '../../components';
import { LocationSearchForm } from '../../containers';
import { stringify } from '../../util/urlHelpers';
import { createResourceLocatorString } from '../../util/routes';
import * as propTypes from '../../util/propTypes';
import css from './HeroSection.css';
const HeroSection = props => (
<section className={css.section}>
<div className={css.content}>
<div className={css.titleWrapper}>
<div className={css.title}>
<FormattedMessage id="HeroSection.title" />
</div>
<div className={css.subTitle}>
<FormattedMessage id="HeroSection.subTitle" />
</div>
</div>
<div className={css.ctaWrapper}>
{props.children}
</div>
const HeroSection = props => {
const { rootClassName, className, flattenedRoutes, history, location } = props;
const handleMobileSearchClick = () => {
const params = { mobilesearch: 'open' };
const path = `${location.pathname}?${stringify(params)}`;
history.push(path);
};
const handleSearchSubmit = values => {
const { search, selectedPlace } = values.location;
const { origin, bounds, country } = selectedPlace;
const searchParams = { address: search, origin, bounds, country };
history.push(createResourceLocatorString('SearchPage', flattenedRoutes, {}, searchParams));
};
const classes = classNames(rootClassName || css.root, className);
return (
<div className={classes}>
<h1 className={css.heroMainTitle}>
<FormattedMessage id="HeroSection.title" />
</h1>
<p className={css.heroSubTitle}>
<FormattedMessage id="HeroSection.subTitle" />
</p>
<button className={css.mobileSearchButton} onClick={handleMobileSearchClick}>
<SearchIcon rootClassName={css.searchIcon} />
<FormattedMessage id="HeroSection.mobileSearchButtonText" />
</button>
<LocationSearchForm className={css.desktopSearchForm} onSubmit={handleSearchSubmit} />
</div>
</section>
);
);
};
HeroSection.defaultProps = { children: [] };
HeroSection.defaultProps = { rootClassName: null, className: null };
const { any } = PropTypes;
const { string, shape, func, arrayOf } = PropTypes;
HeroSection.propTypes = { children: any };
HeroSection.propTypes = {
rootClassName: string,
className: string,
flattenedRoutes: arrayOf(propTypes.route).isRequired,
history: shape({
push: func.isRequired,
}).isRequired,
location: shape({
search: string.isRequired,
}).isRequired,
};
export default HeroSection;

View file

@ -2,13 +2,18 @@ import React from 'react';
import { renderDeep } from '../../util/test-helpers';
import HeroSection from './HeroSection';
const noop = () => null;
describe('HeroSection', () => {
it('matches snapshot', () => {
const tree = renderDeep(
<HeroSection params={{ displayName: 'most-awesome-shop' }}>
test
</HeroSection>
);
window.google = { maps: {} };
const heroProps = {
flattenedRoutes: [],
history: { push: noop },
location: { search: '' },
};
const tree = renderDeep(<HeroSection {...heroProps} />);
delete window.google;
expect(tree).toMatchSnapshot();
});
});

View file

@ -1,27 +1,89 @@
exports[`HeroSection matches snapshot 1`] = `
<section
className={undefined}>
<div
<div
className="">
<h1
className={undefined}>
<span>
Book saunas everywhere.
</span>
</h1>
<p
className={undefined}>
<span>
The largest online community to rent saunas in Finland.
</span>
</p>
<button
className={undefined}
onClick={[Function]}>
<svg
className=""
height="22"
viewBox="0 0 21 22"
width="21"
xmlns="http://www.w3.org/2000/svg">
<g
fill="none"
fillRule="evenodd"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
transform="matrix(-1 0 0 1 20 1)">
<path
d="M13 14l5.241 5.241" />
<circle
cx="7.5"
cy="7.5"
r="7.5" />
</g>
</svg>
<span>
Search saunas
</span>
</button>
<form
className=""
onSubmit={[Function]}>
<div
className={undefined}>
className="">
<div
className={undefined}>
<span>
Book Studiotime anywhere
</span>
</div>
<div
className={undefined}>
<span>
The largest online community to rent music studios
</span>
className="">
<svg
className={undefined}
height="22"
viewBox="0 0 21 22"
width="21"
xmlns="http://www.w3.org/2000/svg">
<g
className={undefined}
fill="none"
fillRule="evenodd"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
transform="matrix(-1 0 0 1 20 1)">
<path
d="M13 14l5.241 5.241" />
<circle
cx="7.5"
cy="7.5"
r="7.5" />
</g>
</svg>
</div>
<input
autoComplete="off"
autoFocus={false}
className=""
name="location"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="Search saunas..."
type="search"
value="" />
</div>
<div
className={undefined}>
test
</div>
</div>
</section>
</form>
</div>
`;

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

View file

@ -3,4 +3,10 @@
width: 100%;
border: 0;
cursor: pointer;
background-color: transparent;
&:hover {
background-color: transparent;
box-shadow: none;
}
}

View file

@ -51,6 +51,7 @@
&:enabled:hover {
background-color: transparent;
color: var(--matterColorDark);
box-shadow: none;
}
&:enabled:active {
background-color: transparent;

View file

@ -0,0 +1,5 @@
@import '../../marketplace.css';
.root {
stroke: var(--marketplaceColor);
}

View file

@ -0,0 +1,43 @@
import React, { PropTypes } from 'react';
import classNames from 'classnames';
import css from './SearchIcon.css';
const SearchIcon = props => {
const { rootClassName, className } = props;
const classes = classNames(rootClassName || css.root, className);
return (
<svg
className={classes}
width="21"
height="22"
viewBox="0 0 21 22"
xmlns="http://www.w3.org/2000/svg"
>
<g
transform="matrix(-1 0 0 1 20 1)"
strokeWidth="2"
fill="none"
fillRule="evenodd"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M13 14l5.241 5.241" /><circle cx="7.5" cy="7.5" r="7.5" />
</g>
</svg>
);
};
SearchIcon.defaultProps = {
rootClassName: null,
className: null,
};
const { string } = PropTypes;
SearchIcon.propTypes = {
rootClassName: string,
className: string,
};
export default SearchIcon;

View file

@ -4,7 +4,7 @@
.root {
/* Size */
width: 100%;
height: 72px;
height: var(--topbarHeightDesktop);
/* Layout for child components */
display: flex;
@ -121,6 +121,7 @@
width: 100%;
padding: 20px 24px;
composes: h4Font from '../../marketplace.css';
composes: textColor from '../../marketplace.css';
text-align: left;
white-space: nowrap;

View file

@ -26,7 +26,7 @@
.avatar {
flex-shrink: 0;
margin-top: 72px;
margin-top: 96px;
width: 120px;
height: 120px;
border-radius: 50%;
@ -51,6 +51,7 @@
.logoutButton {
/* Position component */
margin: 6px 0;
width: initial;
/* Logout font is smaller and gray since the action is not recommended. */
composes: h3Font from '../../marketplace.css';

View file

@ -39,6 +39,7 @@ import PaginationLinks from './PaginationLinks/PaginationLinks';
import Promised from './Promised/Promised';
import RoutesProvider from './RoutesProvider/RoutesProvider';
import SaleDetailsPanel from './SaleDetailsPanel/SaleDetailsPanel';
import SearchIcon from './SearchIcon/SearchIcon';
import SearchResultsPanel from './SearchResultsPanel/SearchResultsPanel';
import Select from './Select/Select';
import StripeBankAccountToken from './StripeBankAccountToken/StripeBankAccountToken';
@ -92,6 +93,7 @@ export {
Promised,
RoutesProvider,
SaleDetailsPanel,
SearchIcon,
SearchResultsPanel,
Select,
StripeBankAccountToken,

View file

@ -1,3 +1,22 @@
.form {
width: 100%;
@import '../../marketplace.css';
.root {
}
.hero {
height: calc(100vh - var(--topbarHeight));
padding: 0 24px 26px 24px;
@media (--desktopViewport) {
height: calc(100vh - var(--topbarHeightDesktop));
}
@media (min-width: 768px) {
padding: 0 24px 26px 24px;
}
@media (min-width: 1024px){
padding: 0 0 195px 192px;
}
}

View file

@ -1,40 +1,33 @@
import React, { PropTypes } from 'react';
import { withRouter } from 'react-router-dom';
import { HeroSection, PageLayout } from '../../components';
import { SearchForm } from '../../containers';
import { createResourceLocatorString } from '../../util/routes';
import * as propTypes from '../../util/propTypes';
import { PageLayout, HeroSection } from '../../components';
import { withFlattenedRoutes } from '../../util/contextHelpers';
import css from './LandingPage.css';
export const LandingPageComponent = props => {
const { history, flattenedRoutes } = props;
const handleSubmit = values => {
const selectedPlace = values && values.location ? values.location.selectedPlace : null;
const { address, origin, bounds, country } = selectedPlace || {};
const searchParams = { address, origin, bounds, country };
history.push(createResourceLocatorString('SearchPage', flattenedRoutes, {}, searchParams));
};
const { flattenedRoutes, history, location } = props;
return (
<PageLayout title="Landing page">
<HeroSection>
<SearchForm form="LandingPageSearchForm" className={css.form} onSubmit={handleSubmit} />
</HeroSection>
<PageLayout title="Landing page" className={css.root}>
<HeroSection
className={css.hero}
flattenedRoutes={flattenedRoutes}
history={history}
location={location}
/>
</PageLayout>
);
};
const { func, arrayOf, shape } = PropTypes;
const { array, object } = PropTypes;
LandingPageComponent.propTypes = {
flattenedRoutes: arrayOf(propTypes.route).isRequired,
// from withFlattenedRoutes
flattenedRoutes: array.isRequired,
// from withRouter
history: shape({
push: func.isRequired,
}).isRequired,
history: object.isRequired,
location: object.isRequired,
};
export default withRouter(LandingPageComponent);
export default withRouter(withFlattenedRoutes(LandingPageComponent));

View file

@ -10,9 +10,9 @@ describe('LandingPage', () => {
it('matches snapshot', () => {
const tree = renderShallow(
<LandingPageComponent
onLocationChanged={v => v}
history={{ push: noop }}
flattenedRoutes={[]}
history={{ push: noop }}
location={{ search: '' }}
/>
);
expect(tree).toMatchSnapshot();

View file

@ -1,10 +1,19 @@
exports[`LandingPage matches snapshot 1`] = `
<Connect(withRouter(PageLayout))
title="Landing page">
<HeroSection>
<ReduxForm
form="LandingPageSearchForm"
onSubmit={[Function]} />
</HeroSection>
<HeroSection
className={null}
flattenedRoutes={Array []}
history={
Object {
"push": [Function],
}
}
location={
Object {
"search": "",
}
}
rootClassName={null} />
</Connect(withRouter(PageLayout))>
`;

View file

@ -0,0 +1,18 @@
.root {
}
.searchInputIcon {
box-sizing: content-box;
padding-left: 24px;
margin-top: 2px;
}
.searchInput {
height: 69px;
}
.searchPredictions {
margin-top: 16px;
margin-bottom: 102px;
}

View file

@ -0,0 +1,66 @@
import React, { PropTypes } from 'react';
import { compose } from 'redux';
import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form';
import { intlShape, injectIntl } from 'react-intl';
import classNames from 'classnames';
import { LocationAutocompleteInput } from '../../components';
import css from './LocationSearchForm.css';
const LocationSearchFormComponent = props => {
const { rootClassName, className, intl, onSubmit } = props;
const onChange = location => {
if (location.selectedPlace) {
// Note that we use `onSubmit` instead of the conventional
// `handleSubmit` prop for submitting. We want to autosubmit
// when a place is selected, and don't require any extra
// validations for the form.
onSubmit({ location });
}
};
const classes = classNames(rootClassName || css.root, className);
// Allow form submit only when the place has changed
const preventFormSubmit = e => e.preventDefault();
return (
<form className={classes} onSubmit={preventFormSubmit}>
<Field
name="location"
label="Location"
placeholder={intl.formatMessage({ id: 'TopbarSearchForm.placeholder' })}
format={null}
component={LocationAutocompleteInput}
iconClassName={css.searchInputIcon}
inputClassName={css.searchInput}
predictionsClassName={css.searchPredictions}
onChange={onChange}
/>
</form>
);
};
const { func, string } = PropTypes;
LocationSearchFormComponent.defaultProps = { rootClassName: null, className: null };
LocationSearchFormComponent.propTypes = {
...formPropTypes,
rootClassName: string,
className: string,
onSubmit: func.isRequired,
// from injectIntl
intl: intlShape.isRequired,
};
const defaultFormName = 'TopbarSearchForm';
const LocationSearchForm = compose(reduxForm({ form: defaultFormName }), injectIntl)(
LocationSearchFormComponent
);
export default LocationSearchForm;

View file

@ -1,22 +0,0 @@
.locationInput {
width: 100%;
background-color: white;
/* Font */
font-size: 16px;
letter-spacing: -0.4px;
font-weight: bold;
}
.locationButton {
width: 100%;
height: 50px;
margin: 0;
text-align: right;
}
.searchIcon {
display: inline-block;
margin-left: 12px;
margin-right: 12px;
}

View file

@ -1,12 +0,0 @@
/* eslint-disable no-console */
import SearchForm from './SearchForm';
export const Empty = {
component: SearchForm,
props: {
onSubmit(values) {
console.log('submit search query:', values);
},
},
group: 'forms',
};

View file

@ -1,55 +0,0 @@
import React from 'react';
import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form';
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
import { LocationAutocompleteInput, Button } from '../../components';
import css from './SearchForm.css';
const SearchForm = props => {
const { className, intl, handleSubmit, submitting } = props;
const addClassName = className ? { className } : {};
const submitDisabled = submitting;
return (
<form {...addClassName} onSubmit={handleSubmit}>
<Field
inputClassName={css.locationInput}
name="location"
label="Location"
placeholder={intl.formatMessage({ id: 'SearchForm.placeholder' })}
format={null}
component={LocationAutocompleteInput}
/>
<Button className={css.locationButton} type="submit" disabled={submitDisabled}>
<FormattedMessage id="SearchForm.search" />
<div className={css.searchIcon}>
<svg width="18" height="18" viewBox="189 165 18 18" xmlns="http://www.w3.org/2000/svg">
<g
fill="none"
fillRule="evenodd"
transform="matrix(-1 0 0 1 206 166)"
strokeLinecap="round"
strokeLinejoin="round"
stroke="#FFF"
strokeWidth="1.5"
>
<path d="M11.733 11.733l3.727 3.727" /><circle cx="6.4" cy="6.4" r="6.4" />
</g>
</svg>
</div>
</Button>
</form>
);
};
SearchForm.propTypes = {
...formPropTypes,
// from injectIntl
intl: intlShape.isRequired,
};
const defaultFormName = 'SearchForm';
export default reduxForm({ form: defaultFormName })(injectIntl(SearchForm));

View file

@ -1,12 +0,0 @@
import React from 'react';
import { renderDeep } from '../../util/test-helpers';
import SearchForm from './SearchForm';
describe('SearchForm', () => {
it('matches snapshot', () => {
window.google = { maps: {} };
const tree = renderDeep(<SearchForm />);
expect(tree).toMatchSnapshot();
delete window.google;
});
});

View file

@ -1,77 +0,0 @@
exports[`SearchForm matches snapshot 1`] = `
<form
onSubmit={[Function]}>
<div
className="">
<div
className="">
<svg
className={undefined}
height="22"
viewBox="0 0 21 22"
width="21"
xmlns="http://www.w3.org/2000/svg">
<g
className={undefined}
fill="none"
fillRule="evenodd"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
transform="matrix(-1 0 0 1 20 1)">
<path
d="M13 14l5.241 5.241" />
<circle
cx="7.5"
cy="7.5"
r="7.5" />
</g>
</svg>
</div>
<input
autoComplete="off"
autoFocus={false}
className=""
name="location"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="Search by location, e.g. New York"
type="search"
value="" />
</div>
<button
className=""
disabled={false}
type="submit">
<span>
Search
</span>
<div
className={undefined}>
<svg
height="18"
viewBox="189 165 18 18"
width="18"
xmlns="http://www.w3.org/2000/svg">
<g
fill="none"
fillRule="evenodd"
stroke="#FFF"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
transform="matrix(-1 0 0 1 206 166)">
<path
d="M11.733 11.733l3.727 3.727" />
<circle
cx="6.4"
cy="6.4"
r="6.4" />
</g>
</svg>
</div>
</button>
</form>
`;

View file

@ -1,7 +1,6 @@
@import '../../marketplace.css';
:root {
--topbarHeight: 60px;
--logoHeight: 25px;
}
@ -61,6 +60,7 @@
&:enabled:hover,
&:enabled:active {
background-color: transparent;
box-shadow: none;
}
&:disabled {
background-color: transparent;

View file

@ -25,11 +25,12 @@
}
.desktopIcon {
height: var(--topbarHeightDesktop);
border: none;
}
.desktopInput {
height: var(--topbarMargin);
height: var(--topbarHeightDesktop);
border: none;
padding-top: 0;
padding-bottom: var(--bottomBorder);
@ -43,6 +44,6 @@
}
.desktopPredictions {
margin-top: calc(var(--topbarMargin) - var(--inputHeight));
margin-top: calc(var(--topbarHeightDesktop) - var(--inputHeight));
max-width: 434px;
}

View file

@ -13,6 +13,7 @@ import EditProfilePage from './EditProfilePage/EditProfilePage';
import InboxPage from './InboxPage/InboxPage';
import LandingPage from './LandingPage/LandingPage';
import ListingPage from './ListingPage/ListingPage';
import LocationSearchForm from './LocationSearchForm/LocationSearchForm';
import LoginForm from './LoginForm/LoginForm';
import ManageListingsPage from './ManageListingsPage/ManageListingsPage';
import NotFoundPage from './NotFoundPage/NotFoundPage';
@ -24,7 +25,6 @@ import PayoutDetailsForm from './PayoutDetailsForm/PayoutDetailsForm';
import PayoutPreferencesPage from './PayoutPreferencesPage/PayoutPreferencesPage';
import ProfilePage from './ProfilePage/ProfilePage';
import SalePage from './SalePage/SalePage';
import SearchForm from './SearchForm/SearchForm';
import SearchPage from './SearchPage/SearchPage';
import SecurityPage from './SecurityPage/SecurityPage';
import SignupForm from './SignupForm/SignupForm';
@ -49,6 +49,7 @@ export {
InboxPage,
LandingPage,
ListingPage,
LocationSearchForm,
LoginForm,
ManageListingsPage,
NotFoundPage,
@ -60,7 +61,6 @@ export {
PayoutPreferencesPage,
ProfilePage,
SalePage,
SearchForm,
SearchPage,
SecurityPage,
SignupForm,

View file

@ -35,7 +35,6 @@ import * as EditListingPhotosForm
from './containers/EditListingPhotosForm/EditListingPhotosForm.example';
import * as EditListingPricingForm
from './containers/EditListingPricingForm/EditListingPricingForm.example';
import * as SearchForm from './containers/SearchForm/SearchForm.example';
import * as LoginForm from './containers/LoginForm/LoginForm.example';
import * as PasswordForgottenForm
from './containers/PasswordForgottenForm/PasswordForgottenForm.example';
@ -71,7 +70,6 @@ export {
PaginationLinks,
PasswordForgottenForm,
PayoutDetailsForm,
SearchForm,
SignupForm,
StripeBankAccountToken,
StripePaymentForm,

View file

@ -28,6 +28,7 @@
--boxShadowLight: 0 2px 4px 0 rgba(0, 0, 0, 0.05);
--boxShadowPopup: 0 8px 16px 0 rgba(0, 0, 0, 0.3);
--boxShadowPopupLight: 0 3px 6px 0 rgba(0, 0, 0, 0.3);
--boxShadowButton: 0 4px 8px 0 rgba(0, 0, 0, 0.10);
/* z-index base levels */
/* small popups on UI should use z-indexes above 50 */
@ -40,6 +41,11 @@
/* Transitions */
--transitionStyle: ease-in 0.2s;
--transitionStyleButton: ease-in-out 0.1s;
/* Topbar */
--topbarHeight: 60px;
--topbarHeightDesktop: 72px;
}
/* ================ Custom media queries ================ */
@ -202,6 +208,7 @@ li,
}
}
button,
.buttonFont {
font-family: "sofiapro", Helvetica, Arial, sans-serif;
font-weight: 600; /* SemiBold */
@ -303,3 +310,28 @@ input {
outline: none;
}
}
button {
display: block;
/* Borders */
border: none;
border-radius: var(--borderRadius);
/* Dimensions */
width: 100%;
padding: 13px 0;
/* Colors */
color: var(--matterColorLight);
background-color: var(--marketplaceColor);
transition: all var(--transitionStyleButton);
&:hover,
&:active {
cursor: pointer;
background-color: var(--marketplaceColorDark);
box-shadow: var(--boxShadowButton);
}
}

View file

@ -52,8 +52,9 @@
"EditListingPricingForm.perNight": "per night",
"EditListingPricingForm.priceRequired": "You need to add a valid price.",
"EditListingPricingPanel.title": "How much does it cost?",
"HeroSection.subTitle": "The largest online community to rent music studios",
"HeroSection.title": "Book Studiotime anywhere",
"HeroSection.mobileSearchButtonText": "Search saunas",
"HeroSection.subTitle": "The largest online community to rent saunas in Finland.",
"HeroSection.title": "Book saunas everywhere.",
"InboxPage.fetchFailed": "Could not load all messages. Please try again.",
"InboxPage.noOrdersFound": "You haven't made any bookings.",
"InboxPage.noSalesFound": "Nobody has booked anything from you yet.",
@ -132,8 +133,6 @@
"SalePage.loadingData": "Loading sale data.",
"SalePage.rejectButton": "Reject request",
"SalePage.title": "Sale details for ${title}.",
"SearchForm.placeholder": "Search by location, e.g. New York",
"SearchForm.search": "Search",
"SearchPage.foundResults": "{count, number} {count, plural, one {listing} other {listings}} found.",
"SearchPage.loadingResults": "Loading search results...",
"SearchPage.loadingResults": "Loading search results...",