210 lines
5.9 KiB
TypeScript
210 lines
5.9 KiB
TypeScript
import clsx from "clsx"
|
|
import Link from "next/link"
|
|
import { Fragment, useEffect, useMemo, useState } from "react"
|
|
import {
|
|
Configure,
|
|
ConfigureProps,
|
|
Index,
|
|
Snippet,
|
|
useHits,
|
|
useInstantSearch,
|
|
} from "react-instantsearch"
|
|
import SearchNoResult from "../NoResults"
|
|
import SearchHitGroupName from "./GroupName"
|
|
import getBaseUrl from "../../../utils/get-base-url"
|
|
import { useSearch } from "../../../providers/search"
|
|
|
|
type Hierarchy = "lvl0" | "lvl1" | "lvl2" | "lvl3" | "lvl4" | "lvl5"
|
|
|
|
export type HitType = {
|
|
hierarchy: {
|
|
lvl0: string | null
|
|
lvl1: string | null
|
|
lvl2: string | null
|
|
lvl3: string | null
|
|
lvl4: string | null
|
|
lvl5: string | null
|
|
}
|
|
_tags: string[]
|
|
url: string
|
|
type?: "lvl1" | "lvl2" | "lvl3" | "lvl4" | "lvl5" | "content"
|
|
content?: string
|
|
__position: number
|
|
__queryID?: string
|
|
objectID: string
|
|
}
|
|
|
|
type GroupedHitType = {
|
|
[k: string]: HitType[]
|
|
}
|
|
|
|
type SearchHitWrapperProps = {
|
|
configureProps: ConfigureProps
|
|
}
|
|
|
|
type IndexResults = {
|
|
[k: string]: boolean
|
|
}
|
|
|
|
const SearchHitsWrapper = ({ configureProps }: SearchHitWrapperProps) => {
|
|
const { status } = useInstantSearch()
|
|
const indices = useMemo(
|
|
() => [
|
|
process.env.NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME || "temp",
|
|
process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp",
|
|
],
|
|
[]
|
|
)
|
|
const [hasNoResults, setHashNoResults] = useState<IndexResults>({
|
|
[indices[0]]: false,
|
|
[indices[1]]: false,
|
|
})
|
|
const showNoResults = useMemo(() => {
|
|
return Object.values(hasNoResults).every((value) => value === true)
|
|
}, [hasNoResults])
|
|
|
|
const setNoResults = (index: string, value: boolean) => {
|
|
setHashNoResults((prev: IndexResults) => ({
|
|
...prev,
|
|
[index]: value,
|
|
}))
|
|
}
|
|
|
|
return (
|
|
<div className="h-full overflow-auto">
|
|
{status !== "loading" && showNoResults && <SearchNoResult />}
|
|
{indices.map((indexName, index) => (
|
|
<Index indexName={indexName} key={index}>
|
|
<SearchHits indexName={indexName} setNoResults={setNoResults} />
|
|
<Configure {...configureProps} />
|
|
</Index>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
type SearchHitsProps = {
|
|
indexName: string
|
|
setNoResults: (index: string, value: boolean) => void
|
|
}
|
|
|
|
const SearchHits = ({ indexName, setNoResults }: SearchHitsProps) => {
|
|
const { hits } = useHits<HitType>()
|
|
const { status } = useInstantSearch()
|
|
const { setIsOpen } = useSearch()
|
|
|
|
// group by lvl0
|
|
const grouped = useMemo(() => {
|
|
const grouped: GroupedHitType = {}
|
|
hits.forEach((hit) => {
|
|
if (hit.hierarchy.lvl0) {
|
|
if (!grouped[hit.hierarchy.lvl0]) {
|
|
grouped[hit.hierarchy.lvl0] = []
|
|
}
|
|
grouped[hit.hierarchy.lvl0].push(hit)
|
|
}
|
|
})
|
|
|
|
return grouped
|
|
}, [hits])
|
|
|
|
useEffect(() => {
|
|
if (status !== "loading" && status !== "stalled") {
|
|
setNoResults(indexName, hits.length === 0)
|
|
}
|
|
}, [hits, status])
|
|
|
|
const getLastAvailableHeirarchy = (item: HitType) => {
|
|
return (
|
|
Object.keys(item.hierarchy)
|
|
.reverse()
|
|
.find((key) => item.hierarchy[key as Hierarchy] !== null) || ""
|
|
)
|
|
}
|
|
|
|
const baseUrl = useMemo(() => getBaseUrl(), [])
|
|
|
|
const checkIfInternal = (url: string): boolean => {
|
|
const testRegex = new RegExp(`^${baseUrl}/api/(admin|store)`)
|
|
return testRegex.test(url)
|
|
}
|
|
|
|
return (
|
|
<div className="overflow-auto">
|
|
{Object.keys(grouped).map((groupName, index) => (
|
|
<Fragment key={index}>
|
|
<SearchHitGroupName name={groupName} />
|
|
{grouped[groupName].map((item, index) => (
|
|
<div
|
|
className={clsx(
|
|
"gap-0.25 relative flex flex-1 flex-col p-0.5",
|
|
"overflow-x-hidden text-ellipsis whitespace-nowrap break-words",
|
|
"hover:bg-medusa-bg-base-hover dark:hover:bg-medusa-bg-base-hover-dark",
|
|
"focus:bg-medusa-bg-base-hover dark:focus:bg-medusa-bg-base-hover-dark",
|
|
"last:mb-1 focus:outline-none"
|
|
)}
|
|
key={index}
|
|
tabIndex={index}
|
|
data-hit
|
|
onClick={(e) => {
|
|
const target = e.target as Element
|
|
if (target.tagName.toLowerCase() === "div") {
|
|
target.querySelector("a")?.click()
|
|
}
|
|
}}
|
|
>
|
|
<span
|
|
className={clsx(
|
|
"text-compact-small-plus text-medusa-fg-base dark:text-medusa-fg-base-dark",
|
|
"max-w-full"
|
|
)}
|
|
>
|
|
<Snippet
|
|
attribute={[
|
|
"hierarchy",
|
|
item.type && item.type !== "content"
|
|
? item.type
|
|
: item.hierarchy.lvl1
|
|
? "lvl1"
|
|
: getLastAvailableHeirarchy(item),
|
|
]}
|
|
hit={item}
|
|
/>
|
|
</span>
|
|
{item.type !== "lvl1" && (
|
|
<span className="text-compact-small text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark">
|
|
<Snippet
|
|
attribute={
|
|
item.content
|
|
? "content"
|
|
: [
|
|
"hierarchy",
|
|
item.type || getLastAvailableHeirarchy(item),
|
|
]
|
|
}
|
|
hit={item}
|
|
/>
|
|
</span>
|
|
)}
|
|
<Link
|
|
href={item.url}
|
|
className="absolute top-0 left-0 h-full w-full"
|
|
target="_self"
|
|
onClick={(e) => {
|
|
if (checkIfInternal(item.url)) {
|
|
e.preventDefault()
|
|
window.location.href = item.url
|
|
setIsOpen(false)
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
))}
|
|
</Fragment>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default SearchHitsWrapper
|