import {Link, useFetcher, type Fetcher} from 'react-router'; import {Image, Money} from '@shopify/hydrogen'; import React, {useRef, useEffect} from 'react'; import { getEmptyPredictiveSearchResult, urlWithTrackingParams, type PredictiveSearchReturn, } from '~/lib/search'; import {useAside} from './Aside'; type PredictiveSearchItems = PredictiveSearchReturn['result']['items']; type UsePredictiveSearchReturn = { term: React.MutableRefObject; total: number; inputRef: React.MutableRefObject; items: PredictiveSearchItems; fetcher: Fetcher; }; type SearchResultsPredictiveArgs = Pick< UsePredictiveSearchReturn, 'term' | 'total' | 'inputRef' | 'items' > & { state: Fetcher['state']; closeSearch: () => void; }; type PartialPredictiveSearchResult< ItemType extends keyof PredictiveSearchItems, ExtraProps extends keyof SearchResultsPredictiveArgs = 'term' | 'closeSearch', > = Pick & Pick; type SearchResultsPredictiveProps = { children: (args: SearchResultsPredictiveArgs) => React.ReactNode; }; /** * Component that renders predictive search results */ export function SearchResultsPredictive({ children, }: SearchResultsPredictiveProps) { const aside = useAside(); const {term, inputRef, fetcher, total, items} = usePredictiveSearch(); /* * Utility that resets the search input */ function resetInput() { if (inputRef.current) { inputRef.current.blur(); inputRef.current.value = ''; } } /** * Utility that resets the search input and closes the search aside */ function closeSearch() { resetInput(); aside.close(); } return children({ items, closeSearch, inputRef, state: fetcher.state, term, total, }); } SearchResultsPredictive.Articles = SearchResultsPredictiveArticles; SearchResultsPredictive.Collections = SearchResultsPredictiveCollections; SearchResultsPredictive.Pages = SearchResultsPredictivePages; SearchResultsPredictive.Products = SearchResultsPredictiveProducts; SearchResultsPredictive.Queries = SearchResultsPredictiveQueries; SearchResultsPredictive.Empty = SearchResultsPredictiveEmpty; function SearchResultsPredictiveArticles({ term, articles, closeSearch, }: PartialPredictiveSearchResult<'articles'>) { if (!articles.length) return null; return (
Articles
    {articles.map((article) => { const articleUrl = urlWithTrackingParams({ baseUrl: `/blogs/${article.blog.handle}/${article.handle}`, trackingParams: article.trackingParameters, term: term.current ?? '', }); return (
  • {article.image?.url && ( {article.image.altText )}
    {article.title}
  • ); })}
); } function SearchResultsPredictiveCollections({ term, collections, closeSearch, }: PartialPredictiveSearchResult<'collections'>) { if (!collections.length) return null; return (
Collections
    {collections.map((collection) => { const collectionUrl = urlWithTrackingParams({ baseUrl: `/collections/${collection.handle}`, trackingParams: collection.trackingParameters, term: term.current, }); return (
  • {collection.image?.url && ( {collection.image.altText )}
    {collection.title}
  • ); })}
); } function SearchResultsPredictivePages({ term, pages, closeSearch, }: PartialPredictiveSearchResult<'pages'>) { if (!pages.length) return null; return (
Pages
    {pages.map((page) => { const pageUrl = urlWithTrackingParams({ baseUrl: `/pages/${page.handle}`, trackingParams: page.trackingParameters, term: term.current, }); return (
  • {page.title}
  • ); })}
); } function SearchResultsPredictiveProducts({ term, products, closeSearch, }: PartialPredictiveSearchResult<'products'>) { if (!products.length) return null; return (
Products
    {products.map((product) => { const productUrl = urlWithTrackingParams({ baseUrl: `/products/${product.handle}`, trackingParams: product.trackingParameters, term: term.current, }); const price = product?.selectedOrFirstAvailableVariant?.price; const image = product?.selectedOrFirstAvailableVariant?.image; return (
  • {image && ( {image.altText )}

    {product.title}

    {price && }
  • ); })}
); } function SearchResultsPredictiveQueries({ queries, queriesDatalistId, }: PartialPredictiveSearchResult<'queries', never> & { queriesDatalistId: string; }) { if (!queries.length) return null; return ( {queries.map((suggestion) => { if (!suggestion) return null; return ); } function SearchResultsPredictiveEmpty({ term, }: { term: React.MutableRefObject; }) { if (!term.current) { return null; } return (

No results found for {term.current}

); } /** * Hook that returns the predictive search results and fetcher and input ref. * @example * '''ts * const { items, total, inputRef, term, fetcher } = usePredictiveSearch(); * ''' **/ function usePredictiveSearch(): UsePredictiveSearchReturn { const fetcher = useFetcher({key: 'search'}); const term = useRef(''); const inputRef = useRef(null); if (fetcher?.state === 'loading') { term.current = String(fetcher.formData?.get('q') || ''); } // capture the search input element as a ref useEffect(() => { if (!inputRef.current) { inputRef.current = document.querySelector('input[type="search"]'); } }, []); const {items, total} = fetcher?.data?.result ?? getEmptyPredictiveSearchResult(); return {items, total, inputRef, term, fetcher}; }