Merge pull request #795 from sharetribe/refactor-searchpage

Refactor searchpage
This commit is contained in:
Vesa Luusua 2018-04-06 12:07:22 +03:00 committed by GitHub
commit ea6ab42429
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 448 additions and 328 deletions

View file

@ -41,10 +41,6 @@ import { createResourceLocatorString } from '../../util/routes';
import { InlineTextButton } from '../../components';
import css from './SearchFiltersPanel.css';
// Create constants from url params and uset them in FILTERS array and while adding actual filters
// e.g. const MULTI_SELECT_URL_PARAM = 'pub_filterX';
const FILTERS = [];
class SearchFiltersPanelComponent extends Component {
constructor(props) {
super(props);
@ -82,9 +78,9 @@ class SearchFiltersPanelComponent extends Component {
// Reset all filter query parameters
resetAll(e) {
const { urlQueryParams, history, onClosePanel } = this.props;
const { urlQueryParams, customURLParamToConfig, history, onClosePanel } = this.props;
const queryParams = omit(urlQueryParams, FILTERS);
const queryParams = omit(urlQueryParams, Object.keys(customURLParamToConfig));
history.push(createResourceLocatorString('SearchPage', routeConfiguration(), {}, queryParams));
// Ensure that panel closes (if now changes have been made)
@ -161,6 +157,7 @@ SearchFiltersPanelComponent.propTypes = {
rootClassName: string,
className: string,
urlQueryParams: object.isRequired,
customURLParamToConfig: object.isRequired,
onClosePanel: func.isRequired,
// from injectIntl

View file

@ -0,0 +1,157 @@
import React, { Component } from 'react';
import { array, bool, func, object, number, string } from 'prop-types';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { propTypes } from '../../util/types';
import {
SearchResultsPanel,
SearchFilters,
SearchFiltersMobile,
SearchFiltersPanel,
} from '../../components';
import css from './SearchPage.css';
class MainPanel extends Component {
constructor(props) {
super(props);
this.state = { isSearchFiltersPanelOpen: false };
}
render() {
const {
className,
rootClassName,
urlQueryParams,
listings,
searchInProgress,
searchListingsError,
searchParamsAreInSync,
onActivateListing,
onManageDisableScrolling,
onMapIconClick,
pagination,
searchParamsForPagination,
showAsModalMaxWidth,
customURLParamToConfig,
primaryFilters,
secondaryFilters,
} = this.props;
const isSearchFiltersPanelOpen = !!secondaryFilters && this.state.isSearchFiltersPanelOpen;
const searchFiltersPanelSelectedCount = !secondaryFilters
? 0
: Object.keys(customURLParamToConfig)
.map(key => urlQueryParams[key])
.filter(param => !!param).length;
const searchFiltersPanelProps = !!secondaryFilters
? {
isSearchFiltersPanelOpen: this.state.isSearchFiltersPanelOpen,
toggleSearchFiltersPanel: isOpen => {
this.setState({ isSearchFiltersPanelOpen: isOpen });
},
searchFiltersPanelSelectedCount,
}
: {};
const hasPaginationInfo = !!pagination && pagination.totalItems != null;
const totalItems = searchParamsAreInSync && hasPaginationInfo ? pagination.totalItems : 0;
const listingsAreLoaded = !searchInProgress && searchParamsAreInSync && hasPaginationInfo;
const classes = classNames(rootClassName || css.searchResultContainer, className);
return (
<div className={classes}>
<SearchFilters
className={css.searchFilters}
urlQueryParams={urlQueryParams}
listingsAreLoaded={listingsAreLoaded}
resultsCount={totalItems}
searchInProgress={searchInProgress}
searchListingsError={searchListingsError}
onManageDisableScrolling={onManageDisableScrolling}
{...searchFiltersPanelProps}
{...primaryFilters}
/>
<SearchFiltersMobile
className={css.searchFiltersMobile}
urlQueryParams={urlQueryParams}
listingsAreLoaded={listingsAreLoaded}
resultsCount={totalItems}
searchInProgress={searchInProgress}
searchListingsError={searchListingsError}
showAsModalMaxWidth={showAsModalMaxWidth}
onMapIconClick={onMapIconClick}
onManageDisableScrolling={onManageDisableScrolling}
onOpenModal={this.onOpenMobileModal}
onCloseModal={this.onCloseMobileModal}
{...primaryFilters}
/>
{isSearchFiltersPanelOpen ? (
<div className={classNames(css.searchFiltersPanel)}>
<SearchFiltersPanel
urlQueryParams={urlQueryParams}
customURLParamToConfig={customURLParamToConfig}
listingsAreLoaded={listingsAreLoaded}
onClosePanel={() => this.setState({ isSearchFiltersPanelOpen: false })}
{...secondaryFilters}
/>
</div>
) : (
<div
className={classNames(css.listings, {
[css.newSearchInProgress]: !listingsAreLoaded,
})}
>
{searchListingsError ? (
<h2 className={css.error}>
<FormattedMessage id="SearchPage.searchError" />
</h2>
) : null}
<SearchResultsPanel
className={css.searchListingsPanel}
listings={listings}
pagination={listingsAreLoaded ? pagination : null}
search={searchParamsForPagination}
setActiveListing={onActivateListing}
/>
</div>
)}
</div>
);
}
}
MainPanel.defaultProps = {
className: null,
rootClassName: null,
listings: [],
resultsCount: 0,
pagination: null,
searchParamsForPagination: {},
primaryFilters: null,
secondaryFilters: null,
};
MainPanel.propTypes = {
className: string,
rootClassName: string,
urlQueryParams: object.isRequired,
listings: array,
searchInProgress: bool.isRequired,
searchListingsError: propTypes.error,
searchParamsAreInSync: bool.isRequired,
onActivateListing: func.isRequired,
onManageDisableScrolling: func.isRequired,
onMapIconClick: func.isRequired,
pagination: propTypes.pagination,
searchParamsForPagination: object,
showAsModalMaxWidth: number.isRequired,
customURLParamToConfig: object.isRequired,
primaryFilters: object,
secondaryFilters: object,
};
export default MainPanel;

View file

@ -0,0 +1,71 @@
# SearchPage
SearchPage component has roughly 3 sections inside its layout:
Topbar, MainPanel (for results and filters), and Map
So, rough JSX presentation is something like:
```jsx
<Page>
<TopbarContainer />
<MainPanel />
<SearchMap />
<Page>
```
Searches can be made by each of these components.
* Topbar contains location search (LocationAutocompleteInput)
* MainPanel has Filters that can fine-tune current location search
* SearchMap can create new location searches when the map's bounding box changes
(i.e. moving, zooming, etc.)
## Topbar
In contrast to other pages, Topbar gets `currentSearchParams` among other props. This makes it
possible for Topbar to take current filters into account.
## MainPanel
MainPanel has two functions: showing searchResults and showing filters. There are two sets of
filters that can be passed to it: `primaryFilters`, `secondaryFilters`.
Primary filters are filters that are shown always on top of SearchResults as dropdown-selections.
We recommend that only 1 - 3 primary filters are passed in since they start to take too much space
on narrow screens.
Secondary filters create one more button to the space containing primary filters: *More filters*.
This more-filters button opens up a SearchFiltersPanel component that can be changed to show those
extra filters passed to it.
**Note:** Currently, it doesn't contain any filter components by default, even if you pass in some
filter data. Creating those filter components is part of the customization process.
On the mobile layout, all the filters are shown in separate mobile filters panel.
## SearchMap
SearchMap listens to 'idle' event and SearchPage function `onIndle` can create a new location search if
SearchMap's bounds have changed enough.
## Other things to consider
### URL Params vs marketplace-custom-config.js
Filter handling needs some mapping between configured extended data. For example,
*marketplace-custom-config.js* defines all the categories used by the app, but that data needs to be in
format `pub_category=selectedCategory` when we talk with the API. (We also use this format in URL.)
This mapping is done at the beginning of the file:
```
const CATEGORY_URL_PARAM = 'pub_category';
const AMENITIES_URL_PARAM = 'pub_amenities';
const customURLParamToConfig = {
[CATEGORY_URL_PARAM]: 'categories',
[AMENITIES_URL_PARAM]: 'amenities',
};
```
### SeachPage schema / SEO
Schema is created inside `createSearchResultSchema` in *SearchPage.helpers.js*. It needs listings
and address to make meaningful JSON-LD presentation for search engines.

View file

@ -0,0 +1,98 @@
import { intersection } from 'lodash';
import config from '../../config';
import { createResourceLocatorString } from '../../util/routes';
import { createSlug } from '../../util/urlHelpers';
import routeConfiguration from '../../routeConfiguration';
// customURLParams
export const validURLParamForExtendedData = (paramKey, urlParams, customConfigKeys) => {
const configKey = customConfigKeys[paramKey];
const value = urlParams[paramKey];
const valueArray = value ? value.split(',') : [];
if (configKey && valueArray.length > 0) {
const allowedValues = config.custom[configKey].map(a => a.key);
const validValues = intersection(valueArray, allowedValues).join(',');
return validValues.length > 0 ? { [paramKey]: validValues } : {};
}
return {};
};
// validate filter params
export const validURLParamsForExtendedData = (params, customURLParams, customURLParamToConfig) => {
const paramKeys = Object.keys(params);
return paramKeys.reduce((validParams, paramKey) => {
return customURLParams.includes(paramKey)
? {
...validParams,
...validURLParamForExtendedData(paramKey, params, customURLParamToConfig),
}
: { ...validParams, [paramKey]: params[paramKey] };
}, {});
};
// extract search parameters, including a custom URL params
// which are validated by mapping the values to marketplace custom config.
export const pickSearchParamsOnly = (params, customURLParams, customURLParamToConfig) => {
const { address, origin, bounds, country, ...rest } = params || {};
const boundsMaybe = bounds ? { bounds } : {};
const originMaybe = config.sortSearchByDistance && origin ? { origin } : {};
const customSearchParamKeys = Object.keys(rest);
const customSearchParams = customSearchParamKeys.reduce((validParams, paramKey) => {
return customURLParams.includes(paramKey)
? { ...validParams, ...validURLParamForExtendedData(paramKey, rest, customURLParamToConfig) }
: { ...validParams };
}, {});
return {
...boundsMaybe,
...originMaybe,
...customSearchParams,
};
};
export const createSearchResultSchema = (listings, address, intl) => {
// Schema for search engines (helps them to understand what this page is about)
// http://schema.org
// We are using JSON-LD format
const siteTitle = config.siteTitle;
const searchAddress = address || intl.formatMessage({ id: 'SearchPage.schemaMapSearch' });
const schemaDescription = intl.formatMessage({ id: 'SearchPage.schemaDescription' });
const schemaTitle = intl.formatMessage(
{ id: 'SearchPage.schemaTitle' },
{ searchAddress, siteTitle }
);
const schemaListings = listings.map((l, i) => {
const title = l.attributes.title;
const pathToItem = createResourceLocatorString('ListingPage', routeConfiguration(), {
id: l.id.uuid,
slug: createSlug(title),
});
return {
'@type': 'ListItem',
position: i,
url: `${config.canonicalRootURL}${pathToItem}`,
name: title,
};
});
const schemaMainEntity = JSON.stringify({
'@type': 'ItemList',
name: searchAddress,
itemListOrder: 'http://schema.org/ItemListOrderAscending',
itemListElement: schemaListings,
});
return {
title: schemaTitle,
description: schemaDescription,
schema: {
'@context': 'http://schema.org',
'@type': 'SearchResultsPage',
description: schemaDescription,
name: schemaTitle,
mainEntity: [schemaMainEntity],
},
};
};

View file

@ -1,10 +1,10 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { injectIntl, intlShape } from 'react-intl';
import { connect } from 'react-redux';
import { compose } from 'redux';
import { withRouter } from 'react-router-dom';
import { debounce, intersection, isEqual, unionWith } from 'lodash';
import { debounce, isEqual, unionWith } from 'lodash';
import classNames from 'classnames';
import config from '../../config';
import routeConfiguration from '../../routeConfiguration';
@ -15,85 +15,37 @@ import {
hasSameSDKBounds,
} from '../../util/googleMaps';
import { createResourceLocatorString } from '../../util/routes';
import { createSlug, parse, stringify } from '../../util/urlHelpers';
import { parse, stringify } from '../../util/urlHelpers';
import { propTypes } from '../../util/types';
import { getListingsById } from '../../ducks/marketplaceData.duck';
import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck';
import {
SearchMap,
ModalInMobile,
Page,
SearchResultsPanel,
SearchFilters,
SearchFiltersMobile,
SearchFiltersPanel,
} from '../../components';
import { SearchMap, ModalInMobile, Page } from '../../components';
import { TopbarContainer } from '../../containers';
import { searchListings, searchMapListings, setActiveListing } from './SearchPage.duck';
import { searchListings, searchMapListings, setActiveListing } from './SearchPage.duck';
import {
pickSearchParamsOnly,
validURLParamForExtendedData,
validURLParamsForExtendedData,
createSearchResultSchema,
} from './SearchPage.helpers';
import MainPanel from './MainPanel';
import css from './SearchPage.css';
// Pagination page size might need to be dynamic on responsive page layouts
// Current design has max 3 columns 12 is divisible by 2 and 3
// So, there's enough cards to fill all columns on full pagination pages
const RESULT_PAGE_SIZE = 24;
const MAX_SEARCH_RESULT_PAGE_SIZE_ON_MAP = 80; // max page size is 100 in API
const MAX_SEARCH_RESULT_PAGES_ON_MAP = 1; // page size * n pages = number of listings shown on a map.
const MODAL_BREAKPOINT = 768; // Search is in modal on mobile layout
const SEARCH_WITH_MAP_DEBOUNCE = 300; // Little bit of debounce before search is initiated.
const BOUNDS_FIXED_PRECISION = 8;
const CATEGORY_URL_PARAM = 'pub_category';
const AMENITIES_URL_PARAM = 'pub_amenities';
const USE_SEARCH_FILTER_PANEL = false;
// Find correct extended data key from config.custom
// e.g. 'pub_category' -> 'categories'.
const customConfigKey = paramKey => {
switch (paramKey) {
case CATEGORY_URL_PARAM:
return 'categories';
case AMENITIES_URL_PARAM:
return 'amenities';
default:
return null;
}
};
const validURLParamForExtendedData = (paramKey, urlParams) => {
const configKey = customConfigKey(paramKey);
const value = urlParams[paramKey];
const valueArray = value ? value.split(',') : [];
if (configKey && valueArray.length > 0) {
const allowedValues = config.custom[configKey].map(a => a.key);
const validValues = intersection(valueArray, allowedValues).join(',');
return validValues.length > 0 ? { [paramKey]: validValues } : {};
}
return {};
};
// validate filter params
const validURLParamsForExtendedData = params => {
const { [CATEGORY_URL_PARAM]: category, [AMENITIES_URL_PARAM]: amenities, ...rest } = params;
return {
...rest,
...validURLParamForExtendedData(CATEGORY_URL_PARAM, params),
...validURLParamForExtendedData(AMENITIES_URL_PARAM, params),
};
};
// extract search parameters, including a custom attribute named category
const pickSearchParamsOnly = params => {
const { address, origin, bounds, country, ...rest } = params || {};
const boundsMaybe = bounds ? { bounds } : {};
const originMaybe = config.sortSearchByDistance && origin ? { origin } : {};
return {
...boundsMaybe,
...originMaybe,
...validURLParamForExtendedData(CATEGORY_URL_PARAM, rest),
...validURLParamForExtendedData(AMENITIES_URL_PARAM, rest),
};
const customURLParamToConfig = {
[CATEGORY_URL_PARAM]: 'categories',
[AMENITIES_URL_PARAM]: 'amenities',
};
export class SearchPageComponent extends Component {
@ -103,7 +55,6 @@ export class SearchPageComponent extends Component {
this.state = {
isSearchMapOpenOnMobile: props.tab === 'map',
isMobileModalOpen: false,
isSearchFiltersPanelOpen: false,
};
// Initiating map creates 'bounds_changes' event
@ -116,19 +67,12 @@ export class SearchPageComponent extends Component {
this.searchMapListingsInProgress = false;
this.onIdle = debounce(this.onIdle.bind(this), SEARCH_WITH_MAP_DEBOUNCE);
this.fetchMoreListingsToMap = this.fetchMoreListingsToMap.bind(this);
this.onOpenMobileModal = this.onOpenMobileModal.bind(this);
this.onCloseMobileModal = this.onCloseMobileModal.bind(this);
}
componentDidMount() {
this.fetchMoreListingsToMap(this.props.location);
}
componentWillReceiveProps(nextProps) {
if (!isEqual(this.props.location, nextProps.location)) {
this.fetchMoreListingsToMap(nextProps.location);
// If no mapSearch url parameter is given, this is original location search
const { mapSearch } = parse(nextProps.location.search, {
latlng: ['origin'],
@ -175,8 +119,8 @@ export class SearchPageComponent extends Component {
bounds: viewportBounds,
country,
mapSearch: true,
...validURLParamForExtendedData(CATEGORY_URL_PARAM, rest),
...validURLParamForExtendedData(AMENITIES_URL_PARAM, rest),
...validURLParamForExtendedData(CATEGORY_URL_PARAM, rest, customURLParamToConfig),
...validURLParamForExtendedData(AMENITIES_URL_PARAM, rest, customURLParamToConfig),
};
this.viewportBounds = viewportBounds;
history.push(
@ -188,52 +132,6 @@ export class SearchPageComponent extends Component {
}
}
fetchMoreListingsToMap(location) {
// TODO Remove this function.
// Temporarily just return immediately to avoid merge conflicts.
return;
// eslint-disable-next-line no-unreachable
const { onSearchMapListings } = this.props;
const searchInURL = parse(location.search, {
latlng: ['origin'],
latlngBounds: ['bounds'],
});
const perPage = MAX_SEARCH_RESULT_PAGE_SIZE_ON_MAP;
const page = 1;
const { address, country, origin, ...rest } = searchInURL;
const originMaybe = config.sortSearchByDistance ? { origin } : {};
const searchParamsForMapResults = {
...rest,
...originMaybe,
include: ['images'],
page,
perPage,
'fields.image': ['variants.landscape-crop', 'variants.landscape-crop2x'],
'limit.images': 1,
};
this.searchMapListingsInProgress = true;
// Search more listings for map
onSearchMapListings(searchParamsForMapResults)
.then(response => {
const hasNextPage =
page < response.data.meta.totalPages && page < MAX_SEARCH_RESULT_PAGES_ON_MAP;
if (hasNextPage) {
onSearchMapListings({ ...searchParamsForMapResults, page: page + 1 });
} else {
this.searchMapListingsInProgress = false;
}
})
.catch(error => {
// In case of error, stop recursive loop and report error.
// TODO: Show and error in the listings column
// eslint-disable-next-line no-console
console.error(`An error (${error} occured while trying to retrieve map listings`);
});
}
// Invoked when a modal is opened from a child component,
// for example when a filter modal is opened in mobile view
onOpenMobileModal() {
@ -271,77 +169,33 @@ export class SearchPageComponent extends Component {
// urlQueryParams doesn't contain page specific url params
// like mapSearch, page or origin (origin depends on config.sortSearchByDistance)
const urlQueryParams = pickSearchParamsOnly(searchInURL);
const urlQueryParams = pickSearchParamsOnly(
searchInURL,
[AMENITIES_URL_PARAM, CATEGORY_URL_PARAM],
customURLParamToConfig
);
// Page transition might initially use values from previous search
const urlQueryString = stringify(urlQueryParams);
const paramsQueryString = stringify(pickSearchParamsOnly(searchParams));
const searchParamsMatch = urlQueryString === paramsQueryString;
const paramsQueryString = stringify(
pickSearchParamsOnly(
searchParams,
[AMENITIES_URL_PARAM, CATEGORY_URL_PARAM],
customURLParamToConfig
)
);
const searchParamsAreInSync = urlQueryString === paramsQueryString;
const { address, bounds, origin } = searchInURL || {};
const hasPaginationInfo = !!pagination && pagination.totalItems != null;
const totalItems = searchParamsMatch && hasPaginationInfo ? pagination.totalItems : 0;
const listingsAreLoaded = !searchInProgress && searchParamsMatch && hasPaginationInfo;
const validQueryParams = validURLParamsForExtendedData(searchInURL);
const searchError = (
<h2 className={css.error}>
<FormattedMessage id="SearchPage.searchError" />
</h2>
const validQueryParams = validURLParamsForExtendedData(
searchInURL,
[AMENITIES_URL_PARAM, CATEGORY_URL_PARAM],
customURLParamToConfig
);
const searchMap = (
<SearchMap
activeListingId={activeListingId}
bounds={bounds}
center={origin}
listings={mapListings || []}
onIdle={this.onIdle}
isOpenOnModal={this.state.isSearchMapOpenOnMobile}
onCloseAsModal={() => {
onManageDisableScrolling('SearchPage.map', false);
}}
useLocationSearchBounds={!this.viewportBounds}
/>
);
const showSearchMapInMobile = this.state.isSearchMapOpenOnMobile ? searchMap : null;
const isWindowDefined = typeof window !== 'undefined';
const searchMapMaybe =
isWindowDefined && window.innerWidth < MODAL_BREAKPOINT ? showSearchMapInMobile : searchMap;
const searchParamsForPagination = parse(location.search);
// Schema for search engines (helps them to understand what this page is about)
// http://schema.org
// We are using JSON-LD format
const siteTitle = config.siteTitle;
const searchAddress = address || intl.formatMessage({ id: 'SearchPage.schemaMapSearch' });
const schemaTitle = intl.formatMessage(
{ id: 'SearchPage.schemaTitle' },
{ searchAddress, siteTitle }
);
const schemaDescription = intl.formatMessage({ id: 'SearchPage.schemaDescription' });
const schemaListings = listings.map((l, i) => {
const title = l.attributes.title;
const pathToItem = createResourceLocatorString('ListingPage', routeConfiguration(), {
id: l.id.uuid,
slug: createSlug(title),
});
return {
'@type': 'ListItem',
position: i,
url: `${config.canonicalRootURL}${pathToItem}`,
name: title,
};
});
const schemaMainEntity = JSON.stringify({
'@type': 'ItemList',
name: searchAddress,
itemListOrder: 'http://schema.org/ItemListOrderAscending',
itemListElement: schemaListings,
});
const isMobileLayout = isWindowDefined && window.innerWidth < MODAL_BREAKPOINT;
const shouldShowSearchMap =
!isMobileLayout || (isMobileLayout && this.state.isSearchMapOpenOnMobile);
const onMapIconClick = () => {
this.useLocationSearchBounds = true;
@ -349,51 +203,8 @@ export class SearchPageComponent extends Component {
this.setState({ isSearchMapOpenOnMobile: true });
};
const extraSearchFiltersPanelOrListings =
USE_SEARCH_FILTER_PANEL && this.state.isSearchFiltersPanelOpen ? (
<div className={classNames(css.searchFiltersPanel)}>
<SearchFiltersPanel
urlQueryParams={validQueryParams}
listingsAreLoaded={listingsAreLoaded}
categories={categories}
amenities={amenities}
onClosePanel={() => this.setState({ isSearchFiltersPanelOpen: false })}
/>
</div>
) : (
<div
className={classNames(css.listings, {
[css.newSearchInProgress]: !listingsAreLoaded,
})}
>
{searchListingsError ? searchError : null}
<SearchResultsPanel
className={css.searchListingsPanel}
listings={listings}
pagination={listingsAreLoaded ? pagination : null}
search={searchParamsForPagination}
setActiveListing={onActivateListing}
/>
</div>
);
// An example how to check how many filters are selected on SearchFilterPanel
// if it is in use.
//
// const searchFiltersPanelSelectedCount = [
// validQueryParams[FILTER_1_URL_PARAM],
// validQueryParams[FILTER_2_URL_PARAM],
// ].filter(param => !!param).length
const searchFiltersPanelSelectedCount = 0;
const searchFiltersPanelProps = USE_SEARCH_FILTER_PANEL
? {
isSearchFiltersPanelOpen: this.state.isSearchFiltersPanelOpen,
toggleSearchFiltersPanel: isOpen => {
this.setState({ isSearchFiltersPanelOpen: isOpen });
},
searchFiltersPanelSelectedCount,
}
: {};
const { address, bounds, origin } = searchInURL || {};
const { title, description, schema } = createSearchResultSchema(listings, address, intl);
// Set topbar class based on if a modal is open in
// a child component
@ -407,15 +218,9 @@ export class SearchPageComponent extends Component {
return (
<Page
scrollingDisabled={scrollingDisabled}
description={schemaDescription}
title={schemaTitle}
schema={{
'@context': 'http://schema.org',
'@type': 'SearchResultsPage',
description: schemaDescription,
name: schemaTitle,
mainEntity: [schemaMainEntity],
}}
description={description}
title={title}
schema={schema}
>
<TopbarContainer
className={topbarClasses}
@ -423,36 +228,22 @@ export class SearchPageComponent extends Component {
currentSearchParams={urlQueryParams}
/>
<div className={css.container}>
<div className={css.searchResultContainer}>
<SearchFilters
className={css.searchFilters}
urlQueryParams={validQueryParams}
listingsAreLoaded={listingsAreLoaded}
resultsCount={totalItems}
searchInProgress={searchInProgress}
searchListingsError={searchListingsError}
onManageDisableScrolling={onManageDisableScrolling}
categories={categories}
amenities={amenities}
{...searchFiltersPanelProps}
/>
<SearchFiltersMobile
className={css.searchFiltersMobile}
urlQueryParams={validQueryParams}
listingsAreLoaded={listingsAreLoaded}
resultsCount={totalItems}
searchInProgress={searchInProgress}
searchListingsError={searchListingsError}
showAsModalMaxWidth={MODAL_BREAKPOINT}
onMapIconClick={onMapIconClick}
onManageDisableScrolling={onManageDisableScrolling}
onOpenModal={this.onOpenMobileModal}
onCloseModal={this.onCloseMobileModal}
categories={categories}
amenities={amenities}
/>
{extraSearchFiltersPanelOrListings}
</div>
<MainPanel
urlQueryParams={validQueryParams}
listings={listings}
searchInProgress={searchInProgress}
searchListingsError={searchListingsError}
searchParamsAreInSync={searchParamsAreInSync}
onActivateListing={onActivateListing}
onManageDisableScrolling={onManageDisableScrolling}
onMapIconClick={onMapIconClick}
pagination={pagination}
searchParamsForPagination={parse(location.search)}
showAsModalMaxWidth={MODAL_BREAKPOINT}
customURLParamToConfig={customURLParamToConfig}
primaryFilters={{ amenities, categories }}
secondaryFilters={{ amenities, categories }}
/>
<ModalInMobile
className={css.mapPanel}
id="SearchPage.map"
@ -461,7 +252,22 @@ export class SearchPageComponent extends Component {
showAsModalMaxWidth={MODAL_BREAKPOINT}
onManageDisableScrolling={onManageDisableScrolling}
>
<div className={css.map}>{searchMapMaybe}</div>
<div className={css.map}>
{shouldShowSearchMap ? (
<SearchMap
activeListingId={activeListingId}
bounds={bounds}
center={origin}
listings={mapListings || []}
onIdle={this.onIdle}
isOpenOnModal={this.state.isSearchMapOpenOnMobile}
onCloseAsModal={() => {
onManageDisableScrolling('SearchPage.map', false);
}}
useLocationSearchBounds={!this.viewportBounds}
/>
) : null}
</div>
</ModalInMobile>
</div>
</Page>
@ -487,6 +293,7 @@ const { array, bool, func, oneOf, object, shape, string } = PropTypes;
SearchPageComponent.propTypes = {
listings: array,
mapListings: array,
onActivateListing: func.isRequired,
onManageDisableScrolling: func.isRequired,
onSearchMapListings: func.isRequired,
pagination: propTypes.pagination,

View file

@ -36,6 +36,7 @@ describe('SearchPageComponent', () => {
currentUserHasListings: false,
intl: fakeIntl,
isAuthenticated: false,
onActivateListing: noop,
onLogout: noop,
onManageDisableScrolling: noop,
onSearchMapListings: noop,

View file

@ -22,10 +22,29 @@ exports[`SearchPageComponent matches snapshot 1`] = `
currentSearchParams={Object {}}
/>
<div>
<div>
<withRouter(InjectIntl(SearchFiltersComponent))
amenities={
Array [
<MainPanel
className={null}
customURLParamToConfig={
Object {
"pub_amenities": "amenities",
"pub_category": "categories",
}
}
listings={Array []}
onActivateListing={[Function]}
onManageDisableScrolling={[Function]}
onMapIconClick={[Function]}
pagination={
Object {
"page": 1,
"perPage": 12,
"totalItems": 22,
"totalPages": 2,
}
}
primaryFilters={
Object {
"amenities": Array [
Object {
"key": "dog1",
"label": "Dog 1",
@ -34,10 +53,8 @@ exports[`SearchPageComponent matches snapshot 1`] = `
"key": "dog2",
"label": "Dog 2",
},
]
}
categories={
Array [
],
"categories": Array [
Object {
"key": "cat1",
"label": "Cat 1",
@ -46,18 +63,18 @@ exports[`SearchPageComponent matches snapshot 1`] = `
"key": "cat2",
"label": "Cat 2",
},
]
],
}
listingsAreLoaded={true}
onManageDisableScrolling={[Function]}
resultsCount={22}
searchInProgress={false}
searchListingsError={null}
urlQueryParams={Object {}}
/>
<InjectIntl(withRouter(SearchFiltersMobileComponent))
amenities={
Array [
}
resultsCount={0}
rootClassName={null}
searchInProgress={false}
searchListingsError={null}
searchParamsAreInSync={true}
searchParamsForPagination={Object {}}
secondaryFilters={
Object {
"amenities": Array [
Object {
"key": "dog1",
"label": "Dog 1",
@ -66,10 +83,8 @@ exports[`SearchPageComponent matches snapshot 1`] = `
"key": "dog2",
"label": "Dog 2",
},
]
}
categories={
Array [
],
"categories": Array [
Object {
"key": "cat1",
"label": "Cat 1",
@ -78,38 +93,12 @@ exports[`SearchPageComponent matches snapshot 1`] = `
"key": "cat2",
"label": "Cat 2",
},
]
],
}
listingsAreLoaded={true}
onCloseModal={[Function]}
onManageDisableScrolling={[Function]}
onMapIconClick={[Function]}
onOpenModal={[Function]}
resultsCount={22}
searchInProgress={false}
searchListingsError={null}
showAsModalMaxWidth={768}
urlQueryParams={Object {}}
/>
<div
className=""
>
<SearchResultsPanel
className={null}
listings={Array []}
pagination={
Object {
"page": 1,
"perPage": 12,
"totalItems": 22,
"totalPages": 2,
}
}
rootClassName={null}
search={Object {}}
/>
</div>
</div>
}
showAsModalMaxWidth={768}
urlQueryParams={Object {}}
/>
<withViewport(ModalInMobileComponent)
id="SearchPage.map"
isModalOpenOnMobile={false}