diff --git a/messages/context.json b/messages/context.json index bc119b827..c92aa645e 100644 --- a/messages/context.json +++ b/messages/context.json @@ -65,6 +65,11 @@ "store/search.filter.placeholder": "Search by {filterName}", "store/search.filter.title.brands": "Brands", "store/search.filter.title.price-ranges": "Price Ranges", + "store/search.filter.title.shipping": "Shipping", + "store/search.filter.shipping.name.delivery": "Deliver to", + "store/search.filter.shipping.name.pickup-in-point": "Pickup at", + "store/search.filter.shipping.name.pickup-nearby": "Pickup nearby", + "store/search.filter.shipping.name.pickup-all": "Pickup", "store/search.text": "Showing {from}-{to} from {recordsFiltered} records", "store/search.selected-filters": "Filtered by:", "store/search.total-products": "{recordsFiltered} {recordsFiltered, plural, one {Product} other {Products}}", diff --git a/messages/en.json b/messages/en.json index 9079f8953..eb70dedbd 100644 --- a/messages/en.json +++ b/messages/en.json @@ -66,6 +66,11 @@ "store/search.filter.placeholder": "Search by {filterName}", "store/search.filter.title.brands": "Brands", "store/search.filter.title.price-ranges": "Price Ranges", + "store/search.filter.title.shipping": "Shipping", + "store/search.filter.shipping.name.delivery": "Deliver to", + "store/search.filter.shipping.name.pickup-in-point": "Pickup at", + "store/search.filter.shipping.name.pickup-nearby": "Pickup nearby", + "store/search.filter.shipping.name.pickup-all": "Pickup", "store/search.text": "Showing {from}-{to} from {recordsFiltered} records", "store/search.selected-filters": "Filtered by:", "store/search.total-products": "{recordsFiltered} {recordsFiltered, plural, one {Product} other {Products}}", diff --git a/react/FilterNavigator.js b/react/FilterNavigator.js index 6186c1783..5a514ce16 100644 --- a/react/FilterNavigator.js +++ b/react/FilterNavigator.js @@ -8,7 +8,7 @@ import { useCssHandles, applyModifiers } from 'vtex.css-handles' import { useSearchPage } from 'vtex.search-page-context/SearchPageContext' // eslint-disable-next-line no-restricted-imports import { flatten } from 'ramda' -import { FormattedMessage } from 'react-intl' +import { FormattedMessage, useIntl } from 'react-intl' import { Button } from 'vtex.styleguide' import FilterSidebar from './components/FilterSidebar' @@ -23,7 +23,7 @@ import { import useFacetNavigation from './hooks/useFacetNavigation' import FilterNavigatorTitleTag from './components/FilterNavigatorTitleTag' import styles from './searchResult.css' -import { CATEGORIES_TITLE } from './utils/getFilters' +import { CATEGORIES_TITLE, SHIPPING_OPTIONS } from './utils/getFilters' import { newFacetPathName } from './utils/slug' import { FACETS_RENDER_THRESHOLD } from './constants/filterConstants' @@ -73,6 +73,7 @@ const FilterNavigator = ({ priceRange, tree = [], specificationFilters = [], + deliveries = [], priceRanges = [], brands = [], loading = false, @@ -101,6 +102,7 @@ const FilterNavigator = ({ priceRangeLayout = 'slider', showQuantityBadgeOnMobile = false, }) => { + const intl = useIntl() const { isMobile } = useDevice() const handles = useCssHandles(CSS_HANDLES) const [truncatedFacetsFetched, setTruncatedFacetsFetched] = useState(false) @@ -158,9 +160,17 @@ const FilterNavigator = ({ } }, [filters, filtersFetchMore, truncatedFacetsFetched, loading]) + const shipping = deliveries.map((delivery) => ({ + ...delivery, + facets: delivery.facets.map((facet) => ({ + ...facet, + name: intl.formatMessage({ id: SHIPPING_OPTIONS[facet.name] }) , + })) + })) + const selectedFilters = useMemo(() => { const options = [ - ...specificationFilters.map(filter => { + ...specificationFilters.concat(shipping).map(filter => { return filter.facets.map(facet => { return { ...newNamedFacet({ ...facet, title: filter.name }), @@ -173,7 +183,7 @@ const FilterNavigator = ({ ] return flatten(options) - }, [brands, priceRanges, specificationFilters]).filter( + }, [brands, priceRanges, specificationFilters, shipping]).filter( facet => facet.selected ) diff --git a/react/FilterNavigatorFlexible.js b/react/FilterNavigatorFlexible.js index becb16071..475f68efd 100644 --- a/react/FilterNavigatorFlexible.js +++ b/react/FilterNavigatorFlexible.js @@ -63,6 +63,7 @@ const withSearchPageContextProps = specificationFilters, categoriesTrees, queryArgs, + deliveries, } = facets const sortedFilters = useMemo( @@ -113,6 +114,7 @@ const withSearchPageContextProps = priceRangeLayout={priceRangeLayout} drawerDirectionMobile={drawerDirectionMobile} showQuantityBadgeOnMobile={showQuantityBadgeOnMobile} + deliveries={deliveries} /> diff --git a/react/SearchResultFlexible.js b/react/SearchResultFlexible.js index 9d59ccb43..84b3dca78 100644 --- a/react/SearchResultFlexible.js +++ b/react/SearchResultFlexible.js @@ -50,6 +50,7 @@ const SearchResultFlexible = ({ preventRouteChange = false, showFacetQuantity = false, showFacetTitle = false, + showShippingFacet = false, // Below are set by SearchContext searchQuery, maxItemsPerPage, @@ -82,6 +83,7 @@ const SearchResultFlexible = ({ priceRanges, specificationFilters, categoriesTrees, + deliveries, } = facets const filters = useMemo( @@ -92,8 +94,17 @@ const SearchResultFlexible = ({ brands, brandsQuantity, hiddenFacets, + deliveries, + showShippingFacet }), - [brands, hiddenFacets, priceRanges, specificationFilters, brandsQuantity] + [ + brands, + hiddenFacets, + priceRanges, + specificationFilters, + brandsQuantity, + deliveries, + ] ) const handles = useCssHandles(CSS_HANDLES) diff --git a/react/components/AccordionFilterContainer.js b/react/components/AccordionFilterContainer.js index 879d2e63f..cfcf585a5 100644 --- a/react/components/AccordionFilterContainer.js +++ b/react/components/AccordionFilterContainer.js @@ -214,7 +214,7 @@ const AccordionFilterContainer = ({ style={{ background: 'rgba(3, 4, 78, 0.4)' }} className={classNames( handles.filterLoadingOverlay, - 'fixed dim top-0 w-100 vh-100 left-0 z-9999 justify-center items-center justify-center items-center flex' + 'fixed dim top-0 w-100 vh-100 left-0 z-999 justify-center items-center justify-center items-center flex' )} > diff --git a/react/components/FacetCheckboxList.js b/react/components/FacetCheckboxList.js index 57ff8aba0..183dbff0d 100644 --- a/react/components/FacetCheckboxList.js +++ b/react/components/FacetCheckboxList.js @@ -1,17 +1,12 @@ -import classNames from 'classnames' import React, { useContext, useState, useMemo } from 'react' -import { Checkbox } from 'vtex.styleguide' -import { applyModifiers } from 'vtex.css-handles' import { useRuntime } from 'vtex.render-runtime' -import { usePixel } from 'vtex.pixel-manager' import { useSearchPage } from 'vtex.search-page-context/SearchPageContext' -import { pushFilterManipulationPixelEvent } from '../utils/filterManipulationPixelEvents' -import styles from '../searchResult.css' import SettingsContext from './SettingsContext' import { SearchFilterBar } from './SearchFilterBar' import { FACETS_RENDER_THRESHOLD } from '../constants/filterConstants' import ShowMoreFilterButton from './ShowMoreFilterButton' +import FacetCheckboxListItem from './FacetCheckboxListItem' const useSettings = () => useContext(SettingsContext) @@ -25,7 +20,6 @@ const FacetCheckboxList = ({ truncatedFacetsFetched, setTruncatedFacetsFetched, }) => { - const { push } = usePixel() const { searchQuery } = useSearchPage() const { showFacetQuantity } = useContext(SettingsContext) const { getSettings } = useRuntime() @@ -76,40 +70,16 @@ const FacetCheckboxList = ({ ) : null} {filteredFacets.slice(0, endSlice).map(facet => { - const { name, value: slugifiedName } = facet - return ( -
- { - pushFilterManipulationPixelEvent({ - name: facetTitle, - value: name, - products: searchQuery?.products ?? [], - push, - }) - - onFilterCheck({ ...facet, title: facetTitle }) - }} - value={name} - /> -
+ ) })} {shouldTruncate && ( diff --git a/react/components/FacetCheckboxListItem.js b/react/components/FacetCheckboxListItem.js new file mode 100644 index 000000000..c5d101a26 --- /dev/null +++ b/react/components/FacetCheckboxListItem.js @@ -0,0 +1,87 @@ +import classNames from 'classnames' +import React, { useMemo } from 'react' +import { applyModifiers } from 'vtex.css-handles' +import { Checkbox } from 'vtex.styleguide' +import { usePixel } from 'vtex.pixel-manager' + +import styles from '../searchResult.css' +import { pushFilterManipulationPixelEvent } from '../utils/filterManipulationPixelEvents' +import useShippingActions from '../hooks/useShippingActions' +import ShippingActionButton from './ShippingActionButton' + +const FacetCheckboxListItem = ({ + facet, + showFacetQuantity, + sampling, + facetTitle, + searchQuery, + onFilterCheck, +}) => { + const { push } = usePixel() + + const { actionLabel, actionType, openDrawer, shouldDisable } = + useShippingActions(facet) + + const showActionButton = !!actionType + + const { name, value: slugifiedName } = facet + + const facetLabel = useMemo(() => { + let labelElement = facet.name + + if (showFacetQuantity && !sampling) { + labelElement = `${labelElement} (${facet.quantity})` + } + + if (showActionButton) { + labelElement = ( +
+ {labelElement} + +
+ ) + } + + return labelElement + }, [ + showFacetQuantity, + sampling, + facet.name, + facet.quantity, + showActionButton, + actionLabel, + openDrawer, + ]) + + return ( +
+ { + pushFilterManipulationPixelEvent({ + name: facetTitle, + value: name, + products: searchQuery?.products ?? [], + push, + }) + + onFilterCheck({ ...facet, title: facetTitle }) + }} + value={name} + /> +
+ ) +} + +export default FacetCheckboxListItem diff --git a/react/components/FacetItem.js b/react/components/FacetItem.js index 7dcdf25f3..a3494e985 100644 --- a/react/components/FacetItem.js +++ b/react/components/FacetItem.js @@ -7,7 +7,8 @@ import { usePixel } from 'vtex.pixel-manager' import { pushFilterManipulationPixelEvent } from '../utils/filterManipulationPixelEvents' import SettingsContext from './SettingsContext' -import useShouldDisableFacet from '../hooks/useShouldDisableFacet' +import ShippingActionButton from './ShippingActionButton' +import useShippingActions from '../hooks/useShippingActions' const CSS_HANDLES = ['filterItem', 'productCount', 'filterItemTitle'] @@ -30,10 +31,15 @@ const FacetItem = ({ preventRouteChange, showTitle = false, }) => { + const { push } = usePixel() + + const { actionLabel, actionType, openDrawer, shouldDisable } = + useShippingActions(facet) + const showActionButton = !!actionType + const { showFacetQuantity } = useContext(SettingsContext) const [selected, setSelected] = useState(facet.selected) - const { push } = usePixel() const handles = useCssHandles(CSS_HANDLES) const classes = classNames( applyModifiers(handles.filterItem, facet.value), @@ -61,13 +67,13 @@ const FacetItem = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [facet.selected]) - const shouldDisable = useShouldDisableFacet(facet) - const facetLabel = useMemo(() => { - const labelElement = - showFacetQuantity && !sampling ? ( + let labelElement = facet.name + + if (showFacetQuantity && !sampling) { + labelElement = ( <> - {facet.name}{' '} + {labelElement}{' '} - ) : ( - facet.name + ) + } + + if (showActionButton) { + labelElement = ( +
+
{labelElement}
+ +
) + } if (showTitle) { - return ( + labelElement = ( <> - {facetTitle}:{' '} - {labelElement} + {facetTitle}: {labelElement} ) } return labelElement }, [ - facet, + showFacetQuantity, + sampling, + facet.name, + facet.value, + facet.quantity, handles.productCount, handles.filterItemTitle, - facetTitle, + showActionButton, + actionLabel, + openDrawer, showTitle, - showFacetQuantity, - sampling, + facetTitle, ]) return (
diff --git a/react/components/SearchFilter.js b/react/components/SearchFilter.js index f39138dcd..a8700203f 100644 --- a/react/components/SearchFilter.js +++ b/react/components/SearchFilter.js @@ -93,6 +93,7 @@ SearchFilter.propTypes = { /** Whether an overview of the applied filters should be displayed (`"show"`) or not (`"hide"`). */ appliedFiltersOverview: PropTypes.string, showClearByFilter: PropTypes.bool, + type: PropTypes.string, } export default SearchFilter diff --git a/react/components/SearchQuery.js b/react/components/SearchQuery.js index c710be21f..c7838f7ff 100644 --- a/react/components/SearchQuery.js +++ b/react/components/SearchQuery.js @@ -247,6 +247,7 @@ const useQueries = (variables, facetsArgs, price) => { specificationFilters: [], categoriesTrees: [], priceRanges: [], + deliveries: [], } const selectedFacetsOutput = diff --git a/react/components/ShippingActionButton.tsx b/react/components/ShippingActionButton.tsx new file mode 100644 index 000000000..b300dd99d --- /dev/null +++ b/react/components/ShippingActionButton.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import { useCssHandles } from 'vtex.css-handles' + +const CSS_HANDLES = ['shippingActionButton'] + +interface Props { + label: string + openDrawer: () => void +} + +const ShippingActionButton = ({ label, openDrawer }: Props) => { + const handles = useCssHandles(CSS_HANDLES) + + return ( + + ) +} + +export default ShippingActionButton diff --git a/react/components/SideBar.js b/react/components/SideBar.js index 005ca787d..76eddca84 100644 --- a/react/components/SideBar.js +++ b/react/components/SideBar.js @@ -42,14 +42,14 @@ class Sidebar extends Component { } const scrimClasses = classNames( - 'fixed dim bg-base--inverted top-0 z-9999 w-100 vh-100 o-40 left-0', + `${searchResult.scrim} fixed dim bg-base--inverted top-0 w-100 vh-100 o-40 left-0`, { dn: !isOpen, } ) const sidebarClasses = classNames( - `${searchResult.sidebar} w-auto-ns h-100 fixed top-0 z-9999 bg-base shadow-2 flex flex-column`, + `${searchResult.sidebar} w-auto-ns h-100 fixed top-0 bg-base shadow-2 flex flex-column`, this.props.fullWidth ? 'w-100' : 'w-80', filtersDrawerDirectionMobile === 'drawerLeft' ? 'right-0' : 'left-0' ) diff --git a/react/hooks/useShippingActions.js b/react/hooks/useShippingActions.js new file mode 100644 index 000000000..d163b352b --- /dev/null +++ b/react/hooks/useShippingActions.js @@ -0,0 +1,132 @@ +import { useCallback, useEffect, useState } from 'react' +import { usePixel, usePixelEventCallback } from 'vtex.pixel-manager' +import { useSSR } from 'vtex.render-runtime/react/components/NoSSR' + +import useShouldDisableFacet from './useShouldDisableFacet' + +const shippingActionTypes = { + delivery: 'DELIVERY', + 'pickup-nearby': 'DELIVERY', + pickup: 'DELIVERY', + 'pickup-in-point': 'PICKUP_POINT', +} + +const eventIdentifiers = { + DELIVERY: 'addressLabel', + PICKUP_POINT: 'pickupPointLabel', +} + +const placeHolders = { + DELIVERY: 'Enter location', + PICKUP_POINT: 'Enter store', +} + +const drawerEvent = { + DELIVERY: 'shipping-option-deliver-to', + PICKUP_POINT: 'shipping-option-store', +} + +const addressDependentValues = [ + 'delivery', + 'pickup-in-point', + 'pickup-nearby', + 'pickup', +] + +const useShippingActions = facet => { + const actionType = shippingActionTypes[facet.value] + const eventIdentifier = actionType ? eventIdentifiers[actionType] : null + + const [actionLabel, setActionLabel] = useState(placeHolders[actionType]) + const [isAddressSet, setIsAddressSet] = useState(false) + const [isPickupSet, setIsPickupSet] = useState(false) + + const isAddressDependent = + addressDependentValues.findIndex(value => facet.value === value) > -1 + + const isSSR = useSSR() + const { push } = usePixel() + + usePixelEventCallback({ + eventId: `shipping-option-${eventIdentifier}`, + handler: e => { + if (e?.data?.label) { + setActionLabel(e.data.label) + + if (isAddressDependent) { + setIsAddressSet(true) + } + + if (eventIdentifier === 'pickupPointLabel') { + setIsPickupSet(true) + } + } else { + setActionLabel(placeHolders[actionType]) + + if (isAddressDependent) { + setIsAddressSet(false) + } + + if (eventIdentifier === 'pickupPointLabel') { + setIsPickupSet(false) + } + } + }, + }) + + useEffect(() => { + if (!isSSR) { + return + } + + const windowLabel = window[eventIdentifier] + + if (windowLabel) { + setActionLabel(windowLabel) + + if (isAddressDependent) { + setIsAddressSet(true) + } + + if (eventIdentifier === 'pickupPointLabel') { + setIsPickupSet(true) + } + } else { + setActionLabel(placeHolders[actionType]) + + if (isAddressDependent) { + setIsAddressSet(false) + } + + if (eventIdentifier === 'pickupPointLabel') { + setIsPickupSet(false) + } + } + }, [actionType, eventIdentifier, isSSR, isAddressDependent]) + + const openDrawer = useCallback(() => { + push({ + id: drawerEvent[actionType], + }) + }, [actionType, push]) + + const shouldDisable = useShouldDisableFacet(facet, isAddressSet, isPickupSet) + + if (facet.value === 'pickup-nearby' || facet.value === 'pickup') { + return { + actionLabel: null, + actionType: null, + openDrawer: null, + shouldDisable, + } + } + + return { + actionType, + actionLabel, + openDrawer, + shouldDisable, + } +} + +export default useShippingActions diff --git a/react/hooks/useShouldDisableFacet.js b/react/hooks/useShouldDisableFacet.js index 759d53e42..e31ef9738 100644 --- a/react/hooks/useShouldDisableFacet.js +++ b/react/hooks/useShouldDisableFacet.js @@ -2,9 +2,30 @@ import { useSearchPage } from 'vtex.search-page-context/SearchPageContext' import { MAP_VALUES_SEP } from '../constants' -export default function useShouldDisableFacet(facet) { +export default function useShouldDisableFacet( + facet, + isAddressSet, + isPickupSet +) { const { map } = useSearchPage() + + if ( + (facet.value === 'delivery' || + facet.value === 'pickup-nearby' || + facet.value === 'pickup') && + !isAddressSet + ) { + return true + } + if (facet.value === 'pickup-in-point' && !isPickupSet) { + return true + } + + if(facet.quantity === 0) { + return true + } + if (!facet.selected || !map) { return false } diff --git a/react/searchResult.css b/react/searchResult.css index 63948a915..c2026798c 100644 --- a/react/searchResult.css +++ b/react/searchResult.css @@ -258,3 +258,18 @@ .notFound--layout { } + +.shippingActionButton { + border: 0; + padding: 0; + font-size: 14px; + background: unset; + cursor: pointer; + text-align: left; + font-weight: 500; + color: #134CD8; +} + +.scrim, .sidebar { + z-index: 999; +} diff --git a/react/typings/vtex.styleguide.d.ts b/react/typings/vtex.styleguide.d.ts index e2c82da55..e2c402bc6 100644 --- a/react/typings/vtex.styleguide.d.ts +++ b/react/typings/vtex.styleguide.d.ts @@ -2,4 +2,5 @@ declare module 'vtex.styleguide' { import type { ComponentType } from 'react' export const Spinner: ComponentType> + export const RadioGroup: ComponentType> } diff --git a/react/utils/compatibilityLayer.js b/react/utils/compatibilityLayer.js index c1f2d9f1c..00daff44e 100644 --- a/react/utils/compatibilityLayer.js +++ b/react/utils/compatibilityLayer.js @@ -54,7 +54,7 @@ export const buildSelectedFacetsAndFullText = (query, map, priceRange) => { } const addMap = facet => { - facet['map'] = facet.key + facet.map = facet.key if (facet.children) { facet.children.forEach(facetChild => addMap(facetChild)) @@ -79,6 +79,8 @@ export const detachFiltersByType = facets => { groupedFilters.TEXT || [] ) + const deliveries = groupedFilters.DELIVERY || [] + const categoriesTrees = pathOr( [], ['CATEGORYTREE', 0, 'facets'], @@ -102,6 +104,7 @@ export const detachFiltersByType = facets => { specificationFilters, categoriesTrees, priceRanges, + deliveries, } } diff --git a/react/utils/getFilters.js b/react/utils/getFilters.js index 1a25feb3a..d52a3aa88 100644 --- a/react/utils/getFilters.js +++ b/react/utils/getFilters.js @@ -1,20 +1,50 @@ import { path, contains, isEmpty } from 'ramda' +import { useIntl } from 'react-intl' +export const SHIPPING_TITLE = 'store/search.filter.title.shipping' export const CATEGORIES_TITLE = 'store/search.filter.title.categories' export const BRANDS_TITLE = 'store/search.filter.title.brands' export const PRICE_RANGES_TITLE = 'store/search.filter.title.price-ranges' +export const SHIPPING_OPTIONS = { + delivery: 'store/search.filter.shipping.name.delivery', + 'pickup-in-point': 'store/search.filter.shipping.name.pickup-in-point', + 'pickup-nearby': 'store/search.filter.shipping.name.pickup-nearby', + 'pickup-all': 'store/search.filter.shipping.name.pickup-all', +} + const BRANDS_TYPE = 'Brands' const PRICE_RANGES_TYPE = 'PriceRanges' const SPECIFICATION_FILTERS_TYPE = 'SpecificationFilters' +const SHIPPING_KEY = 'shipping' + +const shippingFacetDefault = { + name: SHIPPING_KEY, + type: 'DELIVERY', + hidden: false, + quantity: 0, + facets: Object.keys(SHIPPING_OPTIONS).map(option => ({ + id: null, + quantity: 0, + name: option, + key: SHIPPING_KEY, + selected: false, + map: SHIPPING_KEY, + value: option, + })), +} const getFilters = ({ specificationFilters = [], priceRanges = [], brands = [], + deliveries = [], brandsQuantity = 0, hiddenFacets = {}, + showShippingFacet = false, }) => { + const intl = useIntl() + const hiddenFacetsNames = ( path(['specificationFilters', 'hiddenFilters'], hiddenFacets) || [] ).map(filter => filter.name) @@ -34,7 +64,28 @@ const getFilters = ({ })) : [] + const deliveriesFormatted = getFormattedDeliveries( + deliveries, + showShippingFacet + ) + + const shippingIndex = deliveriesFormatted.findIndex( + d => d.name === SHIPPING_KEY + ) + + if (shippingIndex !== -1) { + deliveriesFormatted[shippingIndex] = { + ...deliveriesFormatted[shippingIndex], + title: SHIPPING_TITLE, + facets: deliveriesFormatted[shippingIndex].facets.map(facet => ({ + ...facet, + name: intl.formatMessage({ id: SHIPPING_OPTIONS[facet.name] }), + })), + } + } + return [ + ...deliveriesFormatted, ...mappedSpecificationFilters, !hiddenFacets.brands && !isEmpty(brands) && { @@ -52,4 +103,26 @@ const getFilters = ({ ].filter(Boolean) } +const getFormattedDeliveries = (deliveries, showShippingFacet) => { + if (!showShippingFacet) { + return deliveries.filter(d => d.name !== SHIPPING_KEY) + } + + const shippingFacet = deliveries.find(d => d.name === SHIPPING_KEY) + + if (!shippingFacet) { + return [...deliveries, shippingFacetDefault] + } + + const facetsNotIncluded = shippingFacetDefault.facets.filter(facet => + shippingFacet.facets.every(f => f.value !== facet.value) + ) + + shippingFacet.facets = [...shippingFacet.facets, ...facetsNotIncluded] + + return deliveries.map(facet => + facet.name === SHIPPING_KEY ? shippingFacet : facet + ) +} + export default getFilters