docs,api-ref: added search filters (#4830)
* initial implementation of search modal * added hit and search suggestions * added support for multiple indices * updated sample env * added close when click outside dropdown * test for mobile * added mobile design * added shortcut * dark mode fixes * added search to docs * added plugins filter * added React import * moved filters to configurations * handled error on page load * change suggestion text * removed hits limit * handle select all * open link in current tab * change highlight colors * added support for shortcuts + auto focus * change header and footer * redesigned search ui
This commit is contained in:
22
www/docs/src/components/Search/EmptyQueryBoundary/index.tsx
Normal file
22
www/docs/src/components/Search/EmptyQueryBoundary/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from "react"
|
||||
import { useInstantSearch } from "react-instantsearch"
|
||||
|
||||
type SearchEmptyQueryBoundaryProps = {
|
||||
children: React.ReactElement
|
||||
fallback: React.ReactElement
|
||||
}
|
||||
|
||||
const SearchEmptyQueryBoundary = ({
|
||||
children,
|
||||
fallback,
|
||||
}: SearchEmptyQueryBoundaryProps) => {
|
||||
const { indexUiState } = useInstantSearch()
|
||||
|
||||
if (!indexUiState.query) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
export default SearchEmptyQueryBoundary
|
||||
22
www/docs/src/components/Search/Hits/GroupName/index.tsx
Normal file
22
www/docs/src/components/Search/Hits/GroupName/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from "react"
|
||||
import clsx from "clsx"
|
||||
|
||||
type SearchHitGroupNameProps = {
|
||||
name: string
|
||||
}
|
||||
|
||||
const SearchHitGroupName = ({ name }: SearchHitGroupNameProps) => {
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
"pb-0.25 flex px-0.5 pt-1",
|
||||
"text-medusa-fg-muted dark:text-medusa-fg-muted-dark",
|
||||
"text-compact-x-small-plus"
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchHitGroupName
|
||||
190
www/docs/src/components/Search/Hits/index.tsx
Normal file
190
www/docs/src/components/Search/Hits/index.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import React from "react"
|
||||
import clsx from "clsx"
|
||||
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 Link from "@docusaurus/Link"
|
||||
import { useThemeConfig } from "@docusaurus/theme-common"
|
||||
import { ThemeConfig } from "@medusajs/docs"
|
||||
|
||||
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 { algoliaConfig: algolia } = useThemeConfig() as ThemeConfig
|
||||
const indices = useMemo(() => Object.values(algolia.indexNames), [])
|
||||
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()
|
||||
|
||||
// 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) || ""
|
||||
)
|
||||
}
|
||||
|
||||
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",
|
||||
"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"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchHitsWrapper
|
||||
312
www/docs/src/components/Search/Modal/index.tsx
Normal file
312
www/docs/src/components/Search/Modal/index.tsx
Normal file
@@ -0,0 +1,312 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react"
|
||||
import algoliasearch, { SearchClient } from "algoliasearch/lite"
|
||||
import { InstantSearch, SearchBox } from "react-instantsearch"
|
||||
import Modal from "../../Modal"
|
||||
import clsx from "clsx"
|
||||
import SearchEmptyQueryBoundary from "../EmptyQueryBoundary"
|
||||
import SearchSuggestions from "../Suggestions"
|
||||
import checkArraySameElms from "../../../utils/array-same-elms"
|
||||
import SearchHitsWrapper from "../Hits"
|
||||
import Button from "../../Button"
|
||||
import useKeyboardShortcut from "../../../hooks/use-keyboard-shortcut"
|
||||
import { OptionType } from "@medusajs/docs"
|
||||
import { useSearch } from "../../../providers/Search"
|
||||
import { findNextSibling, findPrevSibling } from "../../../utils/dom-utils"
|
||||
import IconMagnifyingGlass from "../../../theme/Icon/MagnifyingGlass"
|
||||
import IconXMark from "../../../theme/Icon/XMark"
|
||||
import SelectBadge from "../../Select/Badge"
|
||||
import Kbd from "../../../theme/MDXComponents/Kbd"
|
||||
import { useThemeConfig } from "@docusaurus/theme-common"
|
||||
import { ThemeConfig } from "@medusajs/docs"
|
||||
|
||||
const SearchModal = () => {
|
||||
const modalRef = useRef<HTMLDialogElement | null>(null)
|
||||
const { algoliaConfig: algolia } = useThemeConfig() as ThemeConfig
|
||||
|
||||
if (!algolia) {
|
||||
throw new Error("Algolia configuration is not defined")
|
||||
}
|
||||
|
||||
const algoliaClient = useMemo(() => {
|
||||
return algoliasearch(algolia.appId, algolia.apiKey)
|
||||
}, [])
|
||||
|
||||
const searchClient: SearchClient = useMemo(() => {
|
||||
return {
|
||||
...algoliaClient,
|
||||
async search(requests) {
|
||||
if (requests.every(({ params }) => !params?.query)) {
|
||||
return Promise.resolve({
|
||||
results: requests.map(() => ({
|
||||
hits: [],
|
||||
nbHits: 0,
|
||||
nbPages: 0,
|
||||
page: 0,
|
||||
processingTimeMS: 0,
|
||||
hitsPerPage: 0,
|
||||
exhaustiveNbHits: false,
|
||||
query: "",
|
||||
params: "",
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
return algoliaClient.search(requests)
|
||||
},
|
||||
}
|
||||
}, [])
|
||||
|
||||
const options: OptionType[] = algolia.filters
|
||||
const { isOpen, setIsOpen, defaultFilters } = useSearch()
|
||||
const [filters, setFilters] = useState<string[]>(defaultFilters)
|
||||
const formattedFilters: string = useMemo(() => {
|
||||
let formatted = ""
|
||||
filters.forEach((filter) => {
|
||||
const split = filter.split("_")
|
||||
split.forEach((f) => {
|
||||
if (formatted.length) {
|
||||
formatted += " OR "
|
||||
}
|
||||
formatted += `_tags:${f}`
|
||||
})
|
||||
})
|
||||
return formatted
|
||||
}, [filters])
|
||||
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()
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const handleKeyAction = (e: KeyboardEvent) => {
|
||||
if (!isOpen) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
const focusedItem = modalRef.current?.querySelector(":focus") as HTMLElement
|
||||
if (!focusedItem) {
|
||||
// focus the first data-hit
|
||||
const nextItem = modalRef.current?.querySelector(
|
||||
"[data-hit]"
|
||||
) as HTMLElement
|
||||
nextItem?.focus()
|
||||
return
|
||||
}
|
||||
|
||||
const isHit = focusedItem.hasAttribute("data-hit")
|
||||
const isInput = focusedItem.tagName.toLowerCase() === "input"
|
||||
|
||||
if (!isHit && !isInput) {
|
||||
// ignore if focused items aren't input/data-hit
|
||||
return
|
||||
}
|
||||
|
||||
const lowerPressedKey = e.key.toLowerCase()
|
||||
|
||||
if (lowerPressedKey === "enter") {
|
||||
if (isHit) {
|
||||
// trigger click event of the focused element
|
||||
focusedItem.click()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (lowerPressedKey === "arrowup") {
|
||||
// only hit items has action on arrow up
|
||||
if (isHit) {
|
||||
// find if there's a data-hit item before this one
|
||||
const beforeItem = findPrevSibling(focusedItem, "[data-hit]")
|
||||
if (!beforeItem) {
|
||||
// focus the input
|
||||
focusSearchInput()
|
||||
} else {
|
||||
// focus the previous item
|
||||
beforeItem.focus()
|
||||
}
|
||||
}
|
||||
} else if (lowerPressedKey === "arrowdown") {
|
||||
// check if item is input or hit
|
||||
if (isInput) {
|
||||
// go to the first data-hit item
|
||||
const nextItem = modalRef.current?.querySelector(
|
||||
"[data-hit]"
|
||||
) as HTMLElement
|
||||
nextItem?.focus()
|
||||
} else {
|
||||
// handle go down for hit items
|
||||
// find if there's a data-hit item after this one
|
||||
const afterItem = findNextSibling(focusedItem, "[data-hit]")
|
||||
if (afterItem) {
|
||||
// focus the next item
|
||||
afterItem.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useKeyboardShortcut({
|
||||
metakey: false,
|
||||
shortcutKeys: ["ArrowUp", "ArrowDown", "Enter"],
|
||||
action: handleKeyAction,
|
||||
checkEditing: false,
|
||||
preventDefault: false,
|
||||
})
|
||||
|
||||
return (
|
||||
<Modal
|
||||
contentClassName={clsx(
|
||||
"!p-0 overflow-hidden relative h-full",
|
||||
"rounded-none md:rounded-lg flex flex-col justify-between"
|
||||
)}
|
||||
modalContainerClassName="w-screen h-screen !rounded-none md:!rounded-lg"
|
||||
open={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
ref={modalRef}
|
||||
>
|
||||
<InstantSearch
|
||||
indexName={algolia.indexNames.docs}
|
||||
searchClient={searchClient}
|
||||
>
|
||||
<div
|
||||
className={clsx("bg-medusa-bg-base dark:bg-medusa-bg-base-dark flex")}
|
||||
>
|
||||
<SearchBox
|
||||
classNames={{
|
||||
root: clsx(
|
||||
"h-[56px] w-full md:rounded-t-xl relative border-0 border-solid",
|
||||
"border-b border-medusa-border-base dark:border-medusa-border-base-dark",
|
||||
"bg-transparent"
|
||||
),
|
||||
form: clsx("h-full md:rounded-t-xl bg-transparent"),
|
||||
input: clsx(
|
||||
"w-full h-full pl-3 text-medusa-fg-base dark:text-medusa-fg-base-dark",
|
||||
"placeholder:text-medusa-fg-muted dark:placeholder:text-medusa-fg-muted-dark",
|
||||
"md:rounded-t-xl text-compact-medium bg-transparent",
|
||||
"appearance-none search-cancel:hidden border-0 active:outline-none focus:outline-none"
|
||||
),
|
||||
submit: clsx("absolute top-[18px] left-1 btn-clear p-0"),
|
||||
reset: clsx(
|
||||
"absolute top-0.75 right-1 hover:bg-medusa-bg-base-hover dark:hover:bg-medusa-bg-base-hover-dark",
|
||||
"p-[5px] md:rounded btn-clear"
|
||||
),
|
||||
loadingIndicator: clsx("absolute top-[18px] right-1"),
|
||||
}}
|
||||
submitIconComponent={() => (
|
||||
<IconMagnifyingGlass iconColorClassName="stroke-medusa-fg-muted dark:stroke-medusa-fg-muted-dark" />
|
||||
)}
|
||||
resetIconComponent={() => (
|
||||
<IconXMark
|
||||
iconColorClassName="stroke-medusa-fg-subtle dark:stroke-medusa-fg-subtle-dark"
|
||||
className="hidden md:block"
|
||||
/>
|
||||
)}
|
||||
placeholder="Find something..."
|
||||
autoFocus
|
||||
formRef={searchBoxRef}
|
||||
/>
|
||||
<Button
|
||||
variant="clear"
|
||||
className={clsx(
|
||||
"bg-medusa-bg-base dark:bg-medusa-bg-base-dark block md:hidden",
|
||||
"border-0 border-solid",
|
||||
"border-medusa-border-base dark:border-medusa-border-base-dark border-b",
|
||||
"pr-1"
|
||||
)}
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
<IconXMark iconColorClassName="stroke-medusa-fg-muted dark:stroke-medusa-fg-muted-dark" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mx-0.5 h-[calc(100%-120px)] md:h-[332px] md:flex-initial lg:max-h-[332px] lg:min-h-[332px]">
|
||||
<SearchEmptyQueryBoundary fallback={<SearchSuggestions />}>
|
||||
<SearchHitsWrapper
|
||||
configureProps={{
|
||||
filters: formattedFilters,
|
||||
attributesToSnippet: [
|
||||
"content",
|
||||
"hierarchy.lvl1",
|
||||
"hierarchy.lvl2",
|
||||
],
|
||||
attributesToHighlight: [
|
||||
"content",
|
||||
"hierarchy.lvl1",
|
||||
"hierarchy.lvl2",
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</SearchEmptyQueryBoundary>
|
||||
</div>
|
||||
</InstantSearch>
|
||||
<div
|
||||
className={clsx(
|
||||
"py-0.75 flex items-center justify-between px-1",
|
||||
"border-0 border-solid",
|
||||
"border-medusa-border-base dark:border-medusa-border-base-dark border-t",
|
||||
"bg-medusa-bg-base dark:bg-medusa-bg-base-dark"
|
||||
)}
|
||||
>
|
||||
<SelectBadge
|
||||
multiple
|
||||
options={options}
|
||||
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(options.map((option) => option.value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="hidden items-center gap-1 md:flex">
|
||||
<div className="flex items-center gap-0.5">
|
||||
<span
|
||||
className={clsx(
|
||||
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
|
||||
"text-compact-x-small"
|
||||
)}
|
||||
>
|
||||
Navigation
|
||||
</span>
|
||||
<span className="gap-0.25 flex">
|
||||
<Kbd>↑</Kbd>
|
||||
<Kbd>↓</Kbd>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<span
|
||||
className={clsx(
|
||||
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
|
||||
"text-compact-x-small"
|
||||
)}
|
||||
>
|
||||
Open Result
|
||||
</span>
|
||||
<Kbd>↵</Kbd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchModal
|
||||
78
www/docs/src/components/Search/ModalOpener/index.tsx
Normal file
78
www/docs/src/components/Search/ModalOpener/index.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import React from "react"
|
||||
import clsx from "clsx"
|
||||
import InputText from "../../Input/Text"
|
||||
import { MouseEvent, useMemo } from "react"
|
||||
import { useSearch } from "../../../providers/Search"
|
||||
import { useWindowSize } from "@docusaurus/theme-common"
|
||||
import Button from "../../Button"
|
||||
import IconMagnifyingGlass from "../../../theme/Icon/MagnifyingGlass"
|
||||
import Kbd from "../../../theme/MDXComponents/Kbd"
|
||||
import useKeyboardShortcut from "../../../hooks/use-keyboard-shortcut"
|
||||
|
||||
const SearchModalOpener = () => {
|
||||
const { setIsOpen } = useSearch()
|
||||
const windowSize = useWindowSize()
|
||||
const isApple = useMemo(() => {
|
||||
return typeof navigator !== "undefined"
|
||||
? navigator.userAgent.toLowerCase().indexOf("mac") !== 0
|
||||
: true
|
||||
}, [])
|
||||
useKeyboardShortcut({
|
||||
shortcutKeys: ["k"],
|
||||
action: () => setIsOpen((prev) => !prev),
|
||||
})
|
||||
|
||||
const handleOpen = (
|
||||
e:
|
||||
| MouseEvent<HTMLDivElement, globalThis.MouseEvent>
|
||||
| MouseEvent<HTMLInputElement, globalThis.MouseEvent>
|
||||
| MouseEvent<HTMLButtonElement, globalThis.MouseEvent>
|
||||
) => {
|
||||
e.preventDefault()
|
||||
if ("blur" in e.target && typeof e.target.blur === "function") {
|
||||
e.target.blur()
|
||||
}
|
||||
setIsOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{windowSize !== "desktop" && (
|
||||
<Button variant="clear" onClick={handleOpen}>
|
||||
<IconMagnifyingGlass iconColorClassName="stroke-medusa-fg-muted dark:stroke-medusa-fg-muted-dark" />
|
||||
</Button>
|
||||
)}
|
||||
{windowSize === "desktop" && (
|
||||
<div
|
||||
className={clsx("relative w-min hover:cursor-pointer")}
|
||||
onClick={handleOpen}
|
||||
>
|
||||
<IconMagnifyingGlass
|
||||
iconColorClassName="stroke-medusa-fg-muted dark:stroke-medusa-fg-muted-dark"
|
||||
className={clsx("absolute left-0.5 top-[5px]")}
|
||||
/>
|
||||
<InputText
|
||||
type="search"
|
||||
className={clsx(
|
||||
"placeholder:text-compact-small",
|
||||
"!py-[5px] !pl-[36px] !pr-[8px]",
|
||||
"cursor-pointer select-none"
|
||||
)}
|
||||
placeholder="Find something"
|
||||
onClick={handleOpen}
|
||||
onFocus={(e) => e.target.blur()}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<span
|
||||
className={clsx("gap-0.25 flex", "absolute right-0.5 top-[5px]")}
|
||||
>
|
||||
<Kbd>{isApple ? "⌘" : "Ctrl"}</Kbd>
|
||||
<Kbd>K</Kbd>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchModalOpener
|
||||
15
www/docs/src/components/Search/NoResults/index.tsx
Normal file
15
www/docs/src/components/Search/NoResults/index.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from "react"
|
||||
import IconExclamationCircleSolid from "../../../theme/Icon/ExclamationCircleSolid"
|
||||
|
||||
const SearchNoResult = () => {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-1">
|
||||
<IconExclamationCircleSolid iconColorClassName="fill-medusa-fg-muted dark:fill-medusa-fg-muted-dark" />
|
||||
<span className="text-compact-small text-medusa-fg-muted dark:text-medusa-fg-muted-dark">
|
||||
No results found. Try changing selected filters.
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchNoResult
|
||||
69
www/docs/src/components/Search/Suggestions/index.tsx
Normal file
69
www/docs/src/components/Search/Suggestions/index.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React from "react"
|
||||
import clsx from "clsx"
|
||||
import { useInstantSearch } from "react-instantsearch"
|
||||
import SearchHitGroupName from "../Hits/GroupName"
|
||||
|
||||
const SearchSuggestions = () => {
|
||||
const { setIndexUiState } = useInstantSearch()
|
||||
const suggestions = [
|
||||
{
|
||||
title: "Getting started? Try one of the following terms.",
|
||||
items: [
|
||||
"Install Medusa with create-medusa-app",
|
||||
"Next.js quickstart",
|
||||
"Admin dashboard quickstart",
|
||||
"Commerce modules",
|
||||
"Medusa architecture",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Developing with Medusa",
|
||||
items: [
|
||||
"Recipes",
|
||||
"How to create endpoints",
|
||||
"How to create an entity",
|
||||
"How to create a plugin",
|
||||
"How to create an admin widget",
|
||||
],
|
||||
},
|
||||
]
|
||||
return (
|
||||
<div className="h-full overflow-auto">
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<SearchHitGroupName name={suggestion.title} />
|
||||
{suggestion.items.map((item, itemIndex) => (
|
||||
<div
|
||||
className={clsx(
|
||||
"flex items-center justify-between",
|
||||
"cursor-pointer rounded-sm p-0.5",
|
||||
"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",
|
||||
"focus:outline-none"
|
||||
)}
|
||||
onClick={() =>
|
||||
setIndexUiState({
|
||||
query: item,
|
||||
})
|
||||
}
|
||||
key={itemIndex}
|
||||
tabIndex={itemIndex}
|
||||
data-hit
|
||||
>
|
||||
<span
|
||||
className={clsx(
|
||||
"text-medusa-fg-base dark:text-medusa-fg-base-dark",
|
||||
"text-compact-small"
|
||||
)}
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchSuggestions
|
||||
Reference in New Issue
Block a user