import * as React from 'react' import {createContext, useContext, useState, useCallback} from 'react' import {SearchIcon, X} from 'lucide-react' import {useProductSearch} from '../../hooks/product/useProductSearch' import {cn} from '../../lib/utils' import {type Product} from '../../types' import {IconButton} from '../atoms/icon-button' import {List} from '../atoms/list' import {Input} from '../ui/input' import {ProductLink} from './product-link' import {ProductLinkSkeleton} from './product-link-skeleton' interface SearchContextValue { query: string setQuery: (query: string) => void products: Product[] | null loading: boolean error: Error | null fetchMore?: () => Promise hasNextPage: boolean isTyping: boolean } const SearchContext = createContext(null) function useSearchContext() { const context = useContext(SearchContext) if (!context) { throw new Error('useSearchContext must be used within a SearchProvider') } return context } /** * Product search with automatic debouncing via `useProductSearch` hook. Two usage patterns: all-in-one `Search` component (fastest setup, fixed layout) or composable `SearchProvider` + `SearchInput` + `SearchResultsList` (custom layouts, shared search state). The all-in-one component handles input, loading skeletons, empty states, and infinite scroll. Use composable approach when you need custom result rendering or multiple search-dependent sections. * @publicDocs */ export interface SearchProviderProps { initialQuery?: string children: React.ReactNode } function SearchProvider({initialQuery = '', children}: SearchProviderProps) { const [query, setQueryState] = useState(initialQuery) const {products, loading, error, fetchMore, hasNextPage, isTyping} = useProductSearch({ query, fetchPolicy: 'network-only', }) const setQuery = useCallback((newQuery: string) => { setQueryState(newQuery) }, []) const contextValue: SearchContextValue = { query, setQuery, products, loading, error, fetchMore, hasNextPage, isTyping, } return ( {children} ) } export interface SearchInputProps { placeholder?: string className?: string inputProps?: React.ComponentProps<'input'> } function SearchInput({ placeholder = 'Search products...', className, inputProps, }: SearchInputProps) { const {query, setQuery} = useSearchContext() const handleQueryChange = useCallback( (event: React.ChangeEvent) => { setQuery(event.target.value) inputProps?.onChange?.(event) }, [inputProps, setQuery] ) return (
{query === '' ? null : ( setQuery('')} buttonStyles="flex items-center rounded-radius-max bg-[var(--grayscale-l20)]" /> )}
) } export interface SearchResultsListProps { renderItem?: (product: Product, index: number) => React.ReactNode height?: number itemHeight?: number initialStateComponent?: React.JSX.Element showScrollbar?: boolean overscanCount?: number } function SearchResultsList({ height = window.innerHeight, renderItem, initialStateComponent, showScrollbar, }: SearchResultsListProps) { const {query, products, loading, fetchMore, hasNextPage, isTyping} = useSearchContext() const _renderItem = (product: Product, index: number) => { if (renderItem) { return renderItem(product, index) } return (
) } const shouldShowStartingState = query.trim().length === 0 const shouldShowLoading = (!products || products.length === 0) && (loading || isTyping) const shouldShowEmptyState = (!products || products.length === 0) && !loading if (shouldShowStartingState) { return ( initialStateComponent || (
Start typing to search for products
) ) } if (shouldShowLoading) { return (
) } if (shouldShowEmptyState) { return (
{`No products found for "${query}"`}
) } return ( ) } interface SearchProviderPropsWithoutChildren extends Omit< SearchProviderProps, 'children' > {} export interface SearchResultsProps extends SearchProviderPropsWithoutChildren, SearchInputProps, SearchResultsListProps { showSearchInput?: boolean onProductClick?: (product: Product) => void } function Search({ initialQuery, placeholder, inputProps, height, className, renderItem, itemHeight, onProductClick, }: SearchResultsProps) { const _renderItem = (product: Product, index: number) => { if (renderItem) { return renderItem(product, index) } return (
) } return (
) } export {SearchProvider, SearchInput, SearchResultsList, Search}