mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-28 20:53:24 +10:00
Merge pull request #489 from sharetribe/map-button-placement
Map button placement in mobile search view
This commit is contained in:
commit
c4ec09f4c8
8 changed files with 136 additions and 163 deletions
|
|
@ -382,6 +382,7 @@ class LocationAutocompleteInput extends Component {
|
|||
placeholder,
|
||||
input,
|
||||
meta,
|
||||
inputRef,
|
||||
} = this.props;
|
||||
const { name, onFocus } = input;
|
||||
const { search, predictions } = currentValue(this.props);
|
||||
|
|
@ -421,6 +422,7 @@ class LocationAutocompleteInput extends Component {
|
|||
onBlur={this.handleOnBlur}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
ref={inputRef}
|
||||
/>
|
||||
{renderPredictions ? (
|
||||
<LocationPredictionsList
|
||||
|
|
@ -448,6 +450,7 @@ LocationAutocompleteInput.defaultProps = {
|
|||
validClassName: null,
|
||||
placeholder: '',
|
||||
meta: null,
|
||||
inputRef: null,
|
||||
};
|
||||
|
||||
LocationAutocompleteInput.propTypes = {
|
||||
|
|
@ -475,6 +478,7 @@ LocationAutocompleteInput.propTypes = {
|
|||
valid: bool.isRequired,
|
||||
touched: bool.isRequired,
|
||||
}),
|
||||
inputRef: func,
|
||||
};
|
||||
|
||||
export default LocationAutocompleteInput;
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
import React, { PropTypes } from 'react'; // eslint-disable-line react/no-deprecated
|
||||
|
||||
const MapIcon = props => {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="27"
|
||||
height="27"
|
||||
viewBox="0 0 27 27"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<filter
|
||||
x="-3.8%"
|
||||
y="-10.4%"
|
||||
width="107.7%"
|
||||
height="129.2%"
|
||||
filterUnits="objectBoundingBox"
|
||||
id="a"
|
||||
>
|
||||
<feOffset dy="2" in="SourceAlpha" result="shadowOffsetOuter1" />
|
||||
<feGaussianBlur stdDeviation="2" in="shadowOffsetOuter1" result="shadowBlurOuter1" />
|
||||
<feColorMatrix
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"
|
||||
in="shadowBlurOuter1"
|
||||
result="shadowMatrixOuter1"
|
||||
/>
|
||||
<feMerge>
|
||||
<feMergeNode in="shadowMatrixOuter1" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<g
|
||||
filter="url(#a)"
|
||||
transform="translate(-41 -12)"
|
||||
strokeWidth="1.4"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M51.667 32L46 27.952V15l5.667 4.048L57.333 15 63 19.048V32l-5.667-4.048zM51.7 19v13M57.3 28V15" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const { string } = PropTypes;
|
||||
|
||||
MapIcon.defaultProps = {
|
||||
className: null,
|
||||
};
|
||||
|
||||
MapIcon.propTypes = {
|
||||
className: string,
|
||||
};
|
||||
|
||||
export default MapIcon;
|
||||
|
|
@ -46,14 +46,51 @@
|
|||
}
|
||||
|
||||
.searchResultSummary {
|
||||
/* Layout */
|
||||
margin: 8px 24px 14px 24px;
|
||||
display: none;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
display: block;
|
||||
margin: 8px 24px 14px 24px;
|
||||
}
|
||||
|
||||
@media (--viewportLarge) {
|
||||
margin: 8px 36px 14px 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.searchResultSummaryMobile {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
background-color: var(--matterColorBright);
|
||||
width: 100vw;
|
||||
padding: 8px 24px 14px 24px;
|
||||
|
||||
/* sticky result summary in mobile */
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 9;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mapIcon {
|
||||
/* background map image */
|
||||
background-image: url(./images/map_icon@2x.png);
|
||||
background-size: cover;
|
||||
|
||||
border-radius: 3px;
|
||||
width: 72px;
|
||||
height: 35px;
|
||||
margin-top: 23px;
|
||||
padding: 9px 22px;
|
||||
|
||||
/* Font */
|
||||
@apply --marketplaceTinyFontStyles;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--failColor);
|
||||
}
|
||||
|
|
@ -78,50 +115,6 @@
|
|||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.openMobileMap {
|
||||
/* Font */
|
||||
@apply --marketplaceH4FontStyles;
|
||||
font-weight: var(--fontWeightSemiBold);
|
||||
|
||||
/* Positioning */
|
||||
/* Positioning from top to place button just above Safari's bottom toolbar
|
||||
/* 120px = button height + toolbar height (68px) */
|
||||
position: fixed;
|
||||
top: calc(100vh - 120px);
|
||||
left: 50vw;
|
||||
width: 175px;
|
||||
transform: translateX(-50%);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
/* Button's layout */
|
||||
padding: 12px 36px;
|
||||
background-color: var(--matterColorLight);
|
||||
border-radius: 30px;
|
||||
border: 0;
|
||||
box-shadow: var(--boxShadow);
|
||||
cursor: pointer;
|
||||
transition: var(--transitionStyleButton);
|
||||
|
||||
& .openMobileMapIcon {
|
||||
stroke: var(--matterColor);
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--marketplaceColor);
|
||||
box-shadow: var(--boxShadowPopupLight);
|
||||
|
||||
& .openMobileMapIcon {
|
||||
stroke: var(--marketplaceColor);
|
||||
}
|
||||
}
|
||||
|
||||
@media (--viewportMedium) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mapPanel {
|
||||
@media (--viewportMedium) {
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import { SearchMap, ModalInMobile, Page, SearchResultsPanel } from '../../compon
|
|||
import { TopbarContainer } from '../../containers';
|
||||
|
||||
import { searchListings, searchMapListings } from './SearchPage.duck';
|
||||
import MapIcon from './MapIcon';
|
||||
import css from './SearchPage.css';
|
||||
|
||||
// Pagination page size might need to be dynamic on responsive page layouts
|
||||
|
|
@ -198,6 +197,12 @@ export class SearchPageComponent extends Component {
|
|||
);
|
||||
const resultsFound = address && !mapSearch ? resultsFoundWithAddress : resultsFoundNoAddress;
|
||||
|
||||
const resultsFoundMobile = (
|
||||
<h2>
|
||||
<FormattedMessage id="SearchPage.foundResultsMobile" values={{ count: totalItems }} />
|
||||
</h2>
|
||||
);
|
||||
|
||||
const noResults = (
|
||||
<h2>
|
||||
<FormattedMessage id="SearchPage.noResults" />
|
||||
|
|
@ -289,6 +294,24 @@ export class SearchPageComponent extends Component {
|
|||
{listingsAreLoaded && totalItems === 0 ? noResults : null}
|
||||
{searchInProgress ? loadingResults : null}
|
||||
</div>
|
||||
<div className={css.searchResultSummaryMobile}>
|
||||
<div>
|
||||
{searchListingsError ? searchError : null}
|
||||
{listingsAreLoaded && totalItems > 0 ? resultsFoundMobile : null}
|
||||
{listingsAreLoaded && totalItems === 0 ? noResults : null}
|
||||
{searchInProgress ? loadingResults : null}
|
||||
</div>
|
||||
<div
|
||||
className={css.mapIcon}
|
||||
onClick={() => {
|
||||
this.useLocationSearchBounds = true;
|
||||
this.modalOpenedBoundsChange = true;
|
||||
this.setState({ isSearchMapOpenOnMobile: true });
|
||||
}}
|
||||
>
|
||||
<FormattedMessage id="SearchPage.openMapView" className={css.mapIconText} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(css.listings, {
|
||||
[css.newSearchInProgress]: !listingsAreLoaded,
|
||||
|
|
@ -299,19 +322,7 @@ export class SearchPageComponent extends Component {
|
|||
listings={listings}
|
||||
pagination={listingsAreLoaded ? pagination : null}
|
||||
search={searchParamsForPagination}
|
||||
>
|
||||
<div
|
||||
className={css.openMobileMap}
|
||||
onClick={() => {
|
||||
this.locationInputSearch = true;
|
||||
this.modalOpenedBoundsChange = true;
|
||||
this.setState({ isSearchMapOpenOnMobile: true });
|
||||
}}
|
||||
>
|
||||
<MapIcon className={css.openMobileMapIcon} />
|
||||
<FormattedMessage id="SearchPage.openMapView" />
|
||||
</div>
|
||||
</SearchResultsPanel>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ModalInMobile
|
||||
|
|
|
|||
|
|
@ -32,6 +32,28 @@ exports[`SearchPageComponent matches snapshot 1`] = `
|
|||
/>
|
||||
</h2>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="SearchPage.foundResultsMobile"
|
||||
values={
|
||||
Object {
|
||||
"count": 22,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
onClick={[Function]}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="SearchPage.openMapView"
|
||||
values={Object {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className=""
|
||||
>
|
||||
|
|
@ -48,19 +70,7 @@ exports[`SearchPageComponent matches snapshot 1`] = `
|
|||
}
|
||||
rootClassName={null}
|
||||
search={Object {}}
|
||||
>
|
||||
<div
|
||||
onClick={[Function]}
|
||||
>
|
||||
<MapIcon
|
||||
className={null}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id="SearchPage.openMapView"
|
||||
values={Object {}}
|
||||
/>
|
||||
</div>
|
||||
</SearchResultsPanel>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<withViewport(ModalInMobileComponent)
|
||||
|
|
|
|||
BIN
src/containers/SearchPage/images/map_icon@2x.png
Normal file
BIN
src/containers/SearchPage/images/map_icon@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -1,4 +1,4 @@
|
|||
import React, { PropTypes } from 'react'; // eslint-disable-line react/no-deprecated
|
||||
import React, { Component, PropTypes } from 'react'; // eslint-disable-line react/no-deprecated
|
||||
import { compose } from 'redux';
|
||||
import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form';
|
||||
import { intlShape, injectIntl } from 'react-intl';
|
||||
|
|
@ -7,43 +7,58 @@ import { Form, LocationAutocompleteInput } from '../../components';
|
|||
|
||||
import css from './TopbarSearchForm.css';
|
||||
|
||||
const TopbarSearchFormComponent = props => {
|
||||
const { rootClassName, className, desktopInputRoot, intl, onSubmit, isMobile } = props;
|
||||
class TopbarSearchFormComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.searchInput = null;
|
||||
}
|
||||
|
||||
const onChange = location => {
|
||||
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 });
|
||||
this.props.onSubmit({ location });
|
||||
// blur search input to hide software keyboard
|
||||
if (this.searchInput) {
|
||||
this.searchInput.blur();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const classes = classNames(rootClassName, className);
|
||||
const desktopInputRootClass = desktopInputRoot || css.desktopInputRoot;
|
||||
render() {
|
||||
const { rootClassName, className, desktopInputRoot, intl, isMobile } = this.props;
|
||||
|
||||
// Allow form submit only when the place has changed
|
||||
const preventFormSubmit = e => e.preventDefault();
|
||||
const classes = classNames(rootClassName, className);
|
||||
const desktopInputRootClass = desktopInputRoot || css.desktopInputRoot;
|
||||
|
||||
return (
|
||||
<Form className={classes} onSubmit={preventFormSubmit}>
|
||||
<Field
|
||||
name="location"
|
||||
label="Location"
|
||||
className={isMobile ? css.mobileInputRoot : desktopInputRootClass}
|
||||
iconClassName={isMobile ? css.mobileIcon : css.desktopIcon}
|
||||
inputClassName={isMobile ? css.mobileInput : css.desktopInput}
|
||||
predictionsClassName={isMobile ? css.mobilePredictions : css.desktopPredictions}
|
||||
placeholder={intl.formatMessage({ id: 'TopbarSearchForm.placeholder' })}
|
||||
format={null}
|
||||
component={LocationAutocompleteInput}
|
||||
closeOnBlur={!isMobile}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
// 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 : desktopInputRootClass}
|
||||
iconClassName={isMobile ? css.mobileIcon : css.desktopIcon}
|
||||
inputClassName={isMobile ? css.mobileInput : css.desktopInput}
|
||||
predictionsClassName={isMobile ? css.mobilePredictions : css.desktopPredictions}
|
||||
placeholder={intl.formatMessage({ id: 'TopbarSearchForm.placeholder' })}
|
||||
format={null}
|
||||
component={LocationAutocompleteInput}
|
||||
closeOnBlur={!isMobile}
|
||||
onChange={this.onChange}
|
||||
inputRef={node => {
|
||||
this.searchInput = node;
|
||||
}}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const { func, string, bool } = PropTypes;
|
||||
|
||||
|
|
|
|||
|
|
@ -448,11 +448,12 @@
|
|||
"SalePage.title": "Sale details: {title}",
|
||||
"SearchMapInfoCard.noImage": "No image",
|
||||
"SearchPage.foundResults": "{count, number} {count, plural, one {sauna} other {saunas}} found",
|
||||
"SearchPage.foundResultsMobile": "{count, number} {count, plural, one {result} other {results}}",
|
||||
"SearchPage.foundResultsWithAddress":
|
||||
"{count, number} {count, plural, one {sauna} other {saunas}} around {address}",
|
||||
"SearchPage.loadingResults": "Loading search results…",
|
||||
"SearchPage.noResults": "Could not find any listings.",
|
||||
"SearchPage.openMapView": "Map view",
|
||||
"SearchPage.openMapView": "Map",
|
||||
"SearchPage.schemaMapSearch": "map search",
|
||||
"SearchPage.schemaDescription": "Showing search results",
|
||||
"SearchPage.schemaTitle": "Search results for {searchAddress} | {siteTitle}",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue