mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-28 12:43:11 +10:00
LandingPage mobile and desktop styles and components
This commit is contained in:
parent
9ae4fd86c2
commit
9a5552abbc
29 changed files with 440 additions and 307 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
|||
BIN
src/components/HeroSection/background.jpg
Normal file
BIN
src/components/HeroSection/background.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 243 KiB |
|
|
@ -3,4 +3,10 @@
|
|||
width: 100%;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@
|
|||
&:enabled:hover {
|
||||
background-color: transparent;
|
||||
color: var(--matterColorDark);
|
||||
box-shadow: none;
|
||||
}
|
||||
&:enabled:active {
|
||||
background-color: transparent;
|
||||
|
|
|
|||
5
src/components/SearchIcon/SearchIcon.css
Normal file
5
src/components/SearchIcon/SearchIcon.css
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
@import '../../marketplace.css';
|
||||
|
||||
.root {
|
||||
stroke: var(--marketplaceColor);
|
||||
}
|
||||
43
src/components/SearchIcon/SearchIcon.js
Normal file
43
src/components/SearchIcon/SearchIcon.js
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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))>
|
||||
`;
|
||||
|
|
|
|||
18
src/containers/LocationSearchForm/LocationSearchForm.css
Normal file
18
src/containers/LocationSearchForm/LocationSearchForm.css
Normal 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;
|
||||
}
|
||||
66
src/containers/LocationSearchForm/LocationSearchForm.js
Normal file
66
src/containers/LocationSearchForm/LocationSearchForm.js
Normal 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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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',
|
||||
};
|
||||
|
|
@ -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));
|
||||
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
|
|
@ -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>
|
||||
`;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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...",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue