Files
medusa-store/www/packages/docs-ui/src/components/Search/index.tsx
T

173 lines
5.9 KiB
TypeScript

"use client"
import React, { useEffect, useMemo, useRef, useState } from "react"
import { InstantSearch, SearchBox } from "react-instantsearch"
import clsx from "clsx"
import { SearchEmptyQueryBoundary } from "./EmptyQueryBoundary"
import { SearchSuggestions, type SearchSuggestionType } from "./Suggestions"
import { AlgoliaProps, useSearch } from "@/providers"
import { checkArraySameElms } from "@/utils"
import { SearchHitsWrapper } from "./Hits"
import { SelectBadge, SpinnerLoading } from "@/components"
import { useSearchNavigation, type OptionType } from "@/hooks"
import { SearchFooter } from "./Footer"
export type SearchProps = {
algolia: AlgoliaProps
isLoading?: boolean
suggestions: SearchSuggestionType[]
checkInternalPattern?: RegExp
filterOptions?: OptionType[]
}
export const Search = ({
algolia,
suggestions,
isLoading = false,
checkInternalPattern,
filterOptions = [],
}: SearchProps) => {
const { isOpen, defaultFilters, searchClient, modalRef } = useSearch()
const [filters, setFilters] = useState<string[]>(defaultFilters)
const searchBoxRef = useRef<HTMLFormElement>(null)
const focusSearchInput = () =>
searchBoxRef.current?.querySelector("input")?.focus()
useEffect(() => {
if (!checkArraySameElms(defaultFilters, filters)) {
setFilters(defaultFilters)
}
}, [defaultFilters])
useEffect(() => {
if (isOpen && searchBoxRef.current) {
focusSearchInput()
} else if (!isOpen) {
const focusedItem = modalRef.current?.querySelector(
":focus"
) as HTMLElement
if (
focusedItem &&
focusedItem === searchBoxRef.current?.querySelector("input")
) {
// remove focus
focusedItem.blur()
}
}
}, [isOpen])
const facetFilters = useMemo(() => {
const filtersToUse =
!filters.length || filters[0] === "all"
? filterOptions.map((option) => option.value)
: filters
return filtersToUse.map((filter) => "_tags:" + filter)
}, [filters, defaultFilters])
useSearchNavigation({
getInputElm: () =>
searchBoxRef.current?.querySelector("input") as HTMLInputElement,
focusInput: focusSearchInput,
keyboardProps: {
isLoading,
},
})
return (
<div className="h-full flex flex-col">
{filterOptions.length > 0 && (
<SelectBadge
multiple
options={filterOptions}
value={filters}
setSelected={(value) =>
setFilters(Array.isArray(value) ? [...value] : [value])
}
addSelected={(value) => setFilters((prev) => [...prev, value])}
removeSelected={(value) =>
setFilters((prev) => prev.filter((v) => v !== value))
}
showClearButton={false}
placeholder="Filters"
handleAddAll={(isAllSelected: boolean) => {
if (isAllSelected) {
setFilters(defaultFilters)
} else {
setFilters(filterOptions.map((option) => option.value))
}
}}
className="px-docs_1 pt-docs_1 bg-medusa-bg-base z-10"
/>
)}
{/* @ts-expect-error React v19 doesn't see this type as a React element */}
<InstantSearch
indexName={algolia.mainIndexName}
searchClient={searchClient}
future={{
preserveSharedStateOnUnmount: true,
}}
>
<div className={clsx("bg-medusa-bg-base flex z-[1]")}>
{/* @ts-expect-error React v19 doesn't see this type as a React element */}
<SearchBox
classNames={{
root: clsx(
"h-[57px] w-full md:rounded-t-docs_xl relative border-0 border-solid",
"border-b border-medusa-border-base",
"bg-transparent"
),
form: clsx("h-full md:rounded-t-docs_xl bg-transparent"),
input: clsx(
"w-full h-full px-docs_1 py-docs_0.75 text-medusa-fg-base",
"placeholder:text-medusa-fg-muted bg-medusa-bg-base",
"md:rounded-t-docs_xl text-compact-large sm:text-compact-medium",
"appearance-none search-cancel:hidden border-0 active:outline-none focus:outline-none"
),
submit: clsx("absolute top-[18px] left-docs_1 btn-clear p-0"),
reset: clsx("absolute top-[18px] right-docs_1", "btn-clear"),
loadingIndicator: clsx("absolute top-[18px] right-docs_1"),
}}
submitIconComponent={() => <></>}
resetIconComponent={() => (
<span className="text-medusa-fg-muted text-compact-small-plus hover:text-medusa-fg-subtle">
Clear
</span>
)}
placeholder="Find something..."
autoFocus
formRef={searchBoxRef as React.RefObject<HTMLFormElement>}
loadingIconComponent={() => <SpinnerLoading />}
/>
</div>
<div
className={clsx(
"md:flex-initial",
filterOptions.length > 0 &&
"h-[calc(100%-95px)] lg:max-h-[calc(100%-140px)] lg:min-h-[calc(100%-140px)]",
filterOptions.length === 0 &&
"h-[calc(100%-75px)] lg:max-h-[calc(100%-100px)] lg:min-h-[calc(100%-100px)]"
)}
>
<SearchEmptyQueryBoundary
fallback={<SearchSuggestions suggestions={suggestions} />}
>
<SearchHitsWrapper
configureProps={{
// filters array has to be wrapped
// in another array for an OR condition
// to be applied between the items.
facetFilters: [facetFilters],
getRankingInfo: true,
}}
indices={algolia.indices}
checkInternalPattern={checkInternalPattern}
/>
</SearchEmptyQueryBoundary>
</div>
</InstantSearch>
<SearchFooter />
</div>
)
}