diff --git a/e2e/scripts/pageHelpers.js b/e2e/scripts/pageHelpers.js index 86c3fc51ff..1269e427e0 100644 --- a/e2e/scripts/pageHelpers.js +++ b/e2e/scripts/pageHelpers.js @@ -439,6 +439,7 @@ export const searchProduct = async ({page, query, isMobile = false}) => { let searchInput = isMobile ? searchInputs.nth(1) : searchInputs.nth(0) await searchInput.fill(query) + await page.waitForTimeout(1000) await searchInput.press('Enter') await page.waitForLoadState() diff --git a/packages/template-retail-react-app/CHANGELOG.md b/packages/template-retail-react-app/CHANGELOG.md index 87d48cbc16..f0204c6d94 100644 --- a/packages/template-retail-react-app/CHANGELOG.md +++ b/packages/template-retail-react-app/CHANGELOG.md @@ -1,4 +1,5 @@ ## v8.1.0-dev (Sep 04, 2025) +- Updated search UX - prices, images, suggestions new layout [#3271](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3271) - Updated the UI for StoreDisplay component which displays pickup in-store information on different pages. [#3248](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3248) - Added warning modal for guest users when toggling between multi ship and ship to one address. [3280] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3280) [3302] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3302) diff --git a/packages/template-retail-react-app/app/components/search/index.jsx b/packages/template-retail-react-app/app/components/search/index.jsx index bdd310ddfb..3257abd7f1 100644 --- a/packages/template-retail-react-app/app/components/search/index.jsx +++ b/packages/template-retail-react-app/app/components/search/index.jsx @@ -23,7 +23,6 @@ import SearchSuggestions from '@salesforce/retail-react-app/app/components/searc import {SearchIcon} from '@salesforce/retail-react-app/app/components/icons' import { capitalize, - boldString, getSessionJSONItem, setSessionJSONItem } from '@salesforce/retail-react-app/app/utils/utils' @@ -50,7 +49,7 @@ function isAskAgentOnSearchEnabled(enabled, askAgentOnSearch) { return enabled === 'true' && askAgentOnSearch === 'true' && onClient } -const formatSuggestions = (searchSuggestions, input) => { +const formatSuggestions = (searchSuggestions) => { return { categorySuggestions: searchSuggestions?.categorySuggestions?.categories?.map( (suggestion) => { @@ -58,7 +57,9 @@ const formatSuggestions = (searchSuggestions, input) => { type: 'category', id: suggestion.id, link: categoryUrlBuilder({id: suggestion.id}), - name: boldString(suggestion.name, capitalize(input)) + name: capitalize(suggestion.name), + image: suggestion.image?.disBaseLink, + parentCategoryName: suggestion.parentCategoryName } } ), @@ -68,19 +69,30 @@ const formatSuggestions = (searchSuggestions, input) => { currency: product.currency, price: product.price, productId: product.productId, - name: boldString(product.productName, capitalize(input)), - link: productUrlBuilder({id: product.productId}) + name: capitalize(product.productName), + link: productUrlBuilder({id: product.productId}), + image: product.image?.disBaseLink } }), - phraseSuggestions: searchSuggestions?.categorySuggestions?.suggestedPhrases?.map( + brandSuggestions: searchSuggestions?.brandSuggestions?.suggestedPhrases?.map((brand) => { + // Init cap the brand name + return { + type: 'brand', + name: capitalize(brand.phrase), + link: searchUrlBuilder(brand.phrase) + } + }), + phraseSuggestions: searchSuggestions?.productSuggestions?.suggestedPhrases?.map( (phrase) => { return { type: 'phrase', - name: boldString(phrase.phrase, capitalize(input)), - link: searchUrlBuilder(phrase.phrase) + name: phrase.phrase, + link: searchUrlBuilder(phrase.phrase), + exactMatch: phrase.exactMatch } } - ) + ), + searchPhrase: searchSuggestions?.searchPhrase } } @@ -102,10 +114,12 @@ const Search = (props) => { const [isOpen, setIsOpen] = useState(false) const [searchQuery, setSearchQuery] = useState('') const navigate = useNavigation() + const searchSuggestion = useSearchSuggestions( { parameters: { - q: searchQuery + q: searchQuery, + expand: 'images,prices' } }, { @@ -119,7 +133,7 @@ const Search = (props) => { }) const recentSearches = getSessionJSONItem(RECENT_SEARCH_KEY) const searchSuggestions = useMemo( - () => formatSuggestions(searchSuggestion.data, searchInputRef?.current?.value), + () => formatSuggestions(searchSuggestion.data), [searchSuggestion] ) @@ -257,7 +271,7 @@ const Search = (props) => { // or we have search suggestions available and have inputed some text (empty text in this scenario should show recent searches) if ( (document.activeElement.id === 'search-input' && recentSearches?.length > 0) || - (searchSuggestionsAvailable && searchInputRef.current.value.length > 0) + (searchSuggestionsAvailable && searchInputRef.current?.value?.length > 0) ) { setIsOpen(true) } else { @@ -310,7 +324,15 @@ const Search = (props) => { - + { const suggestionPopoverEl = await screen.getByTestId('sf-suggestion-popover') await waitFor(() => { - const suggestionsEl = within(suggestionPopoverEl).getByTestId('sf-suggestion') - expect(suggestionsEl.querySelector('button').textContent).toBe('Dresses') + const suggestionsEls = within(suggestionPopoverEl).getAllByTestId('sf-suggestion') + expect(suggestionsEls.length).toBeGreaterThan(0) + const hasDressesSuggestion = suggestionsEls.some((el) => + el.querySelector('button')?.textContent?.includes('Dresses') + ) + expect(hasDressesSuggestion).toBe(true) }) }) @@ -423,3 +427,96 @@ test('when sendTextMessage and launchChat both fail, no additional send text is // Verify sendTextMessage was only called once expect(sendTextMessageSpy).toHaveBeenCalledTimes(1) }) + +test('handles search phrase in formatSuggestions', async () => { + const user = setupUserEvent() + + const mockResultsWithPhrase = { + ...mockSearchResults, + searchPhrase: 'test search phrase' + } + + global.server.use( + rest.get('*/search-suggestions', (req, res, ctx) => { + return res(ctx.delay(0), ctx.status(200), ctx.json(mockResultsWithPhrase)) + }) + ) + + renderWithProviders() + const searchInput = document.querySelector('input[type="search"]') + + await user.type(searchInput, 'test') + + // Wait for suggestions to load with search phrase + await waitFor(() => { + expect(screen.getByTestId('sf-suggestion-popover')).toBeInTheDocument() + }) +}) + +test('handles phrase suggestions in formatSuggestions', async () => { + const user = setupUserEvent() + + // Mock search results with phrase suggestions + const mockResultsWithPhrases = { + ...mockSearchResults, + productSuggestions: { + ...mockSearchResults.productSuggestions, + suggestedPhrases: [ + {phrase: 'running shoes', exactMatch: true}, + {phrase: 'athletic wear', exactMatch: false} + ] + } + } + + global.server.use( + rest.get('*/search-suggestions', (req, res, ctx) => { + return res(ctx.delay(0), ctx.status(200), ctx.json(mockResultsWithPhrases)) + }) + ) + + renderWithProviders() + const searchInput = document.querySelector('input[type="search"]') + + await user.type(searchInput, 'running') + + // Wait for suggestions to load with phrase suggestions + await waitFor(() => { + expect(screen.getByTestId('sf-suggestion-popover')).toBeInTheDocument() + }) +}) + +test('handles product suggestions with images', async () => { + const user = setupUserEvent() + + // Mock search results with product suggestions that have images + const mockResultsWithProductImages = { + ...mockSearchResults, + productSuggestions: { + ...mockSearchResults.productSuggestions, + products: [ + { + ...mockSearchResults.productSuggestions.products[0], + image: { + disBaseLink: 'https://example.com/product-image.jpg' + } + } + ] + } + } + + global.server.use( + rest.get('*/search-suggestions', (req, res, ctx) => { + return res(ctx.delay(0), ctx.status(200), ctx.json(mockResultsWithProductImages)) + }) + ) + + renderWithProviders() + const searchInput = document.querySelector('input[type="search"]') + + await user.type(searchInput, 'Dress') + + // Wait for suggestions to load with product images + await waitFor(() => { + expect(screen.getByTestId('sf-suggestion-popover')).toBeInTheDocument() + }) +}) diff --git a/packages/template-retail-react-app/app/components/search/partials/horizontal-suggestions.jsx b/packages/template-retail-react-app/app/components/search/partials/horizontal-suggestions.jsx new file mode 100644 index 0000000000..4e693394ec --- /dev/null +++ b/packages/template-retail-react-app/app/components/search/partials/horizontal-suggestions.jsx @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2021, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import React from 'react' +import PropTypes from 'prop-types' +import {Text, Box, Flex, AspectRatio} from '@salesforce/retail-react-app/app/components/shared/ui' +import DynamicImage from '@salesforce/retail-react-app/app/components/dynamic-image' +import Link from '@salesforce/retail-react-app/app/components/link' +import {useStyleConfig} from '@chakra-ui/react' + +const HorizontalSuggestions = ({suggestions, closeAndNavigate, dynamicImageProps}) => { + const styles = useStyleConfig('HorizontalSuggestions') + + if (!suggestions) { + return null + } + + return ( + + + {suggestions.map((suggestion, idx) => ( + closeAndNavigate(suggestion.link)} + > + + {/* Product Image */} + + {suggestion.image ? ( + + + + ) : null} + + + {suggestion.name} + + {suggestion.price && ( + ${suggestion.price} + )} + + + ))} + + + ) +} + +HorizontalSuggestions.propTypes = { + suggestions: PropTypes.array, + closeAndNavigate: PropTypes.func, + dynamicImageProps: PropTypes.object +} + +export default HorizontalSuggestions diff --git a/packages/template-retail-react-app/app/components/search/partials/horizontal-suggestions.test.jsx b/packages/template-retail-react-app/app/components/search/partials/horizontal-suggestions.test.jsx new file mode 100644 index 0000000000..77750aa5db --- /dev/null +++ b/packages/template-retail-react-app/app/components/search/partials/horizontal-suggestions.test.jsx @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2021, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import React from 'react' +import {screen} from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils' +import HorizontalSuggestions from '@salesforce/retail-react-app/app/components/search/partials/horizontal-suggestions' + +jest.mock('@salesforce/retail-react-app/app/components/dynamic-image', () => { + return function MockDynamicImage(props) { + const {src, widths, imageProps} = props || {} + return ( + {imageProps?.alt + ) + } +}) + +const sampleSuggestions = [ + { + link: '/product-1', + image: 'https://example.com/image-1.jpg', + name: 'Product 1', + price: '29.99' + }, + { + link: '/product-2', + name: 'Product 2' + } +] + +test('returns null when suggestions are undefined', () => { + renderWithProviders( + + ) + expect(screen.queryByTestId('sf-horizontal-product-suggestions')).not.toBeInTheDocument() +}) + +test('renders product tiles with names and optional prices', () => { + renderWithProviders( + + ) + + // container + expect(screen.getByTestId('sf-horizontal-product-suggestions')).toBeInTheDocument() + + // tiles + const tiles = screen.getAllByTestId('product-tile') + expect(tiles).toHaveLength(2) + + // names + expect(screen.getByText('Product 1')).toBeInTheDocument() + expect(screen.getByText('Product 2')).toBeInTheDocument() + + // price only for first suggestion + expect(screen.getByText('$29.99')).toBeInTheDocument() +}) + +test('renders DynamicImage when image is provided and passes widths via dynamicImageProps', () => { + const dynamicImageProps = {widths: [200, 400, 800]} + renderWithProviders( + + ) + + const images = screen.getAllByTestId('dynamic-image') + // Only first suggestion has an image + expect(images).toHaveLength(1) + + const img = images[0] + // src should be appended with the width token by the component + expect(img.getAttribute('data-src')).toBe(`${sampleSuggestions[0].image}[?sw={width}&q=60]`) + expect(img.getAttribute('data-widths')).toBe('200,400,800') +}) + +test('does not render DynamicImage when image is absent', () => { + renderWithProviders( + + ) + + expect(screen.queryByTestId('dynamic-image')).not.toBeInTheDocument() +}) + +test('clicking a product tile triggers closeAndNavigate with the link', async () => { + const user = userEvent.setup() + const closeAndNavigate = jest.fn() + + renderWithProviders( + + ) + + const tiles = screen.getAllByTestId('product-tile') + await user.click(tiles[1]) + + expect(closeAndNavigate).toHaveBeenCalledWith(sampleSuggestions[1].link) +}) diff --git a/packages/template-retail-react-app/app/components/search/partials/search-suggestions-section.jsx b/packages/template-retail-react-app/app/components/search/partials/search-suggestions-section.jsx new file mode 100644 index 0000000000..2723f7bf52 --- /dev/null +++ b/packages/template-retail-react-app/app/components/search/partials/search-suggestions-section.jsx @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2021, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import React, {Fragment} from 'react' +import PropTypes from 'prop-types' +import {Box} from '@salesforce/retail-react-app/app/components/shared/ui' +import Suggestions from '@salesforce/retail-react-app/app/components/search/partials/suggestions' +import HorizontalSuggestions from '@salesforce/retail-react-app/app/components/search/partials/horizontal-suggestions' +import {FormattedMessage} from 'react-intl' +import {HideOnDesktop, HideOnMobile} from '@salesforce/retail-react-app/app/components/responsive' +import Link from '@salesforce/retail-react-app/app/components/link' +import {searchUrlBuilder} from '@salesforce/retail-react-app/app/utils/url' + +const SuggestionSection = ({searchSuggestions, closeAndNavigate, styles}) => { + const hasCategories = searchSuggestions?.categorySuggestions?.length + const hasProducts = searchSuggestions?.productSuggestions?.length + const hasPhraseSuggestions = searchSuggestions?.phraseSuggestions?.length + + return ( + + {/* Mobile - Vertical alignment */} + + {hasPhraseSuggestions && + searchSuggestions?.phraseSuggestions[0].exactMatch === false && ( + + + + + {' ' + searchSuggestions?.phraseSuggestions[0].name + '?'} + + + + )} + {hasCategories && ( + + + + + + + )} + {hasProducts && ( + + + + + + + )} + + {/* Desktop - Vertical and Horizontal alignment */} + + + + {hasPhraseSuggestions && + searchSuggestions?.phraseSuggestions[0].exactMatch === false && ( + + + + + {' ' + + searchSuggestions?.phraseSuggestions[0].name + + '?'} + + + + )} + {hasCategories && ( + + + + + + + )} + + + {hasProducts && ( + + + + )} + + + {hasProducts && ( + + + + + + + + )} + + + + + ) +} + +SuggestionSection.propTypes = { + searchSuggestions: PropTypes.object.isRequired, + closeAndNavigate: PropTypes.func.isRequired, + styles: PropTypes.object.isRequired +} + +export default SuggestionSection diff --git a/packages/template-retail-react-app/app/components/search/partials/search-suggestions-section.test.jsx b/packages/template-retail-react-app/app/components/search/partials/search-suggestions-section.test.jsx new file mode 100644 index 0000000000..3f0a3a5933 --- /dev/null +++ b/packages/template-retail-react-app/app/components/search/partials/search-suggestions-section.test.jsx @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2021, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import React from 'react' +import {screen, within} from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils' +import SuggestionSection from '@salesforce/retail-react-app/app/components/search/partials/search-suggestions-section' + +// Mock dynamic image to keep DOM simple when HorizontalSuggestions renders +jest.mock('@salesforce/retail-react-app/app/components/dynamic-image', () => { + return function MockDynamicImage(props) { + const {src, widths, imageProps} = props || {} + return ( + {imageProps?.alt + ) + } +}) + +const baseStyles = { + textContainer: {}, + sectionHeader: {}, + phraseContainer: {} +} + +const makeSearchSuggestions = (overrides = {}) => ({ + searchPhrase: 'Dress', + phraseSuggestions: [], + categorySuggestions: [], + productSuggestions: [], + ...overrides +}) + +test('renders "Did you mean" with suggestion link when non-exact phrase exists (mobile and desktop)', () => { + const searchSuggestions = makeSearchSuggestions({ + phraseSuggestions: [{name: 'dresses', link: '/search?q=dresses', exactMatch: false}] + }) + + renderWithProviders( + + ) + + // Appears in both mobile and desktop sections + const didYouMeanTexts = screen.getAllByText(/Did you mean/i) + expect(didYouMeanTexts.length).toBeGreaterThanOrEqual(1) + + const links = screen.getAllByRole('link', {name: /dresses\?/i}) + expect(links.length).toBeGreaterThanOrEqual(1) +}) + +test('renders Categories header and category suggestions', () => { + const searchSuggestions = makeSearchSuggestions({ + categorySuggestions: [ + {type: 'category', name: 'Women', link: '/women'}, + {type: 'category', name: 'Men', link: '/men'} + ] + }) + + renderWithProviders( + + ) + + // Header present (could be duplicated for mobile/desktop) + expect(screen.getAllByText('Categories').length).toBeGreaterThanOrEqual(1) + + // Suggestions component renders buttons; ensure names are present + expect(screen.getAllByText('Women').length).toBeGreaterThanOrEqual(1) + expect(screen.getAllByText('Men').length).toBeGreaterThanOrEqual(1) +}) + +test('renders horizontal product suggestions and "View All"; clicking a tile calls closeAndNavigate', async () => { + const user = userEvent.setup() + const closeAndNavigate = jest.fn() + + const searchSuggestions = makeSearchSuggestions({ + productSuggestions: [ + { + type: 'product', + name: 'Product 1', + link: '/p1', + image: 'https://example.com/p1.jpg', + price: '19.99' + }, + {type: 'product', name: 'Product 2', link: '/p2'} + ] + }) + + renderWithProviders( + + ) + + // HorizontalSuggestions container + expect(screen.getByTestId('sf-horizontal-product-suggestions')).toBeInTheDocument() + + // "View All" link only renders when products exist (may be hidden by responsive wrapper in tests) + expect(screen.getByText(/View All/i, {selector: 'a'})).toBeInTheDocument() + + // Click a product tile (desktop horizontal suggestions) + const container = screen.getByTestId('sf-horizontal-product-suggestions') + const tiles = within(container).getAllByTestId('product-tile') + await user.click(tiles[1]) + expect(closeAndNavigate).toHaveBeenCalledWith('/p2') +}) + +test('renders nothing when there are no categories, products, or phrase suggestions', () => { + const searchSuggestions = makeSearchSuggestions() + + renderWithProviders( + + ) + + expect(screen.queryByText('Categories')).not.toBeInTheDocument() + expect(screen.queryByText('Products')).not.toBeInTheDocument() + expect(screen.queryByTestId('sf-horizontal-product-suggestions')).not.toBeInTheDocument() +}) diff --git a/packages/template-retail-react-app/app/components/search/partials/search-suggestions.jsx b/packages/template-retail-react-app/app/components/search/partials/search-suggestions.jsx index 29c5c14067..c09ca93abe 100644 --- a/packages/template-retail-react-app/app/components/search/partials/search-suggestions.jsx +++ b/packages/template-retail-react-app/app/components/search/partials/search-suggestions.jsx @@ -4,28 +4,27 @@ * SPDX-License-Identifier: BSD-3-Clause * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import React, {Fragment} from 'react' +import React from 'react' import PropTypes from 'prop-types' -import {Stack} from '@salesforce/retail-react-app/app/components/shared/ui' +import {Stack, useMultiStyleConfig} from '@salesforce/retail-react-app/app/components/shared/ui' import RecentSearches from '@salesforce/retail-react-app/app/components/search/partials/recent-searches' -import Suggestions from '@salesforce/retail-react-app/app/components/search/partials/suggestions' +import SuggestionSection from '@salesforce/retail-react-app/app/components/search/partials/search-suggestions-section' const SearchSuggestions = ({recentSearches, searchSuggestions, closeAndNavigate}) => { - const useSuggestions = searchSuggestions && searchSuggestions?.categorySuggestions?.length + const styles = useMultiStyleConfig('SearchSuggestions') + const hasCategories = searchSuggestions?.categorySuggestions?.length + const hasProducts = searchSuggestions?.productSuggestions?.length + const hasBrands = searchSuggestions?.brandSuggestions?.length + const hasSuggestions = hasCategories || hasProducts || hasBrands + return ( - - {useSuggestions ? ( - - - {/* */} - {/* */} - + + {hasSuggestions ? ( + ) : ( { + const styles = useMultiStyleConfig('SearchSuggestions') + if (!suggestions) { return null } return ( - - + + {suggestions.map((suggestion, idx) => ( ))} diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json index 0066215b23..c40aaaca4c 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json @@ -3419,6 +3419,30 @@ "value": "Cancel" } ], + "search.suggestions.categories": [ + { + "type": 0, + "value": "Categories" + } + ], + "search.suggestions.didYouMean": [ + { + "type": 0, + "value": "Did you mean" + } + ], + "search.suggestions.products": [ + { + "type": 0, + "value": "Products" + } + ], + "search.suggestions.viewAll": [ + { + "type": 0, + "value": "View All" + } + ], "selected_refinements.action.assistive_msg.clear_all": [ { "type": 0, diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json index 0066215b23..c40aaaca4c 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json @@ -3419,6 +3419,30 @@ "value": "Cancel" } ], + "search.suggestions.categories": [ + { + "type": 0, + "value": "Categories" + } + ], + "search.suggestions.didYouMean": [ + { + "type": 0, + "value": "Did you mean" + } + ], + "search.suggestions.products": [ + { + "type": 0, + "value": "Products" + } + ], + "search.suggestions.viewAll": [ + { + "type": 0, + "value": "View All" + } + ], "selected_refinements.action.assistive_msg.clear_all": [ { "type": 0, diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json index ee8e0a6ef5..fab642170b 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json @@ -7219,6 +7219,62 @@ "value": "]" } ], + "search.suggestions.categories": [ + { + "type": 0, + "value": "[" + }, + { + "type": 0, + "value": "Ƈȧȧŧḗḗɠǿǿřīḗḗş" + }, + { + "type": 0, + "value": "]" + } + ], + "search.suggestions.didYouMean": [ + { + "type": 0, + "value": "[" + }, + { + "type": 0, + "value": "Ḓīḓ ẏǿǿŭŭ ḿḗḗȧȧƞ" + }, + { + "type": 0, + "value": "]" + } + ], + "search.suggestions.products": [ + { + "type": 0, + "value": "[" + }, + { + "type": 0, + "value": "Ƥřǿǿḓŭŭƈŧş" + }, + { + "type": 0, + "value": "]" + } + ], + "search.suggestions.viewAll": [ + { + "type": 0, + "value": "[" + }, + { + "type": 0, + "value": "Ṽīḗḗẇ Ȧŀŀ" + }, + { + "type": 0, + "value": "]" + } + ], "selected_refinements.action.assistive_msg.clear_all": [ { "type": 0, diff --git a/packages/template-retail-react-app/app/theme/components/project/horizontal-suggestions.js b/packages/template-retail-react-app/app/theme/components/project/horizontal-suggestions.js new file mode 100644 index 0000000000..2473bb1313 --- /dev/null +++ b/packages/template-retail-react-app/app/theme/components/project/horizontal-suggestions.js @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2021, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +export default { + baseStyle: { + container: { + // Main container for horizontal suggestions + }, + flexContainer: { + gap: 4, + overflowX: 'auto', + pb: 2 + }, + suggestionItem: { + width: { + base: '50vw', + md: '50vw', + lg: '10vw' + }, + flex: '0 0 auto' + }, + imageContainer: { + mb: 2 + }, + aspectRatio: { + ratio: 1 + }, + dynamicImage: { + height: '100%', + width: '100%', + '& picture': { + display: 'block', + height: '100%', + width: '100%' + }, + '& img': { + display: 'block', + height: '100%', + width: '100%', + objectFit: 'cover' + } + }, + productName: { + fontSize: 'sm', + fontWeight: 'medium', + color: 'gray.900', + mb: 1, + noOfLines: 2 + }, + productPrice: { + fontSize: 'sm', + color: 'gray.900', + fontWeight: 'medium' + } + } +} diff --git a/packages/template-retail-react-app/app/theme/components/project/search-suggestions.js b/packages/template-retail-react-app/app/theme/components/project/search-suggestions.js new file mode 100644 index 0000000000..f64d7dd245 --- /dev/null +++ b/packages/template-retail-react-app/app/theme/components/project/search-suggestions.js @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2021, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +export default { + baseStyle: { + container: { + padding: 6, + spacing: 0 + }, + sectionHeader: { + fontWeight: 200, + margin: '2 0 1 0', + paddingLeft: 12, + color: 'gray.500', + fontSize: 'sm', + lineHeight: 1.2 + }, + phraseContainer: { + margin: '2 0 1 0', + paddingLeft: 12 + }, + suggestionsContainer: { + spacing: 0 + }, + suggestionsBox: { + mx: '-16px' + // borderBottom: '1px solid', + // borderColor: 'gray.200' + }, + suggestionButton: { + width: 'full', + fontSize: 'md', + marginTop: 0, + variant: 'menu-link', + style: { + justifyContent: 'flex-start', + padding: '8px 12px' + } + }, + imageContainer: { + width: 10, + height: 10, + marginRight: 4, + borderRadius: 'full', + background: 'transparent', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden' + }, + suggestionImage: { + boxSize: 10, + borderRadius: 'full', + objectFit: 'cover', + background: '#f3f3f3' + }, + textContainer: { + textAlign: 'left' + }, + suggestionName: { + fontWeight: '500', + as: 'span' + }, + brandName: { + fontWeight: '700', + as: 'span' + }, + categoryParent: { + as: 'span', + color: 'gray.500', + fontSize: 'sm' + }, + badgeGroup: { + position: 'absolute', + top: 2, + left: 2 + } + } +} diff --git a/packages/template-retail-react-app/app/theme/index.js b/packages/template-retail-react-app/app/theme/index.js index df2077f17a..5da80d9ecb 100644 --- a/packages/template-retail-react-app/app/theme/index.js +++ b/packages/template-retail-react-app/app/theme/index.js @@ -50,6 +50,8 @@ import ProductTile from '@salesforce/retail-react-app/app/theme/components/proje import SocialIcons from '@salesforce/retail-react-app/app/theme/components/project/social-icons' import SwatchGroup from '@salesforce/retail-react-app/app/theme/components/project/swatch-group' import ImageGallery from '@salesforce/retail-react-app/app/theme/components/project/image-gallery' +import SearchSuggestions from '@salesforce/retail-react-app/app/theme/components/project/search-suggestions' +import HorizontalSuggestions from '@salesforce/retail-react-app/app/theme/components/project/horizontal-suggestions' // Please refer to the Chakra-Ui theme customization docs found // here https://chakra-ui.com/docs/theming/customize-theme to learn @@ -97,7 +99,9 @@ export const overrides = { Pagination, ProductTile, SwatchGroup, - ImageGallery + ImageGallery, + SearchSuggestions, + HorizontalSuggestions } } diff --git a/packages/template-retail-react-app/package.json b/packages/template-retail-react-app/package.json index 9fc34957ec..22406f8a1d 100644 --- a/packages/template-retail-react-app/package.json +++ b/packages/template-retail-react-app/package.json @@ -100,7 +100,7 @@ "bundlesize": [ { "path": "build/main.js", - "maxSize": "66 kB" + "maxSize": "67 kB" }, { "path": "build/vendor.js", diff --git a/packages/template-retail-react-app/translations/en-GB.json b/packages/template-retail-react-app/translations/en-GB.json index 8c23e59240..34c762dfd3 100644 --- a/packages/template-retail-react-app/translations/en-GB.json +++ b/packages/template-retail-react-app/translations/en-GB.json @@ -1439,6 +1439,18 @@ "search.action.cancel": { "defaultMessage": "Cancel" }, + "search.suggestions.categories": { + "defaultMessage": "Categories" + }, + "search.suggestions.didYouMean": { + "defaultMessage": "Did you mean" + }, + "search.suggestions.products": { + "defaultMessage": "Products" + }, + "search.suggestions.viewAll": { + "defaultMessage": "View All" + }, "selected_refinements.action.assistive_msg.clear_all": { "defaultMessage": "Clear all filters" }, diff --git a/packages/template-retail-react-app/translations/en-US.json b/packages/template-retail-react-app/translations/en-US.json index 8c23e59240..34c762dfd3 100644 --- a/packages/template-retail-react-app/translations/en-US.json +++ b/packages/template-retail-react-app/translations/en-US.json @@ -1439,6 +1439,18 @@ "search.action.cancel": { "defaultMessage": "Cancel" }, + "search.suggestions.categories": { + "defaultMessage": "Categories" + }, + "search.suggestions.didYouMean": { + "defaultMessage": "Did you mean" + }, + "search.suggestions.products": { + "defaultMessage": "Products" + }, + "search.suggestions.viewAll": { + "defaultMessage": "View All" + }, "selected_refinements.action.assistive_msg.clear_all": { "defaultMessage": "Clear all filters" },