diff --git a/src/components/SearchFiltersMobile/SearchFiltersMobile.js b/src/components/SearchFiltersMobile/SearchFiltersMobile.js index df4c1571..7e2d3cca 100644 --- a/src/components/SearchFiltersMobile/SearchFiltersMobile.js +++ b/src/components/SearchFiltersMobile/SearchFiltersMobile.js @@ -11,13 +11,15 @@ import { SecondaryButton, ModalInMobile, Button, SelectSingleFilterMobile } from import css from './SearchFiltersMobile.css'; const CATEGORY_URL_PARAM = 'pub_category'; +const allowedCategories = [CATEGORY_URL_PARAM]; const validateParamValue = value => value !== null && value !== undefined && value.length > 0; +const validateParamKey = key => allowedCategories.includes(key); // Check if a filter parameter is included query parameters const hasFilterQueryParams = queryParams => { const firstFilterParam = toPairs(queryParams).find(entry => { - return validateParamValue(entry[1]); + return validateParamKey(entry[0]) && validateParamValue(entry[1]); }); return !!firstFilterParam; }; @@ -131,13 +133,15 @@ class SearchFiltersMobileComponent extends Component { const categoryLabel = intl.formatMessage({ id: 'SearchFiltersMobile.categoryLabel', }); + const initialCategory = urlQueryParams[CATEGORY_URL_PARAM]; + const categoryFilter = categories ? ( ) : null; diff --git a/src/components/SelectMultipleFilter/SelectMultipleFilter.js b/src/components/SelectMultipleFilter/SelectMultipleFilter.js index d9f3d823..37206a3e 100644 --- a/src/components/SelectMultipleFilter/SelectMultipleFilter.js +++ b/src/components/SelectMultipleFilter/SelectMultipleFilter.js @@ -2,34 +2,13 @@ import React, { Component } from 'react'; import { array, arrayOf, func, number, string } from 'prop-types'; import classNames from 'classnames'; import { injectIntl, intlShape } from 'react-intl'; -import { toPairs } from 'lodash'; +import { arrayToFormValues, formValuesToArray } from '../../util/data'; import SelectMultipleFilterForm from './SelectMultipleFilterForm'; import css from './SelectMultipleFilter.css'; const KEY_CODE_ESCAPE = 27; -/** - * Convert an array of option keys into - * an object that can be passed to a redux - * form as initial values. - */ -const keysToValues = keys => { - return keys.reduce((map, key) => { - map[key] = true; - return map; - }, {}); -}; - -/** - * Convert an object containing values received - * from a redux form into an array of values. - */ -const valuesToKeys = values => { - const entries = toPairs(values); - return entries.filter(entry => entry[1] === true).map(entry => entry[0]); -}; - class SelectMultipleFilter extends Component { constructor(props) { super(props); @@ -49,7 +28,7 @@ class SelectMultipleFilter extends Component { handleSubmit(values) { const { name, onSelect, urlParam } = this.props; - const selectedKeys = valuesToKeys(values[name]); + const selectedKeys = formValuesToArray(values[name]); this.setState({ isOpen: false }); onSelect(urlParam, selectedKeys); } @@ -129,7 +108,7 @@ class SelectMultipleFilter extends Component { // turn a list of values into a map that can be passed to // a redux form - const initialValuesObj = keysToValues(initialValues); + const initialValuesObj = arrayToFormValues(initialValues); // pass the initial values with the name key so that // they can be passed to the correct field diff --git a/src/components/SelectMultipleFilterMobile/SelectMultipleFilterMobile.css b/src/components/SelectMultipleFilterMobile/SelectMultipleFilterMobile.css new file mode 100644 index 00000000..4621d7d7 --- /dev/null +++ b/src/components/SelectMultipleFilterMobile/SelectMultipleFilterMobile.css @@ -0,0 +1,6 @@ +@import '../../marketplace.css'; + +.root { + padding-bottom: 16px; + border-bottom: 1px solid var(--matterColorNegative); +} diff --git a/src/components/SelectMultipleFilterMobile/SelectMultipleFilterMobile.example.js b/src/components/SelectMultipleFilterMobile/SelectMultipleFilterMobile.example.js new file mode 100644 index 00000000..6b63bcb5 --- /dev/null +++ b/src/components/SelectMultipleFilterMobile/SelectMultipleFilterMobile.example.js @@ -0,0 +1,71 @@ +import React from 'react'; +import { withRouter } from 'react-router-dom'; +import SelectMultipleFilterMobile from './SelectMultipleFilterMobile'; +import { stringify, parse } from '../../util/urlHelpers'; + +const URL_PARAM = 'pub_amenities'; + +const options = [ + { + key: 'towels', + label: 'Towels', + }, + { + key: 'bathroom', + label: 'Bathroom', + }, + { + key: 'swimming_pool', + label: 'Swimming pool', + }, + { + key: 'own_drinks', + label: 'Own drinks allowed', + }, + { + key: 'jacuzzi', + label: 'Jacuzzi', + }, + { + key: 'audiovisual_entertainment', + label: 'Audiovisual entertainment', + }, + { + key: 'barbeque', + label: 'Barbeque', + }, + { + key: 'own_food_allowed', + label: 'Own food allowed', + }, +]; + +const handleSelect = (urlParam, values, history) => { + const queryParams = values ? `?${stringify({ [urlParam]: values.join(',') })}` : ''; + history.push(`${window.location.pathname}${queryParams}`); +}; + +const AmenitiesFilterComponent = props => { + const { history, location } = props; + + const params = parse(location.search); + const amenities = params[URL_PARAM]; + const initialValues = !!amenities ? amenities.split(',') : []; + + return ( + handleSelect(urlParam, values, history)} + initialValues={initialValues} + /> + ); +}; + +export const AmenitiesFilter = { + component: withRouter(AmenitiesFilterComponent), + props: {}, + group: 'misc', +}; diff --git a/src/components/SelectMultipleFilterMobile/SelectMultipleFilterMobile.js b/src/components/SelectMultipleFilterMobile/SelectMultipleFilterMobile.js new file mode 100644 index 00000000..79d9826d --- /dev/null +++ b/src/components/SelectMultipleFilterMobile/SelectMultipleFilterMobile.js @@ -0,0 +1,73 @@ +import React from 'react'; +import { array, string, arrayOf, func, shape } from 'prop-types'; +import classNames from 'classnames'; + +import SelectMultipleFilterMobileForm from './SelectMultipleFilterMobileForm'; +import { arrayToFormValues, formValuesToArray } from '../../util/data'; + +import css from './SelectMultipleFilterMobile.css'; + +const SelectMultipleFilterMobile = props => { + const { + rootClassName, + className, + name, + urlParam, + label, + onSelect, + options, + initialValues, + } = props; + + const handleSelect = values => { + const selectedKeys = formValuesToArray(values[name]); + onSelect(urlParam, selectedKeys); + }; + + const handleClear = () => { + onSelect(urlParam, null); + }; + + const classes = classNames(rootClassName || css.root, className); + + const initialValuesObj = arrayToFormValues(initialValues); + const namedInitialValues = { [name]: initialValuesObj }; + + return ( +
+ +
+ ); +}; + +SelectMultipleFilterMobile.defaultProps = { + rootClassName: null, + className: null, + initialValues: [], +}; + +SelectMultipleFilterMobile.propTypes = { + rootClassName: string, + className: string, + name: string.isRequired, + urlParam: string.isRequired, + label: string.isRequired, + onSelect: func.isRequired, + options: arrayOf( + shape({ + key: string.isRequired, + label: string.isRequired, + }) + ).isRequired, + initialValues: array, +}; + +export default SelectMultipleFilterMobile; diff --git a/src/components/SelectMultipleFilterMobile/SelectMultipleFilterMobileForm.css b/src/components/SelectMultipleFilterMobile/SelectMultipleFilterMobileForm.css new file mode 100644 index 00000000..65477bf5 --- /dev/null +++ b/src/components/SelectMultipleFilterMobile/SelectMultipleFilterMobileForm.css @@ -0,0 +1,58 @@ +@import '../../marketplace.css'; + +.root { +} + +.filterLabel, +.filterLabelSelected { + @apply --marketplaceH3FontStyles; + margin-bottom: 16px; +} + +.filterLabel { + color: var(--matterColorDark); +} + +.filterLabelSelected { + color: var(--marketplaceColor); +} + +.labelButton { + /* Override button styles */ + outline: none; + text-align: left; + border: none; + padding: 0; +} + +.optionsContainerOpen { + padding-left: 20px; + height: auto; +} + +.optionsContainerClosed { + height: 0; + overflow: hidden; +} + +.clearButton { + @apply --marketplaceH5FontStyles; + font-weight: var(--fontWeightMedium); + color: var(--matterColorAnti); + + /* Layout */ + display: inline; + float: right; + margin-top: 6px; + padding: 0; + + /* Override button styles */ + outline: none; + text-align: left; + border: none; + + &:focus, + &:hover { + color: var(--matterColor); + } +} diff --git a/src/components/SelectMultipleFilterMobile/SelectMultipleFilterMobileForm.js b/src/components/SelectMultipleFilterMobile/SelectMultipleFilterMobileForm.js new file mode 100644 index 00000000..fd36e9e4 --- /dev/null +++ b/src/components/SelectMultipleFilterMobile/SelectMultipleFilterMobileForm.js @@ -0,0 +1,92 @@ +import React, { Component } from 'react'; +import { arrayOf, func, node, number, shape, string } from 'prop-types'; +import { compose } from 'redux'; +import { reduxForm, propTypes as formPropTypes } from 'redux-form'; +import { FormattedMessage, injectIntl, intlShape } from 'react-intl'; + +import { FieldGroupCheckbox, Form } from '../../components'; +import css from './SelectMultipleFilterMobileForm.css'; + +class SelectMultipleFilterMobileFormComponent extends Component { + constructor(props) { + super(props); + this.state = { isOpen: true }; + + this.handleClear = this.handleClear.bind(this); + this.toggleIsOpen = this.toggleIsOpen.bind(this); + } + + handleClear() { + const { destroy, onClear } = this.props; + // destroy redux form state so that initial values passed + // to the form are also cleared + destroy(); + onClear(); + } + + toggleIsOpen() { + this.setState(prevState => ({ isOpen: !prevState.isOpen })); + } + + render() { + const { form, name, label, options, initialValuesCount, intl } = this.props; + + const hasInitialValues = initialValuesCount > 0; + const labelClass = hasInitialValues ? css.filterLabelSelected : css.filterLabel; + + const labelText = hasInitialValues + ? intl.formatMessage( + { id: 'SelectMultipleFilterMobileForm.labelSelected' }, + { labelText: label, count: initialValuesCount } + ) + : label; + + const optionsContainerClass = this.state.isOpen + ? css.optionsContainerOpen + : css.optionsContainerClosed; + + return ( +
+
+ + +
+ +
+ +
+
+ ); + } +} + +SelectMultipleFilterMobileFormComponent.defaultProps = { + initialValuesCount: 0, +}; + +SelectMultipleFilterMobileFormComponent.propTypes = { + ...formPropTypes, + name: string.isRequired, + label: string.isRequired, + onClear: func.isRequired, + options: arrayOf( + shape({ + key: string.isRequired, + label: node.isRequired, + }) + ).isRequired, + initialValuesCount: number, + + // form injectIntl + intl: intlShape.isRequired, +}; + +const defaultFormName = 'SelectMultipleFilterMobileForm'; + +export default compose(reduxForm({ form: defaultFormName }), injectIntl)( + SelectMultipleFilterMobileFormComponent +); diff --git a/src/components/SelectSingleFilterMobile/SelectSingleFilterMobile.js b/src/components/SelectSingleFilterMobile/SelectSingleFilterMobile.js index 966e94a1..faa9cd02 100644 --- a/src/components/SelectSingleFilterMobile/SelectSingleFilterMobile.js +++ b/src/components/SelectSingleFilterMobile/SelectSingleFilterMobile.js @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import { object, string, func, arrayOf, shape } from 'prop-types'; +import { string, func, arrayOf, shape } from 'prop-types'; import classNames from 'classnames'; import { FormattedMessage } from 'react-intl'; @@ -28,12 +28,9 @@ class SelectSingleFilterMobile extends Component { } render() { - const { rootClassName, className, urlQueryParams, urlParam, paramLabel, options } = this.props; + const { rootClassName, className, label, options, initialValue } = this.props; - // current value of this custom attribute filter - const currentValue = urlQueryParams[urlParam]; - - const labelClass = currentValue ? css.filterLabelSelected : css.filterLabel; + const labelClass = initialValue ? css.filterLabelSelected : css.filterLabel; const optionsContainerClass = this.state.isOpen ? css.optionsContainerOpen @@ -45,7 +42,7 @@ class SelectSingleFilterMobile extends Component {