@@ -253,11 +304,15 @@ class SearchFiltersMobileComponent extends Component {
- {categoryFilterElement}
- {amenitiesFilterElement}
- {priceFilterElement}
-
+ {categoryFilterElement}
+ {amenitiesFilterElement}
+ {priceFilterElement}
+ {dateRangeFilterElement}
+
diff --git a/src/components/SelectSingleFilter/SelectSingleFilter.css b/src/components/SelectSingleFilter/SelectSingleFilterPopup.css
similarity index 100%
rename from src/components/SelectSingleFilter/SelectSingleFilter.css
rename to src/components/SelectSingleFilter/SelectSingleFilterPopup.css
diff --git a/src/components/SelectSingleFilter/SelectSingleFilterPopup.js b/src/components/SelectSingleFilter/SelectSingleFilterPopup.js
new file mode 100644
index 00000000..bcce2899
--- /dev/null
+++ b/src/components/SelectSingleFilter/SelectSingleFilterPopup.js
@@ -0,0 +1,111 @@
+import React, { Component } from 'react';
+import { string, func, arrayOf, shape, number } from 'prop-types';
+import { FormattedMessage } from 'react-intl';
+import classNames from 'classnames';
+
+import { Menu, MenuContent, MenuItem, MenuLabel } from '..';
+import css from './SelectSingleFilterPopup.css';
+
+const optionLabel = (options, key) => {
+ const option = options.find(o => o.key === key);
+ return option ? option.label : key;
+};
+
+class SelectSingleFilterPopup extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = { isOpen: false };
+ this.onToggleActive = this.onToggleActive.bind(this);
+ this.selectOption = this.selectOption.bind(this);
+ }
+
+ onToggleActive(isOpen) {
+ this.setState({ isOpen: isOpen });
+ }
+
+ selectOption(urlParam, option) {
+ this.setState({ isOpen: false });
+ this.props.onSelect(urlParam, option);
+ }
+
+ render() {
+ const {
+ rootClassName,
+ className,
+ urlParam,
+ label,
+ options,
+ initialValue,
+ contentPlacementOffset,
+ } = this.props;
+
+ // resolve menu label text and class
+ const menuLabel = initialValue ? optionLabel(options, initialValue) : label;
+ const menuLabelClass = initialValue ? css.menuLabelSelected : css.menuLabel;
+
+ const classes = classNames(rootClassName || css.root, className);
+
+ return (
+
+ {menuLabel}
+
+ {options.map(option => {
+ // check if this option is selected
+ const selected = initialValue === option.key;
+ // menu item border class
+ const menuItemBorderClass = selected ? css.menuItemBorderSelected : css.menuItemBorder;
+
+ return (
+
+ this.selectOption(urlParam, option.key)}
+ >
+
+ {option.label}
+
+
+ );
+ })}
+
+ this.selectOption(urlParam, null)}>
+
+
+
+
+
+ );
+ }
+}
+
+SelectSingleFilterPopup.defaultProps = {
+ rootClassName: null,
+ className: null,
+ initialValue: null,
+ contentPlacementOffset: 0,
+};
+
+SelectSingleFilterPopup.propTypes = {
+ rootClassName: string,
+ className: string,
+ urlParam: string.isRequired,
+ label: string.isRequired,
+ onSelect: func.isRequired,
+ options: arrayOf(
+ shape({
+ key: string.isRequired,
+ label: string.isRequired,
+ })
+ ).isRequired,
+ initialValue: string,
+ contentPlacementOffset: number,
+};
+
+export default SelectSingleFilterPopup;
diff --git a/src/components/index.js b/src/components/index.js
index 167705b3..2f82f024 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -2,6 +2,7 @@ export { default as ActivityFeed } from './ActivityFeed/ActivityFeed';
export { default as AddImages } from './AddImages/AddImages';
export { default as Avatar, AvatarMedium, AvatarLarge } from './Avatar/Avatar';
export { default as BookingBreakdown } from './BookingBreakdown/BookingBreakdown';
+export { default as BookingDateRangeFilter } from './BookingDateRangeFilter/BookingDateRangeFilter';
export { default as Button, PrimaryButton, SecondaryButton, InlineTextButton } from './Button/Button';
export { default as BookingPanel } from './BookingPanel/BookingPanel';
export { default as CookieConsent } from './CookieConsent/CookieConsent';
@@ -22,12 +23,15 @@ export { default as FieldCheckbox } from './FieldCheckbox/FieldCheckbox';
export { default as FieldCheckboxGroup } from './FieldCheckboxGroup/FieldCheckboxGroup';
export { default as FieldCurrencyInput } from './FieldCurrencyInput/FieldCurrencyInput';
export { default as FieldDateInput } from './FieldDateInput/FieldDateInput';
+export { default as FieldDateRangeController } from './FieldDateRangeController/FieldDateRangeController';
export { default as FieldDateRangeInput } from './FieldDateRangeInput/FieldDateRangeInput';
export { default as FieldRadioButton } from './FieldRadioButton/FieldRadioButton';
export { default as FieldPhoneNumberInput } from './FieldPhoneNumberInput/FieldPhoneNumberInput';
export { default as FieldReviewRating } from './FieldReviewRating/FieldReviewRating';
export { default as FieldSelect } from './FieldSelect/FieldSelect';
export { default as FieldTextInput } from './FieldTextInput/FieldTextInput';
+export { default as FilterPlain } from './FilterPlain/FilterPlain';
+export { default as FilterPopup } from './FilterPopup/FilterPopup';
export { default as Footer } from './Footer/Footer';
export { default as Form } from './Form/Form';
export { default as IconAdd } from './IconAdd/IconAdd';
@@ -74,6 +78,7 @@ export { default as NamedLink } from './NamedLink/NamedLink';
export { default as NamedRedirect } from './NamedRedirect/NamedRedirect';
export { default as NotificationBadge } from './NotificationBadge/NotificationBadge';
export { default as OrderDiscussionPanel } from './OrderDiscussionPanel/OrderDiscussionPanel';
+export { default as OutsideClickHandler } from './OutsideClickHandler/OutsideClickHandler';
export { default as Page } from './Page/Page';
export { default as PaginationLinks } from './PaginationLinks/PaginationLinks';
export { default as PriceFilter } from './PriceFilter/PriceFilter';
@@ -98,9 +103,7 @@ export { default as SectionHowItWorks } from './SectionHowItWorks/SectionHowItWo
export { default as SectionLocations } from './SectionLocations/SectionLocations';
export { default as SectionThumbnailLinks } from './SectionThumbnailLinks/SectionThumbnailLinks';
export { default as SelectMultipleFilter } from './SelectMultipleFilter/SelectMultipleFilter';
-export { default as SelectMultipleFilterPlain } from './SelectMultipleFilterPlain/SelectMultipleFilterPlain';
export { default as SelectSingleFilter } from './SelectSingleFilter/SelectSingleFilter';
-export { default as SelectSingleFilterPlain } from './SelectSingleFilterPlain/SelectSingleFilterPlain';
export { default as StripeBankAccountTokenInputField } from './StripeBankAccountTokenInputField/StripeBankAccountTokenInputField';
export { default as TabNav } from './TabNav/TabNav';
export { LinkTabNavHorizontal, ButtonTabNavHorizontal } from './TabNavHorizontal/TabNavHorizontal';
diff --git a/src/containers/SearchPage/SearchPage.duck.js b/src/containers/SearchPage/SearchPage.duck.js
index be796592..27a4a626 100644
--- a/src/containers/SearchPage/SearchPage.duck.js
+++ b/src/containers/SearchPage/SearchPage.duck.js
@@ -2,6 +2,7 @@ import unionWith from 'lodash/unionWith';
import { storableError } from '../../util/errors';
import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck';
import { convertUnitToSubUnit, unitDivisor } from '../../util/currency';
+import { formatDateStringToUTC, getExclusiveEndDate } from '../../util/dates';
import config from '../../config';
// ================ Action types ================ //
@@ -131,11 +132,30 @@ export const searchListings = searchParams => (dispatch, getState, sdk) => {
: {};
};
- const { perPage, price, ...rest } = searchParams;
+ const datesSearchParams = datesParam => {
+ const values = datesParam ? datesParam.split(',') : [];
+ const hasValues = datesParam && values.length === 2;
+ const startDate = hasValues ? values[0] : null;
+ const endDate = hasValues ? getExclusiveEndDate(values[1]) : null;
+
+ return hasValues
+ ? {
+ start: formatDateStringToUTC(startDate),
+ end: formatDateStringToUTC(endDate),
+ // Availability can be full or partial. Default value is full.
+ availability: 'full',
+ }
+ : {};
+ };
+
+ const { perPage, price, dates, ...rest } = searchParams;
const priceMaybe = priceSearchParams(price);
+ const datesMaybe = datesSearchParams(dates);
+
const params = {
...rest,
...priceMaybe,
+ ...datesMaybe,
per_page: perPage,
};
diff --git a/src/containers/SearchPage/SearchPage.helpers.js b/src/containers/SearchPage/SearchPage.helpers.js
index 0002724a..0be2ba0d 100644
--- a/src/containers/SearchPage/SearchPage.helpers.js
+++ b/src/containers/SearchPage/SearchPage.helpers.js
@@ -21,18 +21,23 @@ export const validURLParamForExtendedData = (paramName, paramValue, filters) =>
const valueArray = paramValue ? paramValue.split(',') : [];
if (filterConfig && valueArray.length > 0) {
- const { min, max } = filterConfig.config || {};
+ const { min, max, active } = filterConfig.config || {};
if (filterConfig.options) {
+ // Single and multiselect filters
const allowedValues = filterConfig.options.map(o => o.key);
const validValues = intersection(valueArray, allowedValues).join(',');
return validValues.length > 0 ? { [paramName]: validValues } : {};
} else if (filterConfig.config && min != null && max != null) {
+ // Price filter
const validValues = valueArray.map(v => {
return v < min ? min : v > max ? max : v;
});
return validValues.length === 2 ? { [paramName]: validValues.join(',') } : {};
+ } else if (filterConfig.config && active) {
+ // Generic filter
+ return paramValue.length > 0 ? { [paramName]: paramValue } : {};
}
}
return {};
diff --git a/src/containers/SearchPage/SearchPage.js b/src/containers/SearchPage/SearchPage.js
index 1b67184c..68033c38 100644
--- a/src/containers/SearchPage/SearchPage.js
+++ b/src/containers/SearchPage/SearchPage.js
@@ -52,7 +52,7 @@ export class SearchPageComponent extends Component {
}
filters() {
- const { categories, amenities, priceFilterConfig } = this.props;
+ const { categories, amenities, priceFilterConfig, dateRangeFilterConfig } = this.props;
return {
categoryFilter: {
@@ -67,6 +67,10 @@ export class SearchPageComponent extends Component {
paramName: 'price',
config: priceFilterConfig,
},
+ dateRangeFilter: {
+ paramName: 'dates',
+ config: dateRangeFilterConfig,
+ },
};
}
@@ -210,6 +214,7 @@ export class SearchPageComponent extends Component {
categoryFilter: filters.categoryFilter,
amenitiesFilter: filters.amenitiesFilter,
priceFilter: filters.priceFilter,
+ dateRangeFilter: filters.dateRangeFilter,
}}
/>
+);
+
+export const FilterFormExample = {
+ component: FilterForm,
+ props: {
+ id: 'FilterFormExample',
+ liveEdit: false,
+ showAsPopup: true,
+ contentPlacementOffset: -14,
+ onSubmit: values => {
+ console.log(values);
+ },
+ onCancel: () => {
+ console.log('onCancel called');
+ },
+ onClear: () => {
+ console.log('onClear called');
+ },
+ label: 'Example label',
+ children: field,
+ },
+ group: 'forms',
+};
+
+export const FilterFormExampleLiveEdit = {
+ component: FilterForm,
+ props: {
+ id: 'FilterFormExampleLiveEdit',
+ liveEdit: true,
+ showAsPopup: false,
+ contentPlacementOffset: -14,
+ onChange: values => {
+ console.log(values);
+ },
+ label: 'Example label',
+ children: field,
+ },
+ group: 'forms',
+};
diff --git a/src/forms/FilterForm/FilterForm.js b/src/forms/FilterForm/FilterForm.js
new file mode 100644
index 00000000..3b8cfc6f
--- /dev/null
+++ b/src/forms/FilterForm/FilterForm.js
@@ -0,0 +1,117 @@
+import React from 'react';
+import { bool, func, node, object } from 'prop-types';
+import classNames from 'classnames';
+import { Form as FinalForm, FormSpy } from 'react-final-form';
+import arrayMutators from 'final-form-arrays';
+import { injectIntl, intlShape } from 'react-intl';
+
+import { Form } from '../../components';
+import css from './FilterForm.css';
+
+const FilterFormComponent = props => {
+ const { liveEdit, onChange, onSubmit, onCancel, onClear, ...rest } = props;
+
+ if (liveEdit && !onChange) {
+ throw new Error('FilterForm: if liveEdit is true you need to provide onChange function');
+ }
+
+ if (!liveEdit && !(onCancel && onClear && onSubmit)) {
+ throw new Error(
+ 'FilterForm: if liveEdit is false you need to provide onCancel, onClear, and onSubmit functions'
+ );
+ }
+
+ const handleChange = formState => {
+ if (formState.dirty) {
+ onChange(formState.values);
+ }
+ };
+
+ const formCallbacks = liveEdit ? { onSubmit: () => null } : { onSubmit, onCancel, onClear };
+ return (
+
{
+ const {
+ id,
+ form,
+ handleSubmit,
+ onClear,
+ onCancel,
+ style,
+ paddingClasses,
+ intl,
+ children,
+ } = formRenderProps;
+
+ const handleCancel = () => {
+ // reset the final form to initialValues
+ form.reset();
+ onCancel();
+ };
+
+ const clear = intl.formatMessage({ id: 'FilterForm.clear' });
+ const cancel = intl.formatMessage({ id: 'FilterForm.cancel' });
+ const submit = intl.formatMessage({ id: 'FilterForm.submit' });
+
+ const classes = classNames(css.root);
+
+ return (
+
+ );
+ }}
+ />
+ );
+};
+
+FilterFormComponent.defaultProps = {
+ liveEdit: false,
+ style: null,
+ onCancel: null,
+ onChange: null,
+ onClear: null,
+ onSubmit: null,
+};
+
+FilterFormComponent.propTypes = {
+ liveEdit: bool,
+ onCancel: func,
+ onChange: func,
+ onClear: func,
+ onSubmit: func,
+ style: object,
+ children: node.isRequired,
+
+ // form injectIntl
+ intl: intlShape.isRequired,
+};
+
+const FilterForm = injectIntl(FilterFormComponent);
+
+export default FilterForm;
diff --git a/src/forms/SelectMultipleFilterForm/SelectMultipleFilterForm.js b/src/forms/SelectMultipleFilterForm/SelectMultipleFilterForm.js
deleted file mode 100644
index 88f87222..00000000
--- a/src/forms/SelectMultipleFilterForm/SelectMultipleFilterForm.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import React from 'react';
-import { arrayOf, bool, func, node, object, shape, string } from 'prop-types';
-import classNames from 'classnames';
-import { Form as FinalForm } from 'react-final-form';
-import { injectIntl, intlShape } from 'react-intl';
-import arrayMutators from 'final-form-arrays';
-
-import { FieldCheckboxGroup, Form } from '../../components';
-import css from './SelectMultipleFilterForm.css';
-
-const SelectMultipleFilterFormComponent = props => {
- return (
- {
- const {
- form,
- handleSubmit,
- id,
- name,
- onClear,
- onCancel,
- options,
- isOpen,
- contentRef,
- style,
- intl,
- } = formRenderProps;
- const classes = classNames(css.root, { [css.isOpen]: isOpen });
-
- const handleCancel = () => {
- // reset the final form to initialValues
- form.reset();
- onCancel();
- };
-
- const clear = intl.formatMessage({ id: 'SelectMultipleFilterForm.clear' });
- const cancel = intl.formatMessage({ id: 'SelectMultipleFilterForm.cancel' });
- const submit = intl.formatMessage({ id: 'SelectMultipleFilterForm.submit' });
- return (
-
- );
- }}
- />
- );
-};
-
-SelectMultipleFilterFormComponent.defaultProps = {
- contentRef: null,
- style: null,
-};
-
-SelectMultipleFilterFormComponent.propTypes = {
- id: string.isRequired,
- name: string.isRequired,
- onClear: func.isRequired,
- onCancel: func.isRequired,
- options: arrayOf(
- shape({
- key: string.isRequired,
- label: node.isRequired,
- })
- ).isRequired,
- isOpen: bool.isRequired,
- contentRef: func,
- style: object,
-
- // form injectIntl
- intl: intlShape.isRequired,
-};
-
-const SelectMultipleFilterForm = injectIntl(SelectMultipleFilterFormComponent);
-
-export default SelectMultipleFilterForm;
diff --git a/src/forms/SelectMultipleFilterPlainForm/SelectMultipleFilterPlainForm.js b/src/forms/SelectMultipleFilterPlainForm/SelectMultipleFilterPlainForm.js
deleted file mode 100644
index 87e484da..00000000
--- a/src/forms/SelectMultipleFilterPlainForm/SelectMultipleFilterPlainForm.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import React from 'react';
-import { arrayOf, bool, node, shape, string, func } from 'prop-types';
-import { Form as FinalForm, FormSpy } from 'react-final-form';
-import arrayMutators from 'final-form-arrays';
-
-import { FieldCheckboxGroup, Form } from '../../components';
-
-const SelectMultipleFilterPlainForm = props => {
- const { onChange, ...rest } = props;
-
- const handleChange = formState => {
- if (formState.dirty) {
- onChange(formState.values);
- }
- };
-
- return (
- null}
- mutators={{ ...arrayMutators }}
- render={formRenderProps => {
- const { className, id, name, options, twoColumns } = formRenderProps;
- return (
-
- );
- }}
- />
- );
-};
-
-SelectMultipleFilterPlainForm.defaultProps = {
- className: null,
- twoColumns: false,
- onChange: () => null,
-};
-
-SelectMultipleFilterPlainForm.propTypes = {
- className: string,
- id: string.isRequired,
- name: string.isRequired,
- options: arrayOf(
- shape({
- key: string.isRequired,
- label: node.isRequired,
- })
- ).isRequired,
- twoColumns: bool,
- onChange: func,
-};
-
-export default SelectMultipleFilterPlainForm;
diff --git a/src/forms/index.js b/src/forms/index.js
index 399596fa..f3607e97 100644
--- a/src/forms/index.js
+++ b/src/forms/index.js
@@ -9,6 +9,7 @@ export { default as EditListingPoliciesForm } from './EditListingPoliciesForm/Ed
export { default as EditListingPricingForm } from './EditListingPricingForm/EditListingPricingForm';
export { default as EmailVerificationForm } from './EmailVerificationForm/EmailVerificationForm';
export { default as EnquiryForm } from './EnquiryForm/EnquiryForm';
+export { default as FilterForm } from './FilterForm/FilterForm';
export { default as LocationSearchForm } from './LocationSearchForm/LocationSearchForm';
export { default as LoginForm } from './LoginForm/LoginForm';
export { default as PasswordChangeForm } from './PasswordChangeForm/PasswordChangeForm';
@@ -19,8 +20,6 @@ export { default as PriceFilterForm } from './PriceFilterForm/PriceFilterForm';
export { default as ProfileSettingsForm } from './ProfileSettingsForm/ProfileSettingsForm';
export { default as ReviewForm } from './ReviewForm/ReviewForm';
export { default as SendMessageForm } from './SendMessageForm/SendMessageForm';
-export { default as SelectMultipleFilterForm } from './SelectMultipleFilterForm/SelectMultipleFilterForm';
-export { default as SelectMultipleFilterPlainForm } from './SelectMultipleFilterPlainForm/SelectMultipleFilterPlainForm';
export { default as SignupForm } from './SignupForm/SignupForm';
export { default as StripePaymentForm } from './StripePaymentForm/StripePaymentForm';
export { default as TopbarSearchForm } from './TopbarSearchForm/TopbarSearchForm';
diff --git a/src/marketplace-custom-config.js b/src/marketplace-custom-config.js
index cd717a38..05c9b08d 100644
--- a/src/marketplace-custom-config.js
+++ b/src/marketplace-custom-config.js
@@ -51,3 +51,8 @@ export const priceFilterConfig = {
max: 1000,
step: 5,
};
+
+// Activate booking dates filter on search page
+export const dateRangeFilterConfig = {
+ active: true,
+};
diff --git a/src/translations/en.json b/src/translations/en.json
index ec37bd63..54762a51 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -67,6 +67,10 @@
"BookingDatesForm.requiredDate": "Oops, make sure your date is correct!",
"BookingDatesForm.timeSlotsError": "Loading listing availability failed. Please refresh the page.",
"BookingDatesForm.youWontBeChargedInfo": "You won't be charged yet",
+ "BookingDateRangeFilter.labelPlain": "Dates",
+ "BookingDateRangeFilter.labelPopup": "Dates",
+ "BookingDateRangeFilter.labelSelectedPlain": "{dates}",
+ "BookingDateRangeFilter.labelSelectedPopup": "{dates}",
"BookingPanel.closedListingButtonText": "Sorry, this listing has been closed.",
"BookingPanel.ctaButtonMessage": "Request to book",
"BookingPanel.hostedBy": "Hosted by {name}",
@@ -256,6 +260,10 @@
"FieldReviewRating.star3": "OK - 3 stars",
"FieldReviewRating.star4": "Good - 4 stars",
"FieldReviewRating.star5": "Awesome - 5 stars",
+ "FilterForm.cancel": "Cancel",
+ "FilterForm.clear": "Clear",
+ "FilterForm.submit": "Apply",
+ "FilterPlain.clear": "Clear",
"Footer.copyright": "© Sharetribe",
"Footer.goToFacebook": "Go to Facebook page",
"Footer.goToInstagram": "Go to Instagram page",
@@ -681,13 +689,9 @@
"SectionLocations.listingsInLocation": "Saunas in {location}",
"SectionLocations.title": "Explore exotic locations in Finland",
"SelectMultipleFilter.labelSelected": "{labelText} • {count}",
- "SelectMultipleFilterForm.cancel": "Cancel",
- "SelectMultipleFilterForm.clear": "Clear",
- "SelectMultipleFilterForm.submit": "Apply",
- "SelectMultipleFilterPlainForm.clear": "Clear",
"SelectMultipleFilterPlainForm.labelSelected": "{labelText} • {count}",
- "SelectSingleFilter.clear": "Clear",
- "SelectSingleFilterPlain.clear": "Clear",
+ "SelectSingleFilter.popupClear": "Clear",
+ "SelectSingleFilter.plainClear": "Clear",
"SendMessageForm.sendFailed": "Failed to send. Please try again.",
"SendMessageForm.sendMessage": "Send message",
"SignupForm.emailInvalid": "A valid email address is required",
diff --git a/src/translations/es.json b/src/translations/es.json
index cbc7c841..fe775c17 100644
--- a/src/translations/es.json
+++ b/src/translations/es.json
@@ -67,6 +67,10 @@
"BookingDatesForm.requiredDate": "Ups, asegúrate de que la fecha es correcta!",
"BookingDatesForm.timeSlotsError": "Error al cargar la disponibilidad del anuncio. Por favor, actualiza la página.",
"BookingDatesForm.youWontBeChargedInfo": "Aún no se hará el cargo a tu tarjeta",
+ "BookingDateRangeFilter.labelPlain": "Fechas",
+ "BookingDateRangeFilter.labelPopup": "Fechas",
+ "BookingDateRangeFilter.labelSelectedPlain": "{dates}",
+ "BookingDateRangeFilter.labelSelectedPopup": "{dates}",
"BookingPanel.closedListingButtonText": "Lo sentimos, este anuncio ha sido cerrado.",
"BookingPanel.ctaButtonMessage": "Solicitar reserva",
"BookingPanel.hostedBy": "Publicado por {name}",
@@ -256,6 +260,10 @@
"FieldReviewRating.star3": "Bien - 3 estrellas",
"FieldReviewRating.star4": "Muy bien - 4 estrellas",
"FieldReviewRating.star5": "Excelente - 5 estrellas",
+ "FilterForm.cancel": "Cancelar",
+ "FilterForm.clear": "Borrar",
+ "FilterForm.submit": "Aplicar",
+ "FilterPlain.clear": "Borrar",
"Footer.copyright": "© Sharetribe",
"Footer.goToFacebook": "Ir a Facebook",
"Footer.goToInstagram": "Ir a Instagram",
@@ -453,6 +461,8 @@
"PasswordResetPage.resetFailed": "Ha ocurrido un error. Por favor, inténtalo de nuevo.",
"PasswordResetPage.title": "Restablecer contraseña",
"PayoutDetailsForm.accountTypeTitle": "Tipo de cuenta",
+ "PayoutDetailsForm.additionalOwnersInfoLink": "páginas de soporte de Stripe.",
+ "PayoutDetailsForm.additionalOwnerInfoText": "Para Hong Kong, Singapur, y países miembros de la Zona Única de Pagos en Euros, Stripe requiere información de cada persona que sea dueña de por lo menos 25% de la empresa. Para mas información, visita las {additionalOwnersInfoLink}",
"PayoutDetailsForm.additionalOwnerLabel": "Añadir proprietario adicional",
"PayoutDetailsForm.additionalOwnerRemove": "Eliminar propietario adicional",
"PayoutDetailsForm.addressTitle": "Dirección",
@@ -630,7 +640,7 @@
"ReviewForm.reviewContentPlaceholder": "Describe tu experiencia...",
"ReviewForm.reviewContentRequired": "Necesitas añadir un comentario a tu valoración",
"ReviewForm.reviewRatingLabel": "Valora tu experiencia",
- "ReviewForm.reviewRatingRequired": "Necesitas añadir un comentario a tu valoración",
+ "ReviewForm.reviewRatingRequired": "Necesitas añadir un comentario a tu valoración.",
"ReviewForm.reviewSubmit": "Publicar valoración",
"ReviewForm.reviewSubmitAlreadySent": "La valoración ya ha sido enviada. Por favor, actualiza la página.",
"ReviewForm.reviewSubmitFailed": "Error al enviar la valoración. Por favor, inténtalo de nuevo.",
@@ -679,13 +689,9 @@
"SectionLocations.listingsInLocation": "Saunas en {location}",
"SectionLocations.title": "Explora lugares exóticos en Finlandia",
"SelectMultipleFilter.labelSelected": "{labelText} • {count}",
- "SelectMultipleFilterForm.cancel": "Cancelar",
- "SelectMultipleFilterForm.clear": "Borrar",
- "SelectMultipleFilterForm.submit": "Aplicar",
- "SelectMultipleFilterPlainForm.clear": "Borrar",
"SelectMultipleFilterPlainForm.labelSelected": "{labelText} • {count}",
- "SelectSingleFilter.clear": "Borrar",
- "SelectSingleFilterPlain.clear": "Borrar",
+ "SelectSingleFilter.popupClear": "Borrar",
+ "SelectSingleFilter.plainClear": "Borrar",
"SendMessageForm.sendFailed": "Error al enviar. Por favor, inténtalo de nuevo.",
"SendMessageForm.sendMessage": "Enviar mensaje",
"SignupForm.emailInvalid": "Se necesita una dirección de correo electrónico válida",
diff --git a/src/translations/fr.json b/src/translations/fr.json
index 302e5ae0..220016b5 100644
--- a/src/translations/fr.json
+++ b/src/translations/fr.json
@@ -67,6 +67,10 @@
"BookingDatesForm.requiredDate": "Oups, vérifiez que la date est correcte",
"BookingDatesForm.timeSlotsError": "Le chargement des disponibilités pour cette annonce a échoué. Veuillez rafraîchir la page.",
"BookingDatesForm.youWontBeChargedInfo": "Vous ne serez pas facturé immédiatement",
+ "BookingDateRangeFilter.labelPlain": "Dates",
+ "BookingDateRangeFilter.labelPopup": "Dates",
+ "BookingDateRangeFilter.labelSelectedPlain": "{dates}",
+ "BookingDateRangeFilter.labelSelectedPopup": "{dates}",
"BookingPanel.closedListingButtonText": "Navré, cette annonce est close.",
"BookingPanel.ctaButtonMessage": "Réserver",
"BookingPanel.hostedBy": "Proposé par {name}",
@@ -256,6 +260,10 @@
"FieldReviewRating.star3": "Moyen - 3 étoiles",
"FieldReviewRating.star4": "Correct - 4 étoiles",
"FieldReviewRating.star5": "Génial - 5 étoiles",
+ "FilterForm.cancel": "Annuler",
+ "FilterForm.clear": "Effacer",
+ "FilterForm.submit": "Appliquer",
+ "FilterPlain.clear": "Effacer",
"Footer.copyright": "© Sharetribe",
"Footer.goToFacebook": "Aller sur la page Facebook",
"Footer.goToInstagram": "Aller sur la page Instagram",
@@ -453,6 +461,8 @@
"PasswordResetPage.resetFailed": "La réinitialisation a échoué.",
"PasswordResetPage.title": "Réinitialiser le mot de passe",
"PayoutDetailsForm.accountTypeTitle": "Type de compte",
+ "PayoutDetailsForm.additionalOwnersInfoLink": "le support Stripe",
+ "PayoutDetailsForm.additionalOwnerInfoText": "Pour Hong Kong, Singapore and les pays membres de la zone Euro, Stripe requiert les informations de chaque personne possédant au moins 24% de l'entreprise. Pour plus d'information, parcourez {additionalOwnersInfoLink}.",
"PayoutDetailsForm.additionalOwnerLabel": "Ajouter un autre propriétaire",
"PayoutDetailsForm.additionalOwnerRemove": "Enlever un propriétaire",
"PayoutDetailsForm.addressTitle": "Adresse",
@@ -488,7 +498,7 @@
"PayoutDetailsForm.companyAddressTitle": "Adresse de l'entreprise",
"PayoutDetailsForm.companyDetailsTitle": "Détails de l'entreprise",
"PayoutDetailsForm.companyNameLabel": "Nom de l'entreprise",
- "PayoutDetailsForm.companyNamePlaceholder": "Entrez le nom de l'entreprise...",
+ "PayoutDetailsForm.companyNamePlaceholder": "Entrez le nom de l'entreprise…",
"PayoutDetailsForm.companyNameRequired": "Le nom de l'entreprise est requis",
"PayoutDetailsForm.companyTaxIdLabel.AT": "Firmenbuchnummer (FN)",
"PayoutDetailsForm.companyTaxIdLabel.AU": "ACN/ABN - TFN de l'entreprise",
@@ -511,7 +521,7 @@
"PayoutDetailsForm.companyTaxIdLabel.PT": "Numéro Contribuinte",
"PayoutDetailsForm.companyTaxIdLabel.SE": "Numéro d'organisation",
"PayoutDetailsForm.companyTaxIdLabel.US": "Identifiant taxes (Tax ID)",
- "PayoutDetailsForm.companyTaxIdPlaceholder": "Entrez {idName}",
+ "PayoutDetailsForm.companyTaxIdPlaceholder": "Entrez {idName}…",
"PayoutDetailsForm.companyTaxIdRequired": "{idName} est requis",
"PayoutDetailsForm.countryLabel": "Pays",
"PayoutDetailsForm.countryNames.AT": "Autriche",
@@ -679,13 +689,9 @@
"SectionLocations.listingsInLocation": "Saunas à {location}",
"SectionLocations.title": "Réchauffez-vous depuis une destination exotique en Finlande",
"SelectMultipleFilter.labelSelected": "{labelText} • {count}",
- "SelectMultipleFilterForm.cancel": "Annuler",
- "SelectMultipleFilterForm.clear": "Effacer",
- "SelectMultipleFilterForm.submit": "Valider",
- "SelectMultipleFilterPlainForm.clear": "Effacer",
"SelectMultipleFilterPlainForm.labelSelected": "{labelText} • {count}",
- "SelectSingleFilter.clear": "Effacer",
- "SelectSingleFilterPlain.clear": "Effacer",
+ "SelectSingleFilter.popupClear": "Effacer",
+ "SelectSingleFilter.plainClear": "Effacer",
"SendMessageForm.sendFailed": "Impossible d'envoyer. Veuillez essayer de nouveau.",
"SendMessageForm.sendMessage": "Envoyer le message",
"SignupForm.emailInvalid": "Une adresse email valide est requise.",
diff --git a/src/util/dates.js b/src/util/dates.js
index a5cbdbf1..326ae73e 100644
--- a/src/util/dates.js
+++ b/src/util/dates.js
@@ -169,3 +169,59 @@ export const formatDate = (intl, todayString, d) => {
return `${formattedDate}, ${formattedTime}`;
};
+
+/**
+ * Converts string given in ISO8601 format to date object.
+ * This is used e.g. when when dates are parsed form urlParams
+ *
+ * @param {String} dateString in 'YYYY-MM-DD'format
+ *
+ * @returns {Date} parsed date object
+ */
+export const parseDateFromISO8601 = dateString => {
+ return moment(dateString, 'YYYY-MM-DD').toDate();
+};
+
+/**
+ * Converts date to string ISO8601 format ('YYYY-MM-DD').
+ * This string is used e.g. in urlParam.
+ *
+ * @param {Date} date
+ *
+ * @returns {String} string in 'YYYY-MM-DD'format
+ */
+
+export const stringifyDateToISO8601 = date => {
+ return moment(date).format('YYYY-MM-DD');
+};
+
+/**
+ * Formats string ('YYYY-MM-DD') to UTC format ('0000-00-00T00:00:00.000Z').
+ * This is used in search query.
+ *
+ * @param {String} string in 'YYYY-MM-DD'format
+ *
+ * @returns {String} string in '0000-00-00T00:00:00.000Z' format
+ */
+
+export const formatDateStringToUTC = dateString => {
+ return moment.utc(dateString).toDate();
+};
+
+/**
+ * Formats string ('YYYY-MM-DD') to UTC format ('0000-00-00T00:00:00.000Z') and adds one day.
+ * This is used as end date of the search query.
+ * One day must be added because end of the availability is exclusive in API.
+ *
+ * @param {String} string in 'YYYY-MM-DD'format
+ *
+ * @returns {String} string in '0000-00-00T00:00:00.000Z' format
+ */
+
+export const getExclusiveEndDate = dateString => {
+ return moment
+ .utc(dateString)
+ .add(1, 'days')
+ .startOf('day')
+ .toDate();
+};
diff --git a/src/util/dates.test.js b/src/util/dates.test.js
index 5cd3bacf..daab97c2 100644
--- a/src/util/dates.test.js
+++ b/src/util/dates.test.js
@@ -1,5 +1,13 @@
import { fakeIntl } from './test-data';
-import { isDate, isSameDate, nightsBetween, daysBetween, formatDate } from './dates';
+import {
+ isDate,
+ isSameDate,
+ nightsBetween,
+ daysBetween,
+ formatDate,
+ parseDateFromISO8601,
+ stringifyDateToISO8601,
+} from './dates';
describe('date utils', () => {
describe('isDate()', () => {
@@ -87,4 +95,19 @@ describe('date utils', () => {
expect(formatDate(fakeIntl, 'Today', d)).toEqual('2017-11-22, 13:51');
});
});
+
+ describe('parseDateFromISO8601()', () => {
+ it('should return date', () => {
+ const dateString = '2018-11-23';
+ const date = new Date(2018, 10, 23);
+ expect(parseDateFromISO8601(dateString)).toEqual(date);
+ });
+ });
+
+ describe('stringifyDateToISO8601()', () => {
+ it('should return string in YYYY-MM-DD format', () => {
+ const date = new Date(2018, 10, 23);
+ expect(stringifyDateToISO8601(date)).toEqual('2018-11-23');
+ });
+ });
});
diff --git a/src/util/types.js b/src/util/types.js
index d3a981f6..88932196 100644
--- a/src/util/types.js
+++ b/src/util/types.js
@@ -464,7 +464,18 @@ const filterWithPriceConfig = shape({
}).isRequired,
});
-propTypes.filterConfig = oneOfType([filterWithOptions, filterWithPriceConfig]);
+const filterIsActiveConfig = shape({
+ paramName: string.isRequired,
+ config: shape({
+ active: bool.isRequired,
+ }).isRequired,
+});
+
+propTypes.filterConfig = oneOfType([
+ filterWithOptions,
+ filterWithPriceConfig,
+ filterIsActiveConfig,
+]);
export const ERROR_CODE_TRANSACTION_LISTING_NOT_FOUND = 'transaction-listing-not-found';
export const ERROR_CODE_TRANSACTION_INVALID_TRANSITION = 'transaction-invalid-transition';