mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-27 09:13:14 +10:00
Merge pull request #714 from sharetribe/searchfilterspanel
Searchfilterspanel
This commit is contained in:
commit
cf6ed72a0d
15 changed files with 500 additions and 39 deletions
|
|
@ -47,6 +47,7 @@
|
|||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checked {
|
||||
|
|
@ -63,4 +64,5 @@
|
|||
color: var(--matterColor);
|
||||
margin-top: -1px;
|
||||
margin-bottom: 1px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
margin-top: 16px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
/* parent uses flexbox: this defines minimum width for text "6 results" */
|
||||
/* parent uses flexbox: this defines minimum width for translated text "noResults" */
|
||||
flex-basis: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
|
@ -65,3 +65,42 @@
|
|||
.resultsFound {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.searchFiltersPanelClosed {
|
||||
@apply --marketplaceButtonStylesSecondary;
|
||||
@apply --marketplaceSearchFilterLabelFontStyles;
|
||||
|
||||
display: inline-block;
|
||||
padding: 9px 16px 10px 16px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
border-radius: 4px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
background-color: var(--matterColorLight);
|
||||
border-color: transparent;
|
||||
text-decoration: none;
|
||||
box-shadow: var(--boxShadowFilterButton);
|
||||
}
|
||||
}
|
||||
|
||||
.searchFiltersPanelOpen {
|
||||
@apply --marketplaceButtonStyles;
|
||||
@apply --marketplaceSearchFilterLabelFontStyles;
|
||||
font-weight: var(--fontWeightSemiBold);
|
||||
|
||||
display: inline-block;
|
||||
padding: 9px 16px 10px 16px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--marketplaceColor);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border: 1px solid var(--marketplaceColorDark);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ const SearchFiltersComponent = props => {
|
|||
searchInProgress,
|
||||
categories,
|
||||
amenities,
|
||||
isSearchFiltersPanelOpen,
|
||||
toggleSearchFiltersPanel,
|
||||
searchFiltersPanelSelectedCount,
|
||||
history,
|
||||
intl,
|
||||
} = props;
|
||||
|
|
@ -90,11 +93,29 @@ const SearchFiltersComponent = props => {
|
|||
/>
|
||||
) : null;
|
||||
|
||||
const toggleSearchFiltersPanelButtonClasses =
|
||||
isSearchFiltersPanelOpen || searchFiltersPanelSelectedCount > 0
|
||||
? css.searchFiltersPanelOpen
|
||||
: css.searchFiltersPanelClosed;
|
||||
const toggleSearchFiltersPanelButton = toggleSearchFiltersPanel ? (
|
||||
<button
|
||||
className={toggleSearchFiltersPanelButtonClasses}
|
||||
onClick={() => {
|
||||
toggleSearchFiltersPanel(!isSearchFiltersPanelOpen);
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="SearchFilters.moreFiltersButton"
|
||||
values={{ count: searchFiltersPanelSelectedCount }}
|
||||
/>
|
||||
</button>
|
||||
) : null;
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className={css.filters}>
|
||||
{categoryFilter}
|
||||
{amenitiesFilter}
|
||||
{toggleSearchFiltersPanelButton}
|
||||
</div>
|
||||
|
||||
{listingsAreLoaded && resultsCount > 0 ? (
|
||||
|
|
@ -127,6 +148,9 @@ SearchFiltersComponent.defaultProps = {
|
|||
searchingInProgress: false,
|
||||
categories: null,
|
||||
amenities: null,
|
||||
isSearchFiltersPanelOpen: false,
|
||||
toggleSearchFiltersPanel: null,
|
||||
searchFiltersPanelSelectedCount: 0,
|
||||
};
|
||||
|
||||
SearchFiltersComponent.propTypes = {
|
||||
|
|
@ -139,6 +163,9 @@ SearchFiltersComponent.propTypes = {
|
|||
onManageDisableScrolling: func.isRequired,
|
||||
categories: array,
|
||||
amenities: array,
|
||||
isSearchFiltersPanelOpen: bool,
|
||||
toggleSearchFiltersPanel: func,
|
||||
searchFiltersPanelSelectedCount: number,
|
||||
|
||||
// from withRouter
|
||||
history: shape({
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const hasFilterQueryParams = queryParams => {
|
|||
class SearchFiltersMobileComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isFiltersOpenOnMobile: false };
|
||||
this.state = { isFiltersOpenOnMobile: false, initialQueryParams: null };
|
||||
|
||||
this.openFilters = this.openFilters.bind(this);
|
||||
this.cancelFilters = this.cancelFilters.bind(this);
|
||||
|
|
|
|||
66
src/components/SearchFiltersPanel/SearchFiltersPanel.css
Normal file
66
src/components/SearchFiltersPanel/SearchFiltersPanel.css
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
@import '../../marketplace.css';
|
||||
|
||||
.root {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
background-color: var(--matterColorBright);
|
||||
}
|
||||
|
||||
.filtersWrapper {
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.resetAllButton {
|
||||
@apply --marketplaceH5FontStyles;
|
||||
font-weight: var(--fontWeightMedium);
|
||||
color: var(--matterColorAnti);
|
||||
|
||||
/* Layout */
|
||||
margin: 0 auto 0 0;
|
||||
|
||||
/* Override button styles */
|
||||
outline: none;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: var(--matterColor);
|
||||
}
|
||||
}
|
||||
|
||||
.cancelButton {
|
||||
@apply --marketplaceH5FontStyles;
|
||||
font-weight: var(--fontWeightMedium);
|
||||
color: var(--matterColorAnti);
|
||||
|
||||
/* Layout */
|
||||
margin: 0 24px 0 24px;
|
||||
|
||||
/* Override button styles */
|
||||
outline: none;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: var(--matterColor);
|
||||
}
|
||||
}
|
||||
|
||||
.applyButton {
|
||||
@apply --marketplaceH5FontStyles;
|
||||
font-weight: var(--fontWeightMedium);
|
||||
|
||||
/* Layout */
|
||||
margin: 0;
|
||||
|
||||
/* Override button styles */
|
||||
outline: none;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: var(--marketplaceColorDark);
|
||||
}
|
||||
}
|
||||
177
src/components/SearchFiltersPanel/SearchFiltersPanel.js
Normal file
177
src/components/SearchFiltersPanel/SearchFiltersPanel.js
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
/**
|
||||
* SearchFiltersPanel can be used to add extra filters to togglable panel in SearchPage.
|
||||
* Using this component will need you to enable it in SearchPage and passing needed props to
|
||||
* SearchFilters component (which is the default place for SearchFilters).
|
||||
*
|
||||
*
|
||||
* An example how to render MultiSelectFilter inside render function:
|
||||
*
|
||||
* const currentQueryParams = this.state.currentQueryParams;
|
||||
* const splitQueryParam = queryParam => queryParam ? queryParam.split(',') : [];
|
||||
*
|
||||
* // initialValue for a select should come either from state.currentQueryParam or urlQueryParam
|
||||
* const hascurrentQueryParam = typeof currentQueryParams[MULTI_SELECT_URL_PARAM] !== 'undefined'
|
||||
* const initialMultiSelectValue = hascurrentQueryParam
|
||||
* ? splitQueryParam(currentQueryParams[MULTI_SELECT_URL_PARAM])
|
||||
* : splitQueryParam(this.props.urlQueryParams[MULTI_SELECT_URL_PARAM]);
|
||||
*
|
||||
* const multiSelectFilterX = multiSelectFilterXFromProps ? (
|
||||
* <SelectMultipleFilterMobile
|
||||
* id="SearchFiltersPanel.multiSelectFilterX"
|
||||
* name="multiSelectFilterX"
|
||||
* urlParam={MULTI_SELECT_URL_PARAM}
|
||||
* label={this.props.intl.formatMessage({ id: 'SearchFiltersPanel.multiSelectFilterXLabel' })}
|
||||
* onSelect={this.handleSelectMultiple}
|
||||
* options={multiSelectFilterXFromProps}
|
||||
* initialValues={initialMultiSelectValue}
|
||||
* twoColumns
|
||||
* />
|
||||
* ) : null;
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { func, object, shape, string } from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { omit } from 'lodash';
|
||||
|
||||
import routeConfiguration from '../../routeConfiguration';
|
||||
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);
|
||||
this.state = { currentQueryParams: props.urlQueryParams };
|
||||
|
||||
this.applyFilters = this.applyFilters.bind(this);
|
||||
this.cancelFilters = this.cancelFilters.bind(this);
|
||||
this.resetAll = this.resetAll.bind(this);
|
||||
this.handleSelectSingle = this.handleSelectSingle.bind(this);
|
||||
this.handleSelectMultiple = this.handleSelectMultiple.bind(this);
|
||||
}
|
||||
|
||||
// Apply the filters by redirecting to SearchPage with new filters.
|
||||
applyFilters() {
|
||||
const { history, urlQueryParams, onClosePanel } = this.props;
|
||||
|
||||
history.push(
|
||||
createResourceLocatorString(
|
||||
'SearchPage',
|
||||
routeConfiguration(),
|
||||
{},
|
||||
{ ...urlQueryParams, ...this.state.currentQueryParams }
|
||||
)
|
||||
);
|
||||
|
||||
// Ensure that panel closes (if now changes have been made)
|
||||
onClosePanel();
|
||||
}
|
||||
|
||||
// Close the filters by clicking cancel, revert to the initial params
|
||||
cancelFilters() {
|
||||
this.setState({ currentQueryParams: {} });
|
||||
this.props.onClosePanel();
|
||||
}
|
||||
|
||||
// Reset all filter query parameters
|
||||
resetAll(e) {
|
||||
const { urlQueryParams, history, onClosePanel } = this.props;
|
||||
|
||||
const queryParams = omit(urlQueryParams, FILTERS);
|
||||
history.push(createResourceLocatorString('SearchPage', routeConfiguration(), {}, queryParams));
|
||||
|
||||
// Ensure that panel closes (if now changes have been made)
|
||||
onClosePanel();
|
||||
|
||||
// blur event target if event is passed
|
||||
if (e && e.currentTarget) {
|
||||
e.currentTarget.blur();
|
||||
}
|
||||
}
|
||||
|
||||
handleSelectSingle(urlParam, option) {
|
||||
const urlQueryParams = this.props.urlQueryParams;
|
||||
this.setState(prevState => {
|
||||
const prevQueryParams = prevState.currentQueryParams;
|
||||
const mergedQueryParams = { ...urlQueryParams, ...prevQueryParams };
|
||||
|
||||
// query parameters after selecting the option
|
||||
// if no option is passed, clear the selection for the filter
|
||||
const currentQueryParams = option
|
||||
? { ...mergedQueryParams, [urlParam]: option }
|
||||
: omit(mergedQueryParams, urlParam);
|
||||
|
||||
return { currentQueryParams };
|
||||
});
|
||||
}
|
||||
|
||||
handleSelectMultiple(urlParam, options) {
|
||||
const urlQueryParams = this.props.urlQueryParams;
|
||||
this.setState(prevState => {
|
||||
const prevQueryParams = prevState.currentQueryParams;
|
||||
const mergedQueryParams = { ...urlQueryParams, ...prevQueryParams };
|
||||
|
||||
// query parameters after selecting options
|
||||
// if no option is passed, clear the selection from state.currentQueryParams
|
||||
const currentQueryParams =
|
||||
options && options.length > 0
|
||||
? { ...mergedQueryParams, [urlParam]: options.join(',') }
|
||||
: { ...mergedQueryParams, [urlParam]: null };
|
||||
|
||||
return { currentQueryParams };
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { rootClassName, className } = this.props;
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className={css.filtersWrapper}>{/* Add filters here */}</div>
|
||||
<div className={css.footer}>
|
||||
<InlineTextButton className={css.resetAllButton} onClick={this.resetAll}>
|
||||
<FormattedMessage id={'SearchFiltersPanel.resetAll'} />
|
||||
</InlineTextButton>
|
||||
<InlineTextButton className={css.cancelButton} onClick={this.cancelFilters}>
|
||||
<FormattedMessage id={'SearchFiltersPanel.cancel'} />
|
||||
</InlineTextButton>
|
||||
<InlineTextButton className={css.applyButton} onClick={this.applyFilters}>
|
||||
<FormattedMessage id={'SearchFiltersPanel.apply'} />
|
||||
</InlineTextButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SearchFiltersPanelComponent.defaultProps = {
|
||||
rootClassName: null,
|
||||
className: null,
|
||||
};
|
||||
|
||||
SearchFiltersPanelComponent.propTypes = {
|
||||
rootClassName: string,
|
||||
className: string,
|
||||
urlQueryParams: object.isRequired,
|
||||
onClosePanel: func.isRequired,
|
||||
|
||||
// from injectIntl
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
// from withRouter
|
||||
history: shape({
|
||||
push: func.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
const SearchFiltersPanel = injectIntl(withRouter(SearchFiltersPanelComponent));
|
||||
|
||||
export default SearchFiltersPanel;
|
||||
|
|
@ -44,6 +44,9 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.columnLayout {
|
||||
}
|
||||
|
||||
.clearButton {
|
||||
@apply --marketplaceH5FontStyles;
|
||||
font-weight: var(--fontWeightMedium);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { Component } from 'react';
|
||||
import { array, string, func } from 'prop-types';
|
||||
import { array, bool, func, string } from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
|
||||
|
||||
|
|
@ -34,7 +34,17 @@ class SelectMultipleFilterMobileComponent extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { rootClassName, className, name, label, options, initialValues, intl } = this.props;
|
||||
const {
|
||||
rootClassName,
|
||||
className,
|
||||
id,
|
||||
name,
|
||||
label,
|
||||
options,
|
||||
initialValues,
|
||||
intl,
|
||||
twoColumns,
|
||||
} = this.props;
|
||||
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
|
||||
|
|
@ -48,9 +58,11 @@ class SelectMultipleFilterMobileComponent extends Component {
|
|||
)
|
||||
: label;
|
||||
|
||||
const optionsContainerClass = this.state.isOpen
|
||||
? css.optionsContainerOpen
|
||||
: css.optionsContainerClosed;
|
||||
const optionsContainerClass = classNames({
|
||||
[css.optionsContainerOpen]: this.state.isOpen,
|
||||
[css.optionsContainerClosed]: !this.state.isOpen,
|
||||
[css.columnLayout]: twoColumns,
|
||||
});
|
||||
|
||||
const initialValuesObj = arrayToFormValues(initialValues);
|
||||
const namedInitialValues = { [name]: initialValuesObj };
|
||||
|
|
@ -66,14 +78,15 @@ class SelectMultipleFilterMobileComponent extends Component {
|
|||
</button>
|
||||
</div>
|
||||
<SelectMultipleFilterMobileForm
|
||||
form={`SelectMultipleFilterMobileForm.${name}`}
|
||||
form={`SelectMultipleFilterMobileForm.${id ? id : name}`}
|
||||
className={optionsContainerClass}
|
||||
name={name}
|
||||
options={options}
|
||||
initialValues={namedInitialValues}
|
||||
onChange={this.handleSelect}
|
||||
enableReinitialize={true}
|
||||
keepDirtyOnReinitialize={true}
|
||||
twoColumns={twoColumns}
|
||||
enableReinitialize
|
||||
keepDirtyOnReinitialize
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -83,18 +96,22 @@ class SelectMultipleFilterMobileComponent extends Component {
|
|||
SelectMultipleFilterMobileComponent.defaultProps = {
|
||||
rootClassName: null,
|
||||
className: null,
|
||||
id: undefined,
|
||||
initialValues: [],
|
||||
twoColumns: false,
|
||||
};
|
||||
|
||||
SelectMultipleFilterMobileComponent.propTypes = {
|
||||
rootClassName: string,
|
||||
className: string,
|
||||
id: string,
|
||||
name: string.isRequired,
|
||||
urlParam: string.isRequired,
|
||||
label: string.isRequired,
|
||||
onSelect: func.isRequired,
|
||||
options: array.isRequired,
|
||||
initialValues: array,
|
||||
twoColumns: bool,
|
||||
|
||||
// from injectIntl
|
||||
intl: intlShape.isRequired,
|
||||
|
|
|
|||
|
|
@ -1,21 +1,27 @@
|
|||
import React from 'react';
|
||||
import { arrayOf, shape, string, node } from 'prop-types';
|
||||
import { arrayOf, bool, node, shape, string } from 'prop-types';
|
||||
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
|
||||
|
||||
import { FieldGroupCheckbox, Form } from '../../components';
|
||||
|
||||
const SelectMultipleFilterMobileFormComponent = props => {
|
||||
const { form, className, name, options } = props;
|
||||
const { form, className, name, options, twoColumns } = props;
|
||||
|
||||
return (
|
||||
<Form className={className}>
|
||||
<FieldGroupCheckbox name={name} id={`${form}.${name}`} options={options} />
|
||||
<FieldGroupCheckbox
|
||||
name={name}
|
||||
id={`${form}.${name}`}
|
||||
options={options}
|
||||
twoColumns={twoColumns}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
SelectMultipleFilterMobileFormComponent.defaultProps = {
|
||||
className: null,
|
||||
twoColumns: false,
|
||||
};
|
||||
|
||||
SelectMultipleFilterMobileFormComponent.propTypes = {
|
||||
|
|
@ -28,6 +34,7 @@ SelectMultipleFilterMobileFormComponent.propTypes = {
|
|||
label: node.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
twoColumns: bool,
|
||||
};
|
||||
|
||||
const SelectMultipleFilterMobileForm = reduxForm({})(SelectMultipleFilterMobileFormComponent);
|
||||
|
|
|
|||
|
|
@ -43,6 +43,16 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hasBullets {
|
||||
padding-left: 26px;
|
||||
}
|
||||
|
||||
.twoColumns {
|
||||
@media (--viewportMedium) {
|
||||
column-count: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.optionBorder,
|
||||
.optionBorderSelected {
|
||||
position: absolute;
|
||||
|
|
@ -64,6 +74,28 @@
|
|||
background-color: var(--matterColorDark);
|
||||
}
|
||||
|
||||
.optionBullet,
|
||||
.optionBulletSelected {
|
||||
position: absolute;
|
||||
left: -5px;
|
||||
top: 13px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--marketplaceColor);
|
||||
transition: opacity var(--transitionStyleButton);
|
||||
}
|
||||
|
||||
/* left animated "border" like hover element */
|
||||
.optionBullet {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* left static border for selected element */
|
||||
.optionBulletSelected {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.option {
|
||||
@apply --marketplaceH4FontStyles;
|
||||
font-weight: var(--fontWeightMedium);
|
||||
|
|
@ -79,6 +111,7 @@
|
|||
/* Override button styles */
|
||||
outline: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
|
|
@ -90,6 +123,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.optionSelected {
|
||||
composes: option;
|
||||
color: var(--matterColorDark);
|
||||
}
|
||||
|
||||
.clearButton {
|
||||
@apply --marketplaceH5FontStyles;
|
||||
font-weight: var(--fontWeightMedium);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { Component } from 'react';
|
||||
import { string, func, arrayOf, shape } from 'prop-types';
|
||||
import { arrayOf, bool, func, shape, string } from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
|
|
@ -28,13 +28,25 @@ class SelectSingleFilterMobile extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { rootClassName, className, label, options, initialValue } = this.props;
|
||||
const {
|
||||
rootClassName,
|
||||
className,
|
||||
label,
|
||||
options,
|
||||
initialValue,
|
||||
twoColumns,
|
||||
useBullets,
|
||||
} = this.props;
|
||||
|
||||
const labelClass = initialValue ? css.filterLabelSelected : css.filterLabel;
|
||||
|
||||
const optionsContainerClass = this.state.isOpen
|
||||
? css.optionsContainerOpen
|
||||
: css.optionsContainerClosed;
|
||||
const hasBullets = useBullets || twoColumns;
|
||||
const optionsContainerClass = classNames({
|
||||
[css.optionsContainerOpen]: this.state.isOpen,
|
||||
[css.optionsContainerClosed]: !this.state.isOpen,
|
||||
[css.hasBullets]: hasBullets,
|
||||
[css.twoColumns]: twoColumns,
|
||||
});
|
||||
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
|
||||
|
|
@ -52,12 +64,21 @@ class SelectSingleFilterMobile extends Component {
|
|||
{options.map(option => {
|
||||
// check if this option is selected
|
||||
const selected = initialValue === option.key;
|
||||
// menu item border class
|
||||
const optionBorderClass = selected ? css.optionBorderSelected : css.optionBorder;
|
||||
const optionClass = hasBullets && selected ? css.optionSelected : css.option;
|
||||
// menu item selected bullet or border class
|
||||
const optionBorderClass = hasBullets
|
||||
? classNames({
|
||||
[css.optionBulletSelected]: selected,
|
||||
[css.optionBullet]: !selected,
|
||||
})
|
||||
: classNames({
|
||||
[css.optionBorderSelected]: selected,
|
||||
[css.optionBorder]: !selected,
|
||||
});
|
||||
return (
|
||||
<button
|
||||
key={option.key}
|
||||
className={css.option}
|
||||
className={optionClass}
|
||||
onClick={() => this.selectOption(option.key)}
|
||||
>
|
||||
<span className={optionBorderClass} />
|
||||
|
|
@ -75,6 +96,8 @@ SelectSingleFilterMobile.defaultProps = {
|
|||
rootClassName: null,
|
||||
className: null,
|
||||
initialValue: null,
|
||||
twoColumns: false,
|
||||
useBullets: false,
|
||||
};
|
||||
|
||||
SelectSingleFilterMobile.propTypes = {
|
||||
|
|
@ -91,6 +114,8 @@ SelectSingleFilterMobile.propTypes = {
|
|||
})
|
||||
).isRequired,
|
||||
initialValue: string,
|
||||
twoColumns: bool,
|
||||
useBullets: bool,
|
||||
};
|
||||
|
||||
export default SelectSingleFilterMobile;
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ export { default as ReviewRating } from './ReviewRating/ReviewRating';
|
|||
export { default as Reviews } from './Reviews/Reviews';
|
||||
export { default as SearchFilters } from './SearchFilters/SearchFilters';
|
||||
export { default as SearchFiltersMobile } from './SearchFiltersMobile/SearchFiltersMobile';
|
||||
export { default as SearchFiltersPanel } from './SearchFiltersPanel/SearchFiltersPanel';
|
||||
export { default as SearchMap } from './SearchMap/SearchMap';
|
||||
export { default as SearchMapGroupLabel } from './SearchMapGroupLabel/SearchMapGroupLabel';
|
||||
export { default as SearchMapInfoCard } from './SearchMapInfoCard/SearchMapInfoCard';
|
||||
|
|
|
|||
|
|
@ -37,10 +37,10 @@
|
|||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 2px 0 96px 0;
|
||||
padding: 2px 0 0 0;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
padding: 18px 0 96px 0;
|
||||
padding: 18px 0 0 0;
|
||||
/**
|
||||
* .container is using flexbox,
|
||||
* This specifies that searchResultContainer is taking 60% from the viewport width
|
||||
|
|
@ -71,6 +71,10 @@
|
|||
padding: 0 24px;
|
||||
margin-bottom: 21px;
|
||||
}
|
||||
|
||||
@media (--viewportLarge) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.searchFiltersMobile {
|
||||
|
|
@ -81,16 +85,29 @@
|
|||
}
|
||||
}
|
||||
|
||||
.searchFiltersPanel {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
padding: 0 24px 24px 24px;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.listings {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 24px;
|
||||
padding: 0 24px 96px 24px;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
padding: 0 0 96px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.newSearchInProgress {
|
||||
opacity: 0.1;
|
||||
transition: opacity var(--transitionStyleButton);
|
||||
}
|
||||
|
||||
.searchListingsPanel {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import {
|
|||
SearchResultsPanel,
|
||||
SearchFilters,
|
||||
SearchFiltersMobile,
|
||||
SearchFiltersPanel,
|
||||
} from '../../components';
|
||||
import { TopbarContainer } from '../../containers';
|
||||
import { searchListings, searchMapListings } from './SearchPage.duck';
|
||||
|
|
@ -44,6 +45,7 @@ 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'.
|
||||
|
|
@ -99,6 +101,7 @@ export class SearchPageComponent extends Component {
|
|||
this.state = {
|
||||
isSearchMapOpenOnMobile: props.tab === 'map',
|
||||
isMobileModalOpen: false,
|
||||
isSearchFiltersPanelOpen: false,
|
||||
};
|
||||
|
||||
// Initiating map creates 'bounds_changes' event
|
||||
|
|
@ -318,6 +321,51 @@ 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}
|
||||
/>
|
||||
</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,
|
||||
}
|
||||
: {};
|
||||
|
||||
// Set topbar class based on if a modal is open in
|
||||
// a child component
|
||||
const topbarClasses = this.state.isMobileModalOpen
|
||||
|
|
@ -353,6 +401,7 @@ export class SearchPageComponent extends Component {
|
|||
onManageDisableScrolling={onManageDisableScrolling}
|
||||
categories={categories}
|
||||
amenities={amenities}
|
||||
{...searchFiltersPanelProps}
|
||||
/>
|
||||
<SearchFiltersMobile
|
||||
className={css.searchFiltersMobile}
|
||||
|
|
@ -369,20 +418,7 @@ export class SearchPageComponent extends Component {
|
|||
categories={categories}
|
||||
amenities={amenities}
|
||||
/>
|
||||
<div
|
||||
className={classNames(css.listings, {
|
||||
[css.newSearchInProgress]: !listingsAreLoaded,
|
||||
})}
|
||||
>
|
||||
{searchListingsError ? searchError : null}
|
||||
|
||||
<SearchResultsPanel
|
||||
className={css.searchListingsPanel}
|
||||
listings={listings}
|
||||
pagination={listingsAreLoaded ? pagination : null}
|
||||
search={searchParamsForPagination}
|
||||
/>
|
||||
</div>
|
||||
{extraSearchFiltersPanelOrListings}
|
||||
</div>
|
||||
<ModalInMobile
|
||||
className={css.mapPanel}
|
||||
|
|
|
|||
|
|
@ -491,6 +491,7 @@
|
|||
"SearchFilters.foundResults": "{count, number} {count, plural, one {result} other {results}}",
|
||||
"SearchFilters.loadingResults": "Loading search results…",
|
||||
"SearchFilters.loadingResultsMobile": "Loading…",
|
||||
"SearchFilters.moreFiltersButton": "{count, plural, =0 {More filters} other {More filters • #}}",
|
||||
"SearchFilters.noResults": "Could not find any listings.",
|
||||
"SearchFilters.noResultsMobile": "No results.",
|
||||
"SearchFilters.openMapView": "Map",
|
||||
|
|
@ -500,6 +501,11 @@
|
|||
"SearchFiltersMobile.heading": "Filter saunas",
|
||||
"SearchFiltersMobile.resetAll": "Reset all",
|
||||
"SearchFiltersMobile.showListings": "Show {count, number} {count, plural, one {sauna} other {saunas}}",
|
||||
"SearchFiltersPanel.amenitiesLabel": "Multiselect",
|
||||
"SearchFiltersPanel.apply": "Apply",
|
||||
"SearchFiltersPanel.cancel": "Cancel",
|
||||
"SearchFiltersPanel.categoryLabel": "Singleselect",
|
||||
"SearchFiltersPanel.resetAll": "Reset all",
|
||||
"SearchMapInfoCard.noImage": "No image",
|
||||
"SearchPage.schemaDescription": "Showing search results",
|
||||
"SearchPage.schemaMapSearch": "map search",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue