mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-28 03:43:28 +10:00
Merge pull request #198 from sharetribe/input-styles
Location search Input style
This commit is contained in:
commit
679f9203e6
21 changed files with 474 additions and 121 deletions
|
|
@ -1,19 +1,5 @@
|
|||
.root {
|
||||
display: block;
|
||||
|
||||
/* Font */
|
||||
font-size: 16px;
|
||||
letter-spacing: -0.4px;
|
||||
|
||||
/* Border */
|
||||
border: 1px solid #979797;
|
||||
border-radius: 0px; /* Remove default iOS border radius */
|
||||
|
||||
/* Dimensions */
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inline {
|
||||
|
|
|
|||
|
|
@ -1,31 +1,96 @@
|
|||
@import '../../marketplace.css';
|
||||
|
||||
:root {
|
||||
--inputHeight: 50px;
|
||||
--sidePadding: 24px;
|
||||
--sidePaddingDesktop: 36px;
|
||||
--poweredImageHeight: 18px;
|
||||
}
|
||||
|
||||
.root {
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
width: 24px;
|
||||
align-self: stretch;
|
||||
border-bottom: 3px solid var(--marketplaceColor);
|
||||
background-color: var(--matterColorLight);
|
||||
}
|
||||
|
||||
.iconSvg {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.iconSvgGroup {
|
||||
stroke: var(--marketplaceColor);
|
||||
}
|
||||
|
||||
.input {
|
||||
height: 50px;
|
||||
flex-grow: 1;
|
||||
height: var(--inputHeight);
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.predictions {
|
||||
/*
|
||||
Predictions container can be overriden with new container styles for
|
||||
size and position, etc.
|
||||
*/
|
||||
.predictionsRoot {
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
top: 50px;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
border: 1px solid #eee;
|
||||
border-top: none;
|
||||
padding-bottom: 98px;
|
||||
top: var(--inputHeight);
|
||||
left: 0;
|
||||
background-color: var(--marketplaceColor);
|
||||
border-bottom-left-radius: var(--borderRadius);
|
||||
border-bottom-right-radius: var(--borderRadius);
|
||||
box-shadow: var(--boxShadowPopup);
|
||||
z-index: var(--zIndexPopup + 1);
|
||||
}
|
||||
|
||||
/*
|
||||
The Google Maps API TOS requires us to show a Powered by Google logo
|
||||
next to the autocomplete service predictions. It is rendered to the
|
||||
bottom of the container.
|
||||
*/
|
||||
.poweredByGoogle {
|
||||
position: absolute;
|
||||
bottom: 27px;
|
||||
width: 100%;
|
||||
height: var(--poweredImageHeight);
|
||||
background-image: url(./images/powered_by_google_on_non_white_hdpi.png);
|
||||
background-size: auto var(--poweredImageHeight);
|
||||
background-position: center;
|
||||
|
||||
@media (--desktopViewport) {
|
||||
background-position: center left var(--sidePaddingDesktop);
|
||||
}
|
||||
}
|
||||
|
||||
/* List of predictions, with a responsive padding size */
|
||||
.predictions {
|
||||
composes: searchResultsFont from '../../marketplace.css';
|
||||
|
||||
margin: 0;
|
||||
padding: 14px 0;
|
||||
|
||||
& li {
|
||||
padding: 0.5rem 1rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
color: var(--matterColorLight);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
/* Assign enough vertical padding to make the element at least 44px high */
|
||||
padding: 10px var(--sidePadding);
|
||||
|
||||
@media (--desktopViewport) {
|
||||
padding: 10px var(--sidePaddingDesktop);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.highlighted {
|
||||
background-color: #eee;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ const FormComponent = props => {
|
|||
<form onSubmit={handleSubmit}>
|
||||
<label htmlFor="location">Select location:</label>
|
||||
<Field name="location" format={null} component={LocationAutocompleteInput} />
|
||||
<Button type="submit" disabled={pristine || submitting}>Submit</Button>
|
||||
<Button type="submit" style={{ marginTop: '24px' }} disabled={pristine || submitting}>
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import { debounce } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import { Input } from '../../components';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import { getPlacePredictions, getPlaceDetails } from '../../util/googleMaps';
|
||||
|
||||
|
|
@ -15,9 +14,38 @@ const KEY_CODE_TAB = 9;
|
|||
const DIRECTION_UP = 'up';
|
||||
const DIRECTION_DOWN = 'down';
|
||||
|
||||
const Icon = () => (
|
||||
<svg
|
||||
className={css.iconSvg}
|
||||
width="21"
|
||||
height="22"
|
||||
viewBox="0 0 21 22"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
className={css.iconSvgGroup}
|
||||
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>
|
||||
);
|
||||
|
||||
// Renders the autocompletion prediction results in a list
|
||||
const LocationPredictionsList = props => {
|
||||
const { predictions, highlightedIndex, onSelectStart, onSelectEnd } = props;
|
||||
const {
|
||||
rootClassName,
|
||||
className,
|
||||
predictions,
|
||||
highlightedIndex,
|
||||
onSelectStart,
|
||||
onSelectEnd,
|
||||
} = props;
|
||||
if (predictions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -41,18 +69,29 @@ const LocationPredictionsList = props => {
|
|||
};
|
||||
/* eslint-enable jsx-a11y/no-static-element-interactions */
|
||||
|
||||
const classes = classNames(rootClassName || css.predictionsRoot, className);
|
||||
|
||||
return (
|
||||
<ul className={css.predictions}>
|
||||
{predictions.map(item)}
|
||||
</ul>
|
||||
<div className={classes}>
|
||||
<ul className={css.predictions}>
|
||||
{predictions.map(item)}
|
||||
</ul>
|
||||
<div className={css.poweredByGoogle} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const { bool, shape, string, arrayOf, func, any, number } = PropTypes;
|
||||
|
||||
LocationPredictionsList.defaultProps = { highlightedIndex: null };
|
||||
LocationPredictionsList.defaultProps = {
|
||||
rootClassName: null,
|
||||
className: null,
|
||||
highlightedIndex: null,
|
||||
};
|
||||
|
||||
LocationPredictionsList.propTypes = {
|
||||
rootClassName: string,
|
||||
className: string,
|
||||
predictions: arrayOf(
|
||||
shape({
|
||||
id: string.isRequired,
|
||||
|
|
@ -299,7 +338,16 @@ class LocationAutocompleteInput extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { autoFocus, className, placeholder, input } = this.props;
|
||||
const {
|
||||
autoFocus,
|
||||
rootClassName,
|
||||
className,
|
||||
iconClassName,
|
||||
inputClassName,
|
||||
predictionsClassName,
|
||||
placeholder,
|
||||
input,
|
||||
} = this.props;
|
||||
const { name, onFocus } = input;
|
||||
const { search, predictions } = currentValue(this.props);
|
||||
|
||||
|
|
@ -308,17 +356,24 @@ class LocationAutocompleteInput extends Component {
|
|||
onFocus(e);
|
||||
};
|
||||
|
||||
const rootClass = classNames(rootClassName || css.root, className);
|
||||
const iconClass = classNames(css.icon, iconClassName);
|
||||
const inputClass = classNames(css.input, inputClassName);
|
||||
const predictionsClass = classNames(predictionsClassName);
|
||||
|
||||
// Only render predictions when the input has focus. For
|
||||
// development and easier workflow with the browser devtools, you
|
||||
// might want to hardcode this to `true`. Otherwise the dropdown
|
||||
// list will disappear.
|
||||
//
|
||||
const renderPredictions = this.state.inputHasFocus;
|
||||
|
||||
return (
|
||||
<div className={css.root}>
|
||||
<Input
|
||||
className={classNames(css.input, className)}
|
||||
<div className={rootClass}>
|
||||
<div className={iconClass}>
|
||||
<Icon />
|
||||
</div>
|
||||
<input
|
||||
className={inputClass}
|
||||
type="search"
|
||||
autoComplete="off"
|
||||
autoFocus={autoFocus}
|
||||
|
|
@ -332,6 +387,7 @@ class LocationAutocompleteInput extends Component {
|
|||
/>
|
||||
{renderPredictions
|
||||
? <LocationPredictionsList
|
||||
className={predictionsClass}
|
||||
predictions={predictions}
|
||||
highlightedIndex={this.state.highlightedIndex}
|
||||
onSelectStart={this.handlePredictionsSelectStart}
|
||||
|
|
@ -343,11 +399,23 @@ class LocationAutocompleteInput extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
LocationAutocompleteInput.defaultProps = { autoFocus: false, className: '', placeholder: '' };
|
||||
LocationAutocompleteInput.defaultProps = {
|
||||
autoFocus: false,
|
||||
rootClassName: null,
|
||||
className: null,
|
||||
iconClassName: null,
|
||||
inputClassName: null,
|
||||
predictionsClassName: null,
|
||||
placeholder: '',
|
||||
};
|
||||
|
||||
LocationAutocompleteInput.propTypes = {
|
||||
autoFocus: bool,
|
||||
rootClassName: string,
|
||||
className: string,
|
||||
iconClassName: string,
|
||||
inputClassName: string,
|
||||
predictionsClassName: string,
|
||||
placeholder: string,
|
||||
input: shape({
|
||||
name: string.isRequired,
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
|
|
@ -10,7 +10,7 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
align-items: flex-start;
|
||||
padding: 0 36px;
|
||||
|
||||
/* fill */
|
||||
|
|
@ -32,7 +32,6 @@
|
|||
|
||||
/* logo */
|
||||
.logoLink {
|
||||
align-self: flex-start;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -46,10 +45,8 @@
|
|||
}
|
||||
|
||||
/* Search */
|
||||
/* TODO This is placeholder, it needs Search component */
|
||||
.searchLink {
|
||||
min-width: 220px;
|
||||
align-self: flex-start;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
|
@ -60,15 +57,9 @@
|
|||
color: var(--matterColor);
|
||||
}
|
||||
|
||||
/* Spacer */
|
||||
/* Extra space is gathered between search and createListingLink */
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* Create listing (CTA for providers) */
|
||||
.createListingLink {
|
||||
align-self: flex-start;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
|
@ -81,7 +72,6 @@
|
|||
|
||||
/* Inbox */
|
||||
.inboxLink {
|
||||
align-self: flex-start;
|
||||
height: 100%;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
|
@ -94,7 +84,6 @@
|
|||
|
||||
/* Profile menu */
|
||||
.profileMenuLabel {
|
||||
align-self: flex-start;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -112,13 +101,13 @@
|
|||
|
||||
/* Authentication */
|
||||
.signupLink {
|
||||
align-self: flex-start;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.loginLink {
|
||||
align-self: flex-start;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable no-console */
|
||||
import { fakeIntl } from '../../util/test-data';
|
||||
import TopbarDesktop from './TopbarDesktop';
|
||||
|
||||
|
|
@ -9,6 +10,9 @@ export const AuthenticatedDesktopTopbar = {
|
|||
isAuthenticated: true,
|
||||
currentUserHasListings: true,
|
||||
name: 'John Doe',
|
||||
onSearchSubmit: values => {
|
||||
console.log('submit search:', values);
|
||||
},
|
||||
intl: fakeIntl,
|
||||
onLogout: noop,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
|
||||
import { FormattedMessage, intlShape } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
Avatar,
|
||||
|
|
@ -10,6 +10,7 @@ import {
|
|||
MenuItem,
|
||||
NamedLink,
|
||||
} from '../../components';
|
||||
import { TopbarSearchForm } from '../../containers';
|
||||
|
||||
import logo from './images/saunatime-logo.png';
|
||||
import css from './TopbarDesktop.css';
|
||||
|
|
@ -24,16 +25,20 @@ const TopbarDesktop = props => {
|
|||
intl,
|
||||
isAuthenticated,
|
||||
onLogout,
|
||||
onSearchSubmit,
|
||||
initialSearchFormValues,
|
||||
} = props;
|
||||
|
||||
const rootClass = rootClassName || css.root;
|
||||
const classes = classNames(rootClass, className);
|
||||
|
||||
// TODO This is just a placeholder
|
||||
const search = (
|
||||
<div className={css.searchLink}>
|
||||
<span className={css.search}>Search should be here</span>
|
||||
</div>
|
||||
<TopbarSearchForm
|
||||
className={css.searchLink}
|
||||
form="TopbarSearchFormDesktop"
|
||||
onSubmit={onSearchSubmit}
|
||||
initialValues={initialSearchFormValues}
|
||||
/>
|
||||
);
|
||||
|
||||
const inboxLink = isAuthenticated
|
||||
|
|
@ -83,7 +88,6 @@ const TopbarDesktop = props => {
|
|||
/>
|
||||
</NamedLink>
|
||||
{search}
|
||||
<div className={css.spacer} />
|
||||
<NamedLink className={css.createListingLink} name="NewListingPage">
|
||||
<span className={css.createListing}>
|
||||
<FormattedMessage id="TopbarDesktop.createListing" />
|
||||
|
|
@ -97,9 +101,15 @@ const TopbarDesktop = props => {
|
|||
);
|
||||
};
|
||||
|
||||
TopbarDesktop.defaultProps = { className: null, firstName: '', lastName: '', rootClassName: '' };
|
||||
const { bool, func, string, object } = PropTypes;
|
||||
|
||||
const { bool, func, string } = PropTypes;
|
||||
TopbarDesktop.defaultProps = {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
className: null,
|
||||
rootClassName: null,
|
||||
initialSearchFormValues: {},
|
||||
};
|
||||
|
||||
TopbarDesktop.propTypes = {
|
||||
className: string,
|
||||
|
|
@ -109,9 +119,9 @@ TopbarDesktop.propTypes = {
|
|||
firstName: string,
|
||||
lastName: string,
|
||||
rootClassName: string,
|
||||
|
||||
// from injectIntl
|
||||
onSearchSubmit: func.isRequired,
|
||||
initialSearchFormValues: object,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(TopbarDesktop);
|
||||
export default TopbarDesktop;
|
||||
|
|
|
|||
|
|
@ -10,18 +10,22 @@ const noop = () => null;
|
|||
|
||||
describe('TopbarDesktop', () => {
|
||||
it('data matches snapshot', () => {
|
||||
window.google = { maps: {} };
|
||||
const flattenedRoutes = flattenRoutes(routesConfiguration);
|
||||
const topbarProps = {
|
||||
isAuthenticated: true,
|
||||
currentUserHasListings: true,
|
||||
name: 'John Doe',
|
||||
onSearchSubmit: noop,
|
||||
intl: fakeIntl,
|
||||
onLogout: noop,
|
||||
};
|
||||
const tree = renderDeep(
|
||||
<RoutesProvider flattenedRoutes={flattenedRoutes}>
|
||||
<TopbarDesktop
|
||||
isAuthenticated
|
||||
currentUserHasListings
|
||||
name="John Doe"
|
||||
intl={fakeIntl}
|
||||
onLogout={noop}
|
||||
/>
|
||||
<TopbarDesktop {...topbarProps} />
|
||||
</RoutesProvider>
|
||||
);
|
||||
delete window.google;
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,19 +7,54 @@ exports[`TopbarDesktop data matches snapshot 1`] = `
|
|||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
<img
|
||||
alt="Logo"
|
||||
alt="TopbarDesktop.logo"
|
||||
className={undefined}
|
||||
src="saunatime-logo.png" />
|
||||
</a>
|
||||
<div
|
||||
className={undefined}>
|
||||
<span
|
||||
className={undefined}>
|
||||
Search should be here
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={undefined} />
|
||||
<form
|
||||
className=""
|
||||
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 saunas..."
|
||||
type="search"
|
||||
value="" />
|
||||
</div>
|
||||
</form>
|
||||
<a
|
||||
className=""
|
||||
href="/l/new"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
font-family: inherit;
|
||||
|
||||
/* Border */
|
||||
border: 1px solid #979797;
|
||||
border-radius: 0px; /* Remove default iOS border radius */
|
||||
|
||||
/* Dimensions */
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const SearchForm = props => {
|
|||
return (
|
||||
<form {...addClassName} onSubmit={handleSubmit}>
|
||||
<Field
|
||||
className={css.locationInput}
|
||||
inputClassName={css.locationInput}
|
||||
name="location"
|
||||
label="Location"
|
||||
placeholder={intl.formatMessage({ id: 'SearchForm.placeholder' })}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,32 @@ exports[`SearchForm matches snapshot 1`] = `
|
|||
<form
|
||||
onSubmit={[Function]}>
|
||||
<div
|
||||
className={undefined}>
|
||||
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}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@media (--desktopViewport) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
.withPadding {
|
||||
padding: calc(2 * var(--spacingUnit));
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@media (--desktopViewport) {
|
||||
padding: calc(2 * var(--spacingUnitDesktop));
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
.defaultWrapperStyles {
|
||||
margin: calc(2 * var(--spacingUnit)) 0;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@media (--desktopViewport) {
|
||||
margin: calc(2 * var(--spacingUnitDesktop)) 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
flex-grow: 1;
|
||||
composes: withPadding;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@media (--desktopViewport) {
|
||||
padding: 56px 0 0 16px;
|
||||
}
|
||||
}
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
background-image: url('data:image/svg+xml;utf8,<svg width="10" height="24" viewBox="0 0 10 24" xmlns="http://www.w3.org/2000/svg"><path d="M0 5h10M0 11h10M0 17h10" stroke="#f1f1f1"/><path d="M0 23h10" stroke="#ddd" stroke-width="1"/></svg>');
|
||||
background-repeat: repeat;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@media (--desktopViewport) {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg width="10" height="24" viewBox="0 0 10 24" xmlns="http://www.w3.org/2000/svg"><path d="M0 7h10M0 15h10M0" stroke="#f1f1f1"/><path d="M0 23h10" stroke="#ddd" stroke-width="1"/></svg>');
|
||||
background-repeat: repeat;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
/* shadows */
|
||||
box-shadow: 0 1px 1px 0 #dcdcdc;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@media (--desktopViewport) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -44,14 +44,10 @@
|
|||
width: 44px;
|
||||
}
|
||||
|
||||
.searchForm {
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
.desktop {
|
||||
display: none;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@media (--desktopViewport) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
|
@ -61,3 +57,21 @@
|
|||
.rootSearchIcon {
|
||||
stroke: var(--matterColor);
|
||||
}
|
||||
|
||||
.searchContainer {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mobileHelp {
|
||||
color: var(--matterColorAnti);
|
||||
margin: 0 24px;
|
||||
|
||||
/* Absolute position to avoid affecting the layout of the autocomplete
|
||||
predictions list */
|
||||
position: absolute;
|
||||
top: 115px;
|
||||
|
||||
/* Pull text behind search suggestions */
|
||||
z-index: -1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import { compose } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { intlShape, injectIntl } from 'react-intl';
|
||||
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
|
||||
import { pickBy } from 'lodash';
|
||||
import { logout, authenticationInProgress } from '../../ducks/Auth.duck';
|
||||
import { FlatButton, Modal, NamedLink, TopbarDesktop, TopbarMobileMenu } from '../../components';
|
||||
import { SearchForm } from '../../containers';
|
||||
import { TopbarSearchForm } from '../../containers';
|
||||
import { withFlattenedRoutes } from '../../util/contextHelpers';
|
||||
import { parse, stringify } from '../../util/urlHelpers';
|
||||
import { ensureUser } from '../../util/data';
|
||||
|
|
@ -63,10 +63,10 @@ class TopbarComponent extends Component {
|
|||
}
|
||||
|
||||
handleSubmit(values) {
|
||||
const selectedPlace = values && values.location ? values.location.selectedPlace : null;
|
||||
const { search, selectedPlace } = values.location;
|
||||
const { flattenedRoutes, history } = this.props;
|
||||
const { address, origin, bounds, country } = selectedPlace || {};
|
||||
const searchParams = { address, origin, bounds, country };
|
||||
const { origin, bounds, country } = selectedPlace;
|
||||
const searchParams = { address: search, origin, bounds, country };
|
||||
history.push(createResourceLocatorString('SearchPage', flattenedRoutes, {}, searchParams));
|
||||
}
|
||||
|
||||
|
|
@ -142,6 +142,8 @@ class TopbarComponent extends Component {
|
|||
onLogout={this.handleLogout}
|
||||
firstName={profile.firstName}
|
||||
lastName={profile.lastName}
|
||||
onSearchSubmit={this.handleSubmit}
|
||||
initialSearchFormValues={initialSearchFormValues}
|
||||
/>
|
||||
</div>
|
||||
<Modal
|
||||
|
|
@ -156,12 +158,17 @@ class TopbarComponent extends Component {
|
|||
onClose={this.handleMobileSearchClose}
|
||||
togglePageClassNames={togglePageClassNames}
|
||||
>
|
||||
<SearchForm
|
||||
form="TopbarSearchForm"
|
||||
className={css.searchForm}
|
||||
onSubmit={this.handleSubmit}
|
||||
initialValues={initialSearchFormValues}
|
||||
/>
|
||||
<div className={css.searchContainer}>
|
||||
<TopbarSearchForm
|
||||
form="TopbarSearchForm"
|
||||
onSubmit={this.handleSubmit}
|
||||
initialValues={initialSearchFormValues}
|
||||
isMobile
|
||||
/>
|
||||
<p className={css.mobileHelp}>
|
||||
<FormattedMessage id="Topbar.mobileSearchHelp" />
|
||||
</p>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
39
src/containers/TopbarSearchForm/TopbarSearchForm.css
Normal file
39
src/containers/TopbarSearchForm/TopbarSearchForm.css
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
@import '../../marketplace.css';
|
||||
|
||||
:root {
|
||||
--inputHeight: 50px;
|
||||
--topbarHeight: 72px;
|
||||
}
|
||||
|
||||
.mobileInputRoot {
|
||||
margin-left: 24px;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.desktopInputRoot {
|
||||
height: var(--topbarHeight);
|
||||
}
|
||||
|
||||
.desktopIcon {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.desktopInput {
|
||||
height: var(--topbarHeight);
|
||||
border: none;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.mobilePredictions {
|
||||
position: fixed;
|
||||
top: 113px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.desktopPredictions {
|
||||
margin-top: calc(var(--topbarHeight) - var(--inputHeight));
|
||||
max-width: 434px;
|
||||
}
|
||||
68
src/containers/TopbarSearchForm/TopbarSearchForm.js
Normal file
68
src/containers/TopbarSearchForm/TopbarSearchForm.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
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 './TopbarSearchForm.css';
|
||||
|
||||
const TopbarSearchFormComponent = props => {
|
||||
const { rootClassName, className, intl, onSubmit, isMobile } = 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, 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"
|
||||
className={isMobile ? css.mobileInputRoot : css.desktopInputRoot}
|
||||
iconClassName={isMobile ? null : css.desktopIcon}
|
||||
inputClassName={isMobile ? null : css.desktopInput}
|
||||
predictionsClassName={isMobile ? css.mobilePredictions : css.desktopPredictions}
|
||||
placeholder={intl.formatMessage({ id: 'TopbarSearchForm.placeholder' })}
|
||||
format={null}
|
||||
component={LocationAutocompleteInput}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const { func, string, bool } = PropTypes;
|
||||
|
||||
TopbarSearchFormComponent.defaultProps = { rootClassName: null, className: null, isMobile: false };
|
||||
|
||||
TopbarSearchFormComponent.propTypes = {
|
||||
...formPropTypes,
|
||||
|
||||
rootClassName: string,
|
||||
className: string,
|
||||
onSubmit: func.isRequired,
|
||||
isMobile: bool,
|
||||
|
||||
// from injectIntl
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
const defaultFormName = 'TopbarSearchForm';
|
||||
|
||||
const TopbarSearchForm = compose(reduxForm({ form: defaultFormName }), injectIntl)(
|
||||
TopbarSearchFormComponent
|
||||
);
|
||||
|
||||
export default TopbarSearchForm;
|
||||
|
|
@ -31,6 +31,7 @@ import SignupForm from './SignupForm/SignupForm';
|
|||
import StripePaymentForm from './StripePaymentForm/StripePaymentForm';
|
||||
import StyleguidePage from './StyleguidePage/StyleguidePage';
|
||||
import Topbar from './Topbar/Topbar';
|
||||
import TopbarSearchForm from './TopbarSearchForm/TopbarSearchForm';
|
||||
|
||||
export {
|
||||
AuthenticationPage,
|
||||
|
|
@ -66,4 +67,5 @@ export {
|
|||
StripePaymentForm,
|
||||
StyleguidePage,
|
||||
Topbar,
|
||||
TopbarSearchForm,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
@import "sanitize.css";
|
||||
|
||||
/* ================ Custom properties aka variables ================ */
|
||||
|
||||
:root {
|
||||
/* Colors */
|
||||
--marketplaceColor: #C0392B;
|
||||
|
|
@ -37,7 +39,12 @@
|
|||
--borderRadius: 2px;
|
||||
}
|
||||
|
||||
/* FONTS */
|
||||
/* ================ Custom media queries ================ */
|
||||
|
||||
@custom-media --desktopViewport (min-width: 768px);
|
||||
|
||||
/* ================ Fonts ================ */
|
||||
|
||||
h1, h2, h3, h4, h5, h6, p, pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
@ -52,7 +59,7 @@ h1,
|
|||
letter-spacing: -1px;
|
||||
|
||||
/* TODO: this seems to be biggest font in Page titles (except in Hero) - is it h1 then? */
|
||||
@media (min-width: 768px) {
|
||||
@media (--desktopViewport) {
|
||||
font-family: "sofiapro";
|
||||
font-weight: 600;
|
||||
font-size: 48px;
|
||||
|
|
@ -72,7 +79,7 @@ h2,
|
|||
letter-spacing: -0.5px;
|
||||
|
||||
/* TODO Are these correct settings? */
|
||||
@media (min-width: 768px) {
|
||||
@media (--desktopViewport) {
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
padding: 4px 0 4px 0; /* 4px + 4px = 8px */
|
||||
|
|
@ -133,6 +140,7 @@ input,
|
|||
button,
|
||||
p,
|
||||
pre,
|
||||
li,
|
||||
.bodyFont {
|
||||
font-family: "sofiapro", Helvetica, Arial, sans-serif;
|
||||
font-weight: 500;
|
||||
|
|
@ -142,7 +150,7 @@ pre,
|
|||
letter-spacing: -0.1px;
|
||||
|
||||
/* TODO Desktop styles needs to be extracted */
|
||||
@media (min-width: 768px) {
|
||||
@media (--desktopViewport) {
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
|
|
@ -191,13 +199,17 @@ pre,
|
|||
letter-spacing: 0.1px;
|
||||
}
|
||||
|
||||
.searchResultsFont {
|
||||
composes: bodyFont;
|
||||
}
|
||||
|
||||
/* Base font color */
|
||||
html,
|
||||
.textColor {
|
||||
color: var(--matterColor);
|
||||
}
|
||||
|
||||
/* NORMALIZATIONS for other elements */
|
||||
/* ================ Normalisations ================ */
|
||||
/* TODO */
|
||||
|
||||
html {
|
||||
|
|
@ -231,5 +243,25 @@ select {
|
|||
|
||||
textarea {
|
||||
width: 100%;
|
||||
border: 1px solid #ddd;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--marketplaceColor);
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
border: none;
|
||||
border-bottom-width: 3px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: var(--marketplaceColor);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--matterColorAnti);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,19 +39,19 @@
|
|||
"EditListingDescriptionForm.titleRequired": "You need to add a name.",
|
||||
"EditListingDescriptionPanel.title": "Add your sauna",
|
||||
"EditListingLocationForm.address": "Address",
|
||||
"EditListingLocationForm.addressRequired": "You need to provide a location",
|
||||
"EditListingLocationForm.addressNotRecognized": "We didn't recognize this location. Please try another location.",
|
||||
"EditListingLocationForm.addressRequired": "You need to provide a location",
|
||||
"EditListingLocationForm.building": "Apt, suite, building # (optional)",
|
||||
"EditListingLocationForm.buildingPlaceholder": "E.g. A 42",
|
||||
"EditListingLocationPanel.title": "Where's your sauna located?",
|
||||
"EditListingPhotosForm.imageRequired": "You need to add at least one image.",
|
||||
"EditListingPage.titleCreateListing": "Create a listing",
|
||||
"EditListingPage.titleEditListing": "Edit listing",
|
||||
"EditListingPhotosForm.bankAccountNumberRequired": "You need to add a bank account number.",
|
||||
"EditListingPhotosForm.imageRequired": "You need to add at least one image.",
|
||||
"EditListingPhotosPanel.title": "Add photos",
|
||||
"EditListingPricingForm.perNight": "per night",
|
||||
"EditListingPricingForm.priceRequired": "You need to add a valid price.",
|
||||
"EditListingPricingPanel.title": "How much does it cost?",
|
||||
"EditListingPage.titleCreateListing": "Create a listing",
|
||||
"EditListingPage.titleEditListing": "Edit listing",
|
||||
"HeroSection.subTitle": "The largest online community to rent music studios",
|
||||
"HeroSection.title": "Book Studiotime anywhere",
|
||||
"InboxPage.fetchFailed": "Could not load all messages. Please try again.",
|
||||
|
|
@ -154,11 +154,11 @@
|
|||
"SignupForm.termsOfService": "By confirming I accept the terms and conditions and the {stripeConnectedAccountAgreementLink}",
|
||||
"StripeBankAccountToken.accountNumberPlaceholder": " ",
|
||||
"StripeBankAccountToken.bankAccountNumberLabel": "Bank account number:",
|
||||
"StripeBankAccountToken.routingNumberLabel": "Routing number:",
|
||||
"StripeBankAccountToken.bankAccountNumberLabelIban": "Bank account number (IBAN):",
|
||||
"StripeBankAccountToken.createBankAccountTokenError": "Could not connect account number. Please double-check that your routing number and account number are valid in {country} when using {currency}",
|
||||
"StripeBankAccountToken.createBankAccountTokenErrorIban": "Could not connect account number. Please double-check that your account number is valid in {country} when using {currency}",
|
||||
"StripeBankAccountToken.invalidRoutingNumber": "Invalid routing number {number} for country {country}",
|
||||
"StripeBankAccountToken.routingNumberLabel": "Routing number:",
|
||||
"StripeBankAccountToken.routingNumberPlaceholder": " ",
|
||||
"StripeBankAccountToken.unsupportedCountry": "Country not supported: {country}",
|
||||
"StripePaymentForm.genericError": "Could not handle payment data. Please try again.",
|
||||
|
|
@ -185,12 +185,13 @@
|
|||
"StripePaymentForm.submitPaymentInfo": "Send request",
|
||||
"Topbar.logoIcon": "Go to homepage",
|
||||
"Topbar.menuIcon": "Open menu",
|
||||
"Topbar.mobileSearchHelp": "Tip: You can also search saunas by zip code, for example \"00500\" or city district – \"Sörnäinen\".",
|
||||
"Topbar.searchIcon": "Open search",
|
||||
"TopbarDesktop.createListing": "+ Add your sauna",
|
||||
"TopbarDesktop.inbox": "Inbox",
|
||||
"TopbarDesktop.login": "Log in",
|
||||
"TopbarDesktop.logout": "Log out",
|
||||
"TopbarDesktop.logo": "Logo",
|
||||
"TopbarDesktop.logout": "Log out",
|
||||
"TopbarDesktop.signup": "Sign up",
|
||||
"TopbarMobileMenu.greeting": "Hello {firstName}",
|
||||
"TopbarMobileMenu.inboxLink": "Inbox",
|
||||
|
|
@ -199,5 +200,8 @@
|
|||
"TopbarMobileMenu.newListingLink": "+ Add your sauna",
|
||||
"TopbarMobileMenu.signupLink": "Sign up",
|
||||
"TopbarMobileMenu.signupOrLogin": "{signup} or {login}",
|
||||
"TopbarMobileMenu.unauthorizedGreeting": "Hello there,{lineBreak}would you like to {signupOrLogin}?"
|
||||
"TopbarMobileMenu.unauthorizedGreeting": "Hello there,{lineBreak}would you like to {signupOrLogin}?",
|
||||
"TopbarSearchForm.placeholder": "Search saunas...",
|
||||
"TopbarSearchForm.placeholder": "Search saunas...",
|
||||
"TopbarSearchForm.searchHelp": "Tip: You can also search saunas by zip code, for example \"00500\" or city district \"Sörnäinen\"."
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue