From 355eb0a5bd133d01252434ad3e9f5bda2f19bc7d Mon Sep 17 00:00:00 2001 From: "Filipe W. Lima" Date: Wed, 8 Jun 2022 15:02:22 -0300 Subject: [PATCH 01/11] feat: Add search history & top searches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A cherry-pick from the Gatsby's merge commit of this same feature. * Skip rendering the history if there isn't any * Clear the history when its button is clicked * Stop trying to open the link in a new tab In a previous design spec we opened the links in a new tab but we are not doing this anymore * Remove onClear prop, does not seem to be needed * Show the search history component * Show up to 4 search history items max The design spec mentions 4 max: https://www.figma.com/file/kJywgfIx8LBIzggCY5AeDy/Handoff-v3.1-(Search)?node-id=2%3A58234 * Add the top searches * Extract a `formatSearchPath` util function * Use a link instead of button for the suggested terms Because the `href` is known so we can use links and not navigate programmatically with buttons. * Fix search test I had removed the `navigate` call because it wasn't needed for the links, but it's also used programmatically to trigger the search in the input. # Please enter the commit message for your changes. Lines starting * Fix SonarQube warning * Create a context so the search dropdown can be closed When terms, history, top search, products are clicked. * Consider `closeSearchInputDropdown` to be null Missed including this in the previous commit. * Fix SonarQube warning * Rename button from Clear to Clear History * Adjust Loading text message spacing * Fix link color for Top Search * Fix grayish clock icon color in search history * Limit search to 5 items per category Noticed sometimes we were showing +5 and, apparently, the `first` pagination doesn't really filter the results. * Also update the search input with the selected option * Fix suggested terms link color * Add the magnifying glass icon to suggested terms * Run TS Hero extension to organize imports of touched files * Better handle search input selections Whenever a selection is made, be it from History, Top Search, Suggested Terms or Products, use the same logic: - Add term to the history - Send term to analytics - Close dropdown - Update the input with the term * Do not show an empty gap when there are not results * Move `SearchInputContext` and `useSearchInput` to `sdk/search/…` * Fix warning about `SearchHistoryProps` type export * Fix overflow-x when expanding the search on mobile * Fix React key warning * Debounce the search suggestions * Add entries to the changelog * Adjust `SuggestionsTopSearch` after the merged changes * Fix Storybook search stories * Simplify top search querying by using `useQuery` Address [this suggestion](https://github.com/vtex-sites/gatsby.store/pull/67#discussion_r882857670). * Remove debounce package in favor of using `useDeferredValue` from React Address [this comment](https://github.com/vtex-sites/gatsby.store/pull/67#discussion_r882856990): > can't we use React18 features like useDefferedValue to not need to implement a debounce function? * Let TypeScript be aware of new types from React 18 So it doesn't complain when we use the new features (e.g., `useDeferredValue`). * Let search history also record the path Instead of using the generic search path for all the searched terms, along with the searched term, also store its path. This way, if a suggested product is selected, we store its name and its PDP path. * Better organize by extracting `useTopSearch` to its own file Address this comment: https://github.com/vtex-sites/gatsby.store/pull/67#discussion_r887397506 * Better organize by extracting `useSuggestions` to its own file Address this comment: https://github.com/vtex-sites/gatsby.store/pull/67#discussion_r887397864 * Fix the missing search input (left) padding Pointed out by this comment: https://github.com/vtex-sites/gatsby.store/pull/67#pullrequestreview-992765808 > should we add padding-left inside the input? What happened was that the padding was being overwritten by the styles when the search input is minimized. --- Conflicts: - CHANGELOG.md - src/components/common/SearchInput/SearchInput.tsx - src/components/search/History/SearchHistory.tsx - src/components/search/SuggestionProductCard/SuggestionProductCard.tsx - src/components/search/Suggestions/Suggestions.tsx - src/components/search/Suggestions/SuggestionsTopSearch.tsx - src/components/ui/Search/Suggestions/Suggestions.stories.tsx - src/components/ui/Search/Suggestions/Suggestions.tsx --- src/components/common/Navbar/navbar.scss | 2 +- .../common/SearchInput/SearchInput.tsx | 110 +++++++++--------- .../common/SearchInput/search-input.scss | 8 ++ .../search/History/SearchHistory.stories.tsx | 30 +++-- .../search/History/SearchHistory.tsx | 37 +++--- .../SuggestionProductCard.tsx | 20 +++- .../suggestion-product-card.scss | 7 ++ .../search/Suggestions/Suggestions.tsx | 81 +++---------- .../SuggestionsTopSearch.stories.tsx | 22 ++-- .../Suggestions/SuggestionsTopSearch.tsx | 37 ++++-- .../search/Suggestions/suggestions.scss | 30 ++--- .../Suggestions/Suggestions.stories.tsx | 11 +- .../ui/Search/Suggestions/Suggestions.tsx | 38 +++--- src/sdk/search/useSearchHistory.ts | 24 ++-- src/sdk/search/useSearchInput.tsx | 43 +++++++ src/sdk/search/useSuggestions.ts | 48 ++++++++ src/sdk/search/useTopSearch.ts | 43 +++++++ tsconfig.json | 1 + 18 files changed, 371 insertions(+), 221 deletions(-) create mode 100644 src/sdk/search/useSearchInput.tsx create mode 100644 src/sdk/search/useSuggestions.ts create mode 100644 src/sdk/search/useTopSearch.ts diff --git a/src/components/common/Navbar/navbar.scss b/src/components/common/Navbar/navbar.scss index dbd12b4f..293a80be 100644 --- a/src/components/common/Navbar/navbar.scss +++ b/src/components/common/Navbar/navbar.scss @@ -76,7 +76,7 @@ order: 0; width: min-content; - [data-store-input] { + &[data-store-search-input-dropdown-open="false"] [data-store-input] { width: 0; padding: 0; border-width: 0; diff --git a/src/components/common/SearchInput/SearchInput.tsx b/src/components/common/SearchInput/SearchInput.tsx index 93990f5e..3e499b5d 100644 --- a/src/components/common/SearchInput/SearchInput.tsx +++ b/src/components/common/SearchInput/SearchInput.tsx @@ -1,17 +1,13 @@ -import { - formatSearchState, - initSearchState, - sendAnalyticsEvent, -} from '@faststore/sdk' +import { sendAnalyticsEvent } from '@faststore/sdk' import { SearchInput as UISearchInput } from '@faststore/ui' import { useRouter } from 'next/router' import { forwardRef, lazy, Suspense, - useCallback, useRef, useState, + useDeferredValue, } from 'react' import type { SearchEvent } from '@faststore/sdk' import type { @@ -21,6 +17,11 @@ import type { import Icon from 'src/components/ui/Icon' import useSearchHistory from 'src/sdk/search/useSearchHistory' +import { + formatSearchPath, + SearchInputProvider, +} from 'src/sdk/search/useSearchInput' +import type { SearchInputContextValue } from 'src/sdk/search/useSearchInput' import useOnClickOutside from 'src/sdk/ui/useOnClickOutside' const Suggestions = lazy(() => import('src/components/search/Suggestions')) @@ -30,30 +31,11 @@ declare type SearchInputProps = { buttonTestId?: string } & Omit -const useSearchHandler = (callback: (term: string) => void) => { - const router = useRouter() - const { addToSearchHistory } = useSearchHistory() - - return useCallback( - (term: string) => { - const { pathname, search } = formatSearchState( - initSearchState({ - term, - base: '/s', - }) - ) - - sendAnalyticsEvent({ - name: 'search', - params: { search_term: term }, - }) - - addToSearchHistory(term) - callback(term) - router.push(`${pathname}${search}`) - }, - [addToSearchHistory, callback, router] - ) +const sendAnalytics = async (term: string) => { + sendAnalyticsEvent({ + name: 'search', + params: { search_term: term }, + }) } const SearchInput = forwardRef( @@ -62,12 +44,19 @@ const SearchInput = forwardRef( ref ) { const [searchQuery, setSearchQuery] = useState('') + const searchQueryDeferred = useDeferredValue(searchQuery) const [suggestionsOpen, setSuggestionsOpen] = useState(false) const searchRef = useRef(null) - const handleSearch = useSearchHandler((term: string) => { - setSuggestionsOpen(false) - setSearchQuery(term) - }) + const { addToSearchHistory } = useSearchHistory() + const router = useRouter() + + const onSearchInputSelection: SearchInputContextValue['onSearchInputSelection'] = + (term, path) => { + addToSearchHistory({ term, path }) + sendAnalytics(term) + setSuggestionsOpen(false) + setSearchQuery(term) + } useOnClickOutside(searchRef, () => setSuggestionsOpen(false)) @@ -77,29 +66,36 @@ const SearchInput = forwardRef( data-store-search-input-wrapper data-store-search-input-dropdown-open={suggestionsOpen} > - - } - placeholder="Search everything at the store" - onChange={(e) => setSearchQuery(e.target.value)} - onSubmit={handleSearch} - onFocus={() => setSuggestionsOpen(true)} - value={searchQuery} - {...props} - /> - {suggestionsOpen && ( - -
- -
-
- )} + + + } + placeholder="Search everything at the store" + onChange={(e) => setSearchQuery(e.target.value)} + onSubmit={(term) => { + const path = formatSearchPath(term) + + onSearchInputSelection(term, path) + router.push(path) + }} + onFocus={() => setSuggestionsOpen(true)} + value={searchQuery} + {...props} + /> + {suggestionsOpen && ( + +
+ +
+
+ )} +
) } diff --git a/src/components/common/SearchInput/search-input.scss b/src/components/common/SearchInput/search-input.scss index 5e6e7448..c9a101c0 100644 --- a/src/components/common/SearchInput/search-input.scss +++ b/src/components/common/SearchInput/search-input.scss @@ -65,6 +65,10 @@ [data-store-search-input-wrapper] { position: relative; + + [data-fs-search-suggestion-section] + [data-fs-search-input-loading-text] { + padding-top: var(--fs-spacing-3); + } } [data-store-search-input-dropdown-wrapper] { @@ -90,6 +94,10 @@ padding: var(--fs-spacing-3); background-color: var(--fs-color-neutral-0); + &:empty { + display: none; + } + @include media(">=tablet") { top: calc(var(--top) - 1px); left: calc(var(--left) - var(--fs-spacing-1)); diff --git a/src/components/search/History/SearchHistory.stories.tsx b/src/components/search/History/SearchHistory.stories.tsx index 8bb9cd78..7ddbcdec 100644 --- a/src/components/search/History/SearchHistory.stories.tsx +++ b/src/components/search/History/SearchHistory.stories.tsx @@ -1,3 +1,5 @@ +import { SearchInputProvider } from 'src/sdk/search/useSearchInput' + import { SearchHistory } from '.' import type { SearchHistoryProps } from '.' @@ -6,18 +8,22 @@ const meta = { title: 'Organisms/Search/History', } -const Template = (props: SearchHistoryProps) => ( -
- -
-) +const Template = (props: SearchHistoryProps) => { + return ( +
+ + + +
+ ) +} export const Default = Template.bind({}) diff --git a/src/components/search/History/SearchHistory.tsx b/src/components/search/History/SearchHistory.tsx index b5e52f4f..cb8c16d9 100644 --- a/src/components/search/History/SearchHistory.tsx +++ b/src/components/search/History/SearchHistory.tsx @@ -1,46 +1,39 @@ -import { formatSearchState, initSearchState } from '@faststore/sdk' import { List as UIList } from '@faststore/ui' import Button from 'src/components/ui/Button' import Icon from 'src/components/ui/Icon' import Link from 'src/components/ui/Link' import useSearchHistory from 'src/sdk/search/useSearchHistory' +import useSearchInput from 'src/sdk/search/useSearchInput' +import type { History } from 'src/sdk/search/useSearchHistory' export interface SearchHistoryProps { - history?: string[] - onClear: () => void + history?: History[] } -const doSearch = (term: string) => { - const { pathname, search } = formatSearchState( - initSearchState({ - term, - base: '/s', - }) - ) - - return `${pathname}${search}` -} +const SearchHistory = ({ history = [] }: SearchHistoryProps) => { + const { onSearchInputSelection } = useSearchInput() + const { searchHistory, clearSearchHistory } = useSearchHistory(history) -const SearchHistory = ({ history = [], onClear }: SearchHistoryProps) => { - const { searchHistory } = useSearchHistory(history) + if (!searchHistory.length) { + return null + } return (

History

-
{searchHistory.map((item) => ( -
  • +
  • onSearchInputSelection?.(item.term, item.path)} > { height={18} data-fs-search-suggestion-item-icon /> - {item} + {item.term}
  • ))} diff --git a/src/components/search/SuggestionProductCard/SuggestionProductCard.tsx b/src/components/search/SuggestionProductCard/SuggestionProductCard.tsx index eb2c1387..e49ef807 100644 --- a/src/components/search/SuggestionProductCard/SuggestionProductCard.tsx +++ b/src/components/search/SuggestionProductCard/SuggestionProductCard.tsx @@ -5,6 +5,7 @@ import Link from 'src/components/ui/Link' import Price from 'src/components/ui/Price' import { useFormattedPrice } from 'src/sdk/product/useFormattedPrice' import { useProductLink } from 'src/sdk/product/useProductLink' +import useSearchInput from 'src/sdk/search/useSearchInput' import type { ProductSummary_ProductFragment } from '@generated/graphql' type SuggestionProductCardProps = { @@ -13,7 +14,13 @@ type SuggestionProductCardProps = { } function SuggestionProductCard({ product, index }: SuggestionProductCardProps) { - const linkProps = useProductLink({ product, selectedOffer: 0, index }) + const { onSearchInputSelection } = useSearchInput() + const { onClick, href, ...linkProps } = useProductLink({ + product, + selectedOffer: 0, + index, + }) + const { isVariantOf: { name }, image: [img], @@ -25,7 +32,16 @@ function SuggestionProductCard({ product, index }: SuggestionProductCardProps) { return ( - + { + onClick() + onSearchInputSelection?.(name, href) + }} + > () - - const [loading, setLoading] = useState(false) - - useEffect(() => { - if (term.length > 0) { - setLoading(true) - request< - SearchSuggestionsQueryQuery, - SearchSuggestionsQueryQueryVariables - >(SearchSuggestionsQuery, { - term, - selectedFacets: [ - { key: 'channel', value: channel ?? '' }, - { key: 'locale', value: locale }, - ], - }) - .then((data) => { - setSuggestions(data.search.suggestions) - }) - .finally(() => setLoading(false)) - } - }, [channel, locale, term]) - - const terms = suggestions?.terms ?? [] - const products = suggestions?.products ?? [] - - return { terms, products, loading } -} +import { SearchHistory } from '../History' +import SuggestionsTopSearch from './SuggestionsTopSearch' function Suggestions({ term = '', ...otherProps }: SuggestionsProps) { - const { terms, products, loading } = useSuggestions(term) + const { terms, products, isLoading } = useSuggestions(term) + + if (term.length === 0 && !isLoading) { + return ( + <> + + + + ) + } - if (term.length === 0 && !loading) { - return

    Top Search List

    + if (isLoading) { + return

    Loading...

    } - if (loading) { - return

    Loading...

    + if (terms.length === 0 && products.length === 0) { + return null } return ( diff --git a/src/components/search/Suggestions/SuggestionsTopSearch.stories.tsx b/src/components/search/Suggestions/SuggestionsTopSearch.stories.tsx index e71cbe44..9bb2a85b 100644 --- a/src/components/search/Suggestions/SuggestionsTopSearch.stories.tsx +++ b/src/components/search/Suggestions/SuggestionsTopSearch.stories.tsx @@ -1,3 +1,7 @@ +import { SessionProvider } from '@faststore/sdk' + +import { SearchInputProvider } from 'src/sdk/search/useSearchInput' + import { SuggestionsTopSearch } from '.' import type { SuggestionsTopSearchProps } from '.' @@ -15,19 +19,23 @@ const Template = (props: SuggestionsTopSearchProps) => ( background: 'white', }} > - + + + + + ) export const Default = Template.bind({}) Default.args = { - searchedItems: [ - { name: 'Office Supplies', href: '/office-supplies' }, - { name: 'Headphones', href: '/headphones' }, - { name: 'Notebooks', href: '/notebooks' }, - { name: 'Laser Printer', href: '/laser-printer' }, - { name: 'Bluetooth Keyboard', href: '/bluetooth-keyboard' }, + topTerms: [ + { value: 'Office Supplies', count: 5 }, + { value: 'Headphones', count: 4 }, + { value: 'Notebooks', count: 3 }, + { value: 'Laser Printer', count: 2 }, + { value: 'Bluetooth Keyboard', count: 1 }, ], } diff --git a/src/components/search/Suggestions/SuggestionsTopSearch.tsx b/src/components/search/Suggestions/SuggestionsTopSearch.tsx index ce6e4aa2..5bf5ba0e 100644 --- a/src/components/search/Suggestions/SuggestionsTopSearch.tsx +++ b/src/components/search/Suggestions/SuggestionsTopSearch.tsx @@ -4,6 +4,9 @@ import type { HTMLAttributes } from 'react' import { Badge } from 'src/components/ui/Badge' import Link from 'src/components/ui/Link' +import useSearchInput, { formatSearchPath } from 'src/sdk/search/useSearchInput' +import useTopSearch from 'src/sdk/search/useTopSearch' +import type { StoreSuggestionTerm } from '@generated/graphql' export interface SuggestionsTopSearchProps extends HTMLAttributes { @@ -14,22 +17,23 @@ export interface SuggestionsTopSearchProps /** * List of top searched items */ - // TODO: Adapts for the real received data type - searchedItems: LinkItem[] -} - -type LinkItem = { - href: string - name: string + topTerms?: StoreSuggestionTerm[] } const SuggestionsTopSearch = forwardRef< HTMLDivElement, SuggestionsTopSearchProps >(function SuggestionsTopSearch( - { testId = 'top-search', searchedItems, ...otherProps }, + { testId = 'top-search', topTerms, ...otherProps }, ref ) { + const { onSearchInputSelection } = useSearchInput() + const { terms, isLoading } = useTopSearch(topTerms) + + if (isLoading) { + return

    Loading...

    + } + return (
    Top Search

    - {searchedItems.map((item, index) => ( -
  • - + {terms.map((term, index) => ( +
  • + + onSearchInputSelection?.( + term.value, + formatSearchPath(term.value) + ) + } + > {index + 1} - {item.name} + {term.value}
  • ))} diff --git a/src/components/search/Suggestions/suggestions.scss b/src/components/search/Suggestions/suggestions.scss index cf72a4d2..82f6a11e 100644 --- a/src/components/search/Suggestions/suggestions.scss +++ b/src/components/search/Suggestions/suggestions.scss @@ -27,6 +27,12 @@ } } +[data-fs-search-suggestion-section="terms"] { + [data-fs-link] { + color: var(--fs-color-text-light); + } +} + [data-fs-search-suggestion-item] { margin-right: calc(-1 * var(--fs-spacing-3)); margin-left: calc(-1 * var(--fs-spacing-3)); @@ -37,8 +43,7 @@ background-color: var(--fs-color-main-0); } - [data-store-link], - [data-fs-button] { + [data-store-link] { display: flex; align-items: center; justify-content: flex-start; @@ -51,6 +56,10 @@ [data-fs-search-suggestion-item-icon] { margin-right: var(--fs-spacing-1); } + + [data-fs-search-suggestion-item-icon] { + color: var(--fs-color-neutral-4); + } } [data-fs-search-suggestions] { @@ -58,21 +67,4 @@ font-weight: var(--fs-text-weight-bold); color: var(--fs-color-neutral-7); } - - [data-fs-button] { - padding-top: var(--fs-spacing-2); - padding-bottom: var(--fs-spacing-2); - font-size: var(--fs-text-size-2); - font-weight: var(--fs-text-weight-regular); - line-height: 1.25; - color: var(--fs-color-text-light); - cursor: pointer; - background-color: transparent; - } - - [data-store-link] { - padding: 0 var(--fs-spacing-3); - text-decoration: none; - outline: none; - } } diff --git a/src/components/ui/Search/Suggestions/Suggestions.stories.tsx b/src/components/ui/Search/Suggestions/Suggestions.stories.tsx index 6acc8371..43e4dec1 100644 --- a/src/components/ui/Search/Suggestions/Suggestions.stories.tsx +++ b/src/components/ui/Search/Suggestions/Suggestions.stories.tsx @@ -1,5 +1,7 @@ import { SessionProvider } from '@faststore/sdk' +import { SearchInputProvider } from 'src/sdk/search/useSearchInput' + import Suggestions from '.' import type { SuggestionsProps } from '.' @@ -49,7 +51,9 @@ const Template = (props: SuggestionsProps) => ( }} > - + + + ) @@ -58,7 +62,10 @@ export const Default = Template.bind({}) Default.args = { term: 'Ste', - terms: ['Steel', 'Stellar'], + terms: [ + { value: 'Steel', count: 1 }, + { value: 'Stellar', count: 2 }, + ], products: [ product({ id: '1', name: 'Handmade Steel Towels Practical' }), product({ id: '2', name: 'Steel Towels' }), diff --git a/src/components/ui/Search/Suggestions/Suggestions.tsx b/src/components/ui/Search/Suggestions/Suggestions.tsx index 4a842e47..8b528105 100644 --- a/src/components/ui/Search/Suggestions/Suggestions.tsx +++ b/src/components/ui/Search/Suggestions/Suggestions.tsx @@ -1,8 +1,11 @@ import { List as UIList } from '@faststore/ui' import type { HTMLAttributes } from 'react' +import { Fragment } from 'react' import SuggestionProductCard from 'src/components/search/SuggestionProductCard' -import Button from 'src/components/ui/Button' +import Icon from 'src/components/ui/Icon' +import Link from 'src/components/ui/Link' +import useSearchInput, { formatSearchPath } from 'src/sdk/search/useSearchInput' import type { ProductSummary_ProductFragment } from '@generated/graphql' function formatSearchTerm( @@ -32,7 +35,7 @@ function handleSuggestions(suggestion: string, searchTerm: string) { return (

    {suggestionSubstring.map((substring, indexSubstring) => ( - <> + {substring.length > 0 && ( {indexSubstring === 0 @@ -42,7 +45,7 @@ function handleSuggestions(suggestion: string, searchTerm: string) { )} {indexSubstring !== suggestionSubstring.length - 1 && formatSearchTerm(indexSubstring, searchTerm, suggestion)} - + ))}

    ) @@ -57,12 +60,6 @@ export interface SuggestionsProps extends HTMLAttributes { * Search term */ term?: string - /** - * Callback to be executed when a suggestion is selected. - * - * @memberof SuggestionsProps - */ - onSearch: (term: string) => void terms?: Array<{ value: string }> products?: ProductSummary_ProductFragment[] } @@ -72,18 +69,33 @@ function Suggestions({ term = '', terms = [], products = [], - onSearch, ...otherProps }: SuggestionsProps) { + const { onSearchInputSelection } = useSearchInput() + return (
    {terms.length > 0 && ( - + {terms?.map(({ value: suggestion }) => (
  • - +
  • ))}
    diff --git a/src/sdk/search/useSearchHistory.ts b/src/sdk/search/useSearchHistory.ts index ffe74b43..7fa4cea5 100644 --- a/src/sdk/search/useSearchHistory.ts +++ b/src/sdk/search/useSearchHistory.ts @@ -2,25 +2,29 @@ import { useStorage } from '@faststore/sdk' const storageKey = 'main::store::searchHistory' -const MAX_HISTORY_SIZE = 5 +const MAX_HISTORY_SIZE = 4 + +export interface History { + term: string + path: string +} export default function useSearchHistory( - history: string[] = [], + history: History[] = [], maxHistorySize: number = MAX_HISTORY_SIZE ) { - const [searchHistory, setSearchHistory] = useStorage( + const [searchHistory, setSearchHistory] = useStorage( storageKey, history ) - function addToSearchHistory(term: string) { - const newHistory = [...new Set([term, ...searchHistory])] + function addToSearchHistory(newHistory: History) { + const set = new Set() + const newHistoryArray = [newHistory, ...searchHistory] + .slice(0, maxHistorySize) + .filter((item) => !set.has(item.term) && set.add(item.term), set) - setSearchHistory( - newHistory.length > maxHistorySize - ? newHistory.slice(0, maxHistorySize) - : newHistory - ) + setSearchHistory(newHistoryArray) } function clearSearchHistory() { diff --git a/src/sdk/search/useSearchInput.tsx b/src/sdk/search/useSearchInput.tsx new file mode 100644 index 00000000..5780857e --- /dev/null +++ b/src/sdk/search/useSearchInput.tsx @@ -0,0 +1,43 @@ +import { formatSearchState, initSearchState } from '@faststore/sdk' +import type { PropsWithChildren } from 'react' +import { createContext, useContext } from 'react' + +export const formatSearchPath = (term: string) => { + const { pathname, search } = formatSearchState( + initSearchState({ + term, + base: '/s', + }) + ) + + return `${pathname}${search}` +} + +export interface SearchInputContextValue { + onSearchInputSelection?: (term: string, path: string) => void +} + +const SearchInputContext = createContext(null) + +export function SearchInputProvider({ + onSearchInputSelection, + children, +}: PropsWithChildren) { + return ( + + {children} + + ) +} + +const useSearchInput = () => { + const context = useContext(SearchInputContext) + + if (!context) { + throw new Error('Do not use outside the SearchInputContext context.') + } + + return context +} + +export default useSearchInput diff --git a/src/sdk/search/useSuggestions.ts b/src/sdk/search/useSuggestions.ts new file mode 100644 index 00000000..b2c8ba9d --- /dev/null +++ b/src/sdk/search/useSuggestions.ts @@ -0,0 +1,48 @@ +import { useSession } from '@faststore/sdk' +import { gql } from '@vtex/graphql-utils' + +import { useQuery } from 'src/sdk/graphql/useQuery' +import type { + SearchSuggestionsQueryQuery as Query, + SearchSuggestionsQueryQueryVariables as Variables, +} from '@generated/graphql' + +const MAX_SUGGESTIONS = 5 + +const query = gql` + query SearchSuggestionsQuery( + $term: String! + $selectedFacets: [IStoreSelectedFacet!] + ) { + search(first: 5, term: $term, selectedFacets: $selectedFacets) { + suggestions { + terms { + value + } + products { + ...ProductSummary_product + } + } + } + } +` + +function useSuggestions(term: string, limit: number = MAX_SUGGESTIONS) { + const { channel, locale } = useSession() + + const { data, error } = useQuery(query, { + term, + selectedFacets: [ + { key: 'channel', value: channel ?? '' }, + { key: 'locale', value: locale }, + ], + }) + + return { + terms: (data?.search.suggestions.terms ?? []).slice(0, limit), + products: (data?.search.suggestions.products ?? []).slice(0, limit), + isLoading: !error && !data, + } +} + +export default useSuggestions diff --git a/src/sdk/search/useTopSearch.ts b/src/sdk/search/useTopSearch.ts new file mode 100644 index 00000000..8431d18f --- /dev/null +++ b/src/sdk/search/useTopSearch.ts @@ -0,0 +1,43 @@ +import { useSession } from '@faststore/sdk' +import { gql } from '@vtex/graphql-utils' + +import { useQuery } from 'src/sdk/graphql/useQuery' +import type { + StoreSuggestionTerm, + SearchSuggestionsQueryQuery as Query, + SearchSuggestionsQueryQueryVariables as Variables, +} from '@generated/graphql' + +const MAX_TOP_SEARCH_TERMS = 5 + +const query = gql` + query SearchSuggestionsQuery { + search { + suggestions { + terms + } + } + } +` + +function useTopSearch( + initialTerms: StoreSuggestionTerm[] = [], + limit: number = MAX_TOP_SEARCH_TERMS +) { + const { channel, locale } = useSession() + + const { data, error } = useQuery(query, { + term: '', + selectedFacets: [ + { key: 'channel', value: channel ?? '' }, + { key: 'locale', value: locale }, + ], + }) + + return { + terms: (data?.search.suggestions.terms ?? initialTerms).slice(0, limit), + isLoading: !error && !data, + } +} + +export default useTopSearch diff --git a/tsconfig.json b/tsconfig.json index 83f41fdc..eeae7e24 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "@vtex/tsconfig", "compilerOptions": { + "types": ["react/next"], "allowSyntheticDefaultImports": true, "moduleResolution": "node", "jsx": "preserve", From 5be7614495c2d3793be757f07602fff25096f742 Mon Sep 17 00:00:00 2001 From: "Filipe W. Lima" Date: Fri, 10 Jun 2022 13:51:52 -0300 Subject: [PATCH 02/11] Add entries to the change log --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c44d539e..ca2f9b42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- The search input now includes the last 4 previously searched terms (`SearchHistory`) ([#112](https://github.com/vtex-sites/nextjs.store/pull/112)). +- The top 5 searches (`SuggestionsTopSearch`) are now integrated into the search input ([#112](https://github.com/vtex-sites/nextjs.store/pull/112)). - Support for the new type definitions from React 18 ([#113](https://github.com/vtex-sites/nextjs.store/pull/113)). ### Changed @@ -19,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- The search input's dropdown not closing when a suggested product was selected ([#112](https://github.com/vtex-sites/nextjs.store/pull/112). + ### Security ## [22.23.0.beta] - 2022-06-10 From d4ed4e0ca375cf1762bf2a49a6354c8964d30bc4 Mon Sep 17 00:00:00 2001 From: Daniel Zanzini Date: Thu, 9 Jun 2022 19:29:57 -0300 Subject: [PATCH 03/11] fix: duplicated query at `useTopSearch` hook Cherry-picked from the `gatsby.store` repo. * Fixes `useTopSearch` query * Updates CHANGELOG.md --- Conflicts: - @generated/graphql/index.ts - @generated/graphql/persisted.json - CHANGELOG.md --- @generated/graphql/index.ts | 75 +++++++++++++++++-------------- @generated/graphql/persisted.json | 3 +- src/sdk/search/useTopSearch.ts | 11 +++-- 3 files changed, 52 insertions(+), 37 deletions(-) diff --git a/@generated/graphql/index.ts b/@generated/graphql/index.ts index 758c5a04..a3bf262a 100644 --- a/@generated/graphql/index.ts +++ b/@generated/graphql/index.ts @@ -600,39 +600,6 @@ export type Filter_FacetsFragment = { }> } -export type SearchSuggestionsQueryQueryVariables = Exact<{ - term: Scalars['String'] - selectedFacets: InputMaybe | IStoreSelectedFacet> -}> - -export type SearchSuggestionsQueryQuery = { - search: { - suggestions: { - terms: Array<{ value: string }> - products: Array<{ - slug: string - sku: string - name: string - gtin: string - id: string - brand: { name: string; brandName: string } - isVariantOf: { productGroupID: string; name: string } - image: Array<{ url: string; alternateName: string }> - offers: { - lowPrice: number - offers: Array<{ - availability: string - price: number - listPrice: number - quantity: number - seller: { identifier: string } - }> - } - }> - } - } -} - export type ProductDetailsFragment_ProductFragment = { sku: string name: string @@ -872,6 +839,48 @@ export type ProductsQueryQuery = { } } +export type SearchSuggestionsQueryQueryVariables = Exact<{ + term: Scalars['String'] + selectedFacets: InputMaybe | IStoreSelectedFacet> +}> + +export type SearchSuggestionsQueryQuery = { + search: { + suggestions: { + terms: Array<{ value: string }> + products: Array<{ + slug: string + sku: string + name: string + gtin: string + id: string + brand: { name: string; brandName: string } + isVariantOf: { productGroupID: string; name: string } + image: Array<{ url: string; alternateName: string }> + offers: { + lowPrice: number + offers: Array<{ + availability: string + price: number + listPrice: number + quantity: number + seller: { identifier: string } + }> + } + }> + } + } +} + +export type TopSearchSuggestionsQueryQueryVariables = Exact<{ + term: Scalars['String'] + selectedFacets: InputMaybe | IStoreSelectedFacet> +}> + +export type TopSearchSuggestionsQueryQuery = { + search: { suggestions: { terms: Array<{ value: string }> } } +} + export type ValidateSessionMutationVariables = Exact<{ session: IStoreSession search: Scalars['String'] diff --git a/@generated/graphql/persisted.json b/@generated/graphql/persisted.json index 8673fcf5..e3f3c4b1 100644 --- a/@generated/graphql/persisted.json +++ b/@generated/graphql/persisted.json @@ -1,10 +1,11 @@ { - "SearchSuggestionsQuery": "query SearchSuggestionsQuery($term: String!, $selectedFacets: [IStoreSelectedFacet!]) {\n search(first: 5, term: $term, selectedFacets: $selectedFacets) {\n suggestions {\n terms {\n value\n }\n products {\n id: productID\n slug\n sku\n brand {\n brandName: name\n name\n }\n name\n gtin\n isVariantOf {\n productGroupID\n name\n }\n image {\n url\n alternateName\n }\n offers {\n lowPrice\n offers {\n availability\n price\n listPrice\n quantity\n seller {\n identifier\n }\n }\n }\n }\n }\n }\n}\n", "ProductGalleryQuery": "query ProductGalleryQuery($first: Int!, $after: String!, $sort: StoreSort!, $term: String!, $selectedFacets: [IStoreSelectedFacet!]!) {\n search(\n first: $first\n after: $after\n sort: $sort\n term: $term\n selectedFacets: $selectedFacets\n ) {\n products {\n pageInfo {\n totalCount\n }\n }\n facets {\n key\n label\n type\n values {\n label\n value\n selected\n quantity\n }\n }\n }\n}\n", "ServerCollectionPageQuery": "query ServerCollectionPageQuery($slug: String!) {\n collection(slug: $slug) {\n seo {\n title\n description\n }\n breadcrumbList {\n itemListElement {\n item\n name\n position\n }\n }\n meta {\n selectedFacets {\n key\n value\n }\n }\n }\n}\n", "ServerProductPageQuery": "query ServerProductPageQuery($id: String!) {\n product(locator: [{key: \"id\", value: $id}]) {\n id: productID\n slug\n seo {\n title\n description\n }\n brand {\n name\n }\n sku\n gtin\n name\n description\n breadcrumbList {\n itemListElement {\n item\n name\n position\n }\n }\n image {\n url\n alternateName\n }\n offers {\n lowPrice\n highPrice\n priceCurrency\n offers {\n availability\n price\n priceValidUntil\n priceCurrency\n itemCondition\n seller {\n identifier\n }\n listPrice\n }\n }\n isVariantOf {\n productGroupID\n name\n }\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n }\n}\n", "ValidateCartMutation": "mutation ValidateCartMutation($cart: IStoreCart!) {\n validateCart(cart: $cart) {\n order {\n orderNumber\n acceptedOffer {\n seller {\n identifier\n }\n quantity\n price\n listPrice\n itemOffered {\n sku\n name\n image {\n url\n alternateName\n }\n brand {\n name\n }\n isVariantOf {\n productGroupID\n name\n }\n gtin\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n }\n }\n }\n messages {\n text\n status\n }\n }\n}\n", "BrowserProductQuery": "query BrowserProductQuery($locator: [IStoreSelectedFacet!]!) {\n product(locator: $locator) {\n id: productID\n sku\n name\n gtin\n description\n isVariantOf {\n productGroupID\n name\n }\n image {\n url\n alternateName\n }\n brand {\n name\n }\n offers {\n lowPrice\n offers {\n availability\n price\n listPrice\n seller {\n identifier\n }\n }\n }\n breadcrumbList {\n itemListElement {\n item\n name\n position\n }\n }\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n }\n}\n", "ProductsQuery": "query ProductsQuery($first: Int!, $after: String, $sort: StoreSort!, $term: String!, $selectedFacets: [IStoreSelectedFacet!]!) {\n search(\n first: $first\n after: $after\n sort: $sort\n term: $term\n selectedFacets: $selectedFacets\n ) {\n products {\n pageInfo {\n totalCount\n }\n edges {\n node {\n id: productID\n slug\n sku\n brand {\n brandName: name\n name\n }\n name\n gtin\n isVariantOf {\n productGroupID\n name\n }\n image {\n url\n alternateName\n }\n offers {\n lowPrice\n offers {\n availability\n price\n listPrice\n quantity\n seller {\n identifier\n }\n }\n }\n }\n }\n }\n }\n}\n", + "SearchSuggestionsQuery": "query SearchSuggestionsQuery($term: String!, $selectedFacets: [IStoreSelectedFacet!]) {\n search(first: 5, term: $term, selectedFacets: $selectedFacets) {\n suggestions {\n terms {\n value\n }\n products {\n id: productID\n slug\n sku\n brand {\n brandName: name\n name\n }\n name\n gtin\n isVariantOf {\n productGroupID\n name\n }\n image {\n url\n alternateName\n }\n offers {\n lowPrice\n offers {\n availability\n price\n listPrice\n quantity\n seller {\n identifier\n }\n }\n }\n }\n }\n }\n}\n", + "TopSearchSuggestionsQuery": "query TopSearchSuggestionsQuery($term: String!, $selectedFacets: [IStoreSelectedFacet!]) {\n search(first: 5, term: $term, selectedFacets: $selectedFacets) {\n suggestions {\n terms {\n value\n }\n }\n }\n}\n", "ValidateSession": "mutation ValidateSession($session: IStoreSession!, $search: String!) {\n validateSession(session: $session, search: $search) {\n locale\n channel\n country\n postalCode\n currency {\n code\n symbol\n }\n person {\n id\n email\n givenName\n familyName\n }\n }\n}\n" } diff --git a/src/sdk/search/useTopSearch.ts b/src/sdk/search/useTopSearch.ts index 8431d18f..54e98f0b 100644 --- a/src/sdk/search/useTopSearch.ts +++ b/src/sdk/search/useTopSearch.ts @@ -11,10 +11,15 @@ import type { const MAX_TOP_SEARCH_TERMS = 5 const query = gql` - query SearchSuggestionsQuery { - search { + query TopSearchSuggestionsQuery( + $term: String! + $selectedFacets: [IStoreSelectedFacet!] + ) { + search(first: 5, term: $term, selectedFacets: $selectedFacets) { suggestions { - terms + terms { + value + } } } } From 86aa107bdb2f1cf072c41975bb6dc05229f424de Mon Sep 17 00:00:00 2001 From: "Filipe W. Lima" Date: Fri, 10 Jun 2022 18:35:37 -0300 Subject: [PATCH 04/11] Undo unnecessary change to `tsconfig.json` I had done that to add support for types from React 18 on Gatsby but that is not needed in the NextJS case. The support for React 18 types was done in a previous PR. --- tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index eeae7e24..83f41fdc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "@vtex/tsconfig", "compilerOptions": { - "types": ["react/next"], "allowSyntheticDefaultImports": true, "moduleResolution": "node", "jsx": "preserve", From 889d26b4b81053ec0c26bf061299180f80f74b0b Mon Sep 17 00:00:00 2001 From: "Filipe W. Lima" Date: Mon, 13 Jun 2022 13:51:43 -0300 Subject: [PATCH 05/11] Fix `SearchHistory` storie, it was not loading --- src/components/search/History/SearchHistory.stories.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/search/History/SearchHistory.stories.tsx b/src/components/search/History/SearchHistory.stories.tsx index 7ddbcdec..3162cc88 100644 --- a/src/components/search/History/SearchHistory.stories.tsx +++ b/src/components/search/History/SearchHistory.stories.tsx @@ -28,7 +28,12 @@ const Template = (props: SearchHistoryProps) => { export const Default = Template.bind({}) Default.args = { - history: ['headphone', 'audio & video', 'mh-7000', 'jbl go'], + history: [ + { term: 'headphone', path: '/' }, + { term: 'audio & video', path: '/' }, + { term: 'mh-7000', path: '/' }, + { term: 'jbl go', path: '/' }, + ], } Default.parameters = { From 655496b572866ad2fce6c7f7e80a2677daebc1c4 Mon Sep 17 00:00:00 2001 From: "Filipe W. Lima" Date: Mon, 13 Jun 2022 16:34:21 -0300 Subject: [PATCH 06/11] Fix the suggested term's link color Part of the link had the blue color instead of light gray. That's because the light gray selector had less CSS specificity than the blue one, so I increased the specificity. I think will be addressed when we got CSS modules working for all components. --- src/components/search/Suggestions/suggestions.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/search/Suggestions/suggestions.scss b/src/components/search/Suggestions/suggestions.scss index 82f6a11e..94038e5c 100644 --- a/src/components/search/Suggestions/suggestions.scss +++ b/src/components/search/Suggestions/suggestions.scss @@ -27,7 +27,9 @@ } } -[data-fs-search-suggestion-section="terms"] { +// Temporarily increased CSS specificity because the normal link blue color +// was winning. Hoping this will be fixed when using CSS modules. +[data-fs-search-suggestions] [data-fs-search-suggestion-section="terms"] { [data-fs-link] { color: var(--fs-color-text-light); } From 829387301cd757cec629e5d64cb26fbf36ff6623 Mon Sep 17 00:00:00 2001 From: "Filipe W. Lima" Date: Mon, 13 Jun 2022 17:43:34 -0300 Subject: [PATCH 07/11] Trigger CI with an empty commit From 506d41ade7e0d320976806341895b52dc57f72e0 Mon Sep 17 00:00:00 2001 From: "Filipe W. Lima" Date: Mon, 13 Jun 2022 19:10:26 -0300 Subject: [PATCH 08/11] Hide Top Search dropdown when there is no data This week querying for the top search for the storeframework account returned no results, that's because this is based on real usage data. Because of that, we were showing the "Top Search" dropdown with the heading but no data. In this scenario, let's not render anything till we decide what to do. --- src/components/search/Suggestions/SuggestionsTopSearch.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/search/Suggestions/SuggestionsTopSearch.tsx b/src/components/search/Suggestions/SuggestionsTopSearch.tsx index 5bf5ba0e..c11102a6 100644 --- a/src/components/search/Suggestions/SuggestionsTopSearch.tsx +++ b/src/components/search/Suggestions/SuggestionsTopSearch.tsx @@ -34,6 +34,10 @@ const SuggestionsTopSearch = forwardRef< return

    Loading...

    } + if (terms.length === 0) { + return null + } + return (
    Date: Mon, 13 Jun 2022 19:23:37 -0300 Subject: [PATCH 09/11] Fix entries order in the change log --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51246357..e567c5a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- 301 redirects when visiting old VTEX product routes ([#93](https://github.com/vtex-sites/nextjs.store/pull/93)) - The search input now includes the last 4 previously searched terms (`SearchHistory`) ([#112](https://github.com/vtex-sites/nextjs.store/pull/112)). - The top 5 searches (`SuggestionsTopSearch`) are now integrated into the search input ([#112](https://github.com/vtex-sites/nextjs.store/pull/112)). +- 301 redirects when visiting old VTEX product routes ([#93](https://github.com/vtex-sites/nextjs.store/pull/93)) - Support for the new type definitions from React 18 ([#113](https://github.com/vtex-sites/nextjs.store/pull/113)). ### Changed @@ -21,9 +21,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed ### Fixed -- Status code when error occurs (404/500) ([#116](https://github.com/vtex-sites/nextjs.store/pull/116)) - The search input's dropdown not closing when a suggested product was selected ([#112](https://github.com/vtex-sites/nextjs.store/pull/112). +- Status code when error occurs (404/500) ([#116](https://github.com/vtex-sites/nextjs.store/pull/116)) ### Security From 54d59da041536e171d422e3b3887b8a0ac20bd0c Mon Sep 17 00:00:00 2001 From: "Filipe W. Lima" Date: Mon, 13 Jun 2022 21:03:58 -0300 Subject: [PATCH 10/11] Fix not showing the outline when focusing on a suggested product card And also adjust the padding so the link covers it. Reported by Fanny! --- .../SuggestionProductCard/suggestion-product-card.scss | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/search/SuggestionProductCard/suggestion-product-card.scss b/src/components/search/SuggestionProductCard/suggestion-product-card.scss index 18ea0792..7c5bfd44 100644 --- a/src/components/search/SuggestionProductCard/suggestion-product-card.scss +++ b/src/components/search/SuggestionProductCard/suggestion-product-card.scss @@ -1,8 +1,6 @@ @import "src/styles/scaffold"; [data-fs-suggestion-product-card] { - padding: var(--fs-spacing-1) 0; - [data-card-image] { display: flex; } @@ -40,9 +38,8 @@ } [data-store-link] { - padding: 0 var(--fs-spacing-3); + padding: var(--fs-spacing-1) var(--fs-spacing-3); color: inherit; text-decoration: none; - outline: none; } } From 6300c54e3803176b1302fec2ec4c43ae5fd4627d Mon Sep 17 00:00:00 2001 From: "Filipe W. Lima" Date: Wed, 15 Jun 2022 14:30:10 -0300 Subject: [PATCH 11/11] Prefix a comment with TODO for easier reference in the future See suggestion: https://github.com/vtex-sites/nextjs.store/pull/112#discussion_r897236782 --- src/components/search/Suggestions/suggestions.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/search/Suggestions/suggestions.scss b/src/components/search/Suggestions/suggestions.scss index 20dbbdfa..6bb16496 100644 --- a/src/components/search/Suggestions/suggestions.scss +++ b/src/components/search/Suggestions/suggestions.scss @@ -27,7 +27,7 @@ } } -// Temporarily increased CSS specificity because the normal link blue color +// TODO: Temporarily increased CSS specificity because the normal link blue color // was winning. Hoping this will be fixed when using CSS modules. [data-fs-search-suggestions] [data-fs-search-suggestion-section="terms"] { [data-fs-link] {