-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement SearchBar using Algolia (#21)
* Implement SearchBar using Algolia * Prettier. * Add missing @algolia/autocomplete-shared * Removing incorrect dependency. * Update. * Add react-instantsearch types. * Add pagination. * Fix unfocus issue. * Fix dependencies. * Fix ts issues. --------- Co-authored-by: James Kwon <[email protected]> Co-authored-by: Robin Huang <[email protected]>
- Loading branch information
1 parent
e78f057
commit 2d6e1cc
Showing
12 changed files
with
2,014 additions
and
1,078 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
import type { SearchClient } from 'algoliasearch/lite' | ||
import type { BaseItem } from '@algolia/autocomplete-core' | ||
import type { AutocompleteOptions } from '@algolia/autocomplete-js' | ||
|
||
import { | ||
createElement, | ||
Fragment, | ||
useEffect, | ||
useMemo, | ||
useRef, | ||
useState, | ||
} from 'react' | ||
import { createRoot, Root } from 'react-dom/client' | ||
|
||
import { usePagination, useSearchBox } from 'react-instantsearch' | ||
import { autocomplete } from '@algolia/autocomplete-js' | ||
import { createLocalStorageRecentSearchesPlugin } from '@algolia/autocomplete-plugin-recent-searches' | ||
import { createQuerySuggestionsPlugin } from '@algolia/autocomplete-plugin-query-suggestions' | ||
// @ts-ignore | ||
import { debounce } from '@algolia/autocomplete-shared' | ||
|
||
import { INSTANT_SEARCH_QUERY_SUGGESTIONS } from 'src/constants' | ||
|
||
import '@algolia/autocomplete-theme-classic' | ||
|
||
type AutocompleteProps = Partial<AutocompleteOptions<BaseItem>> & { | ||
searchClient: SearchClient | ||
className?: string | ||
} | ||
|
||
type SetInstantSearchUiStateOptions = { | ||
query: string | ||
} | ||
|
||
export default function Autocomplete({ | ||
searchClient, | ||
className, | ||
...autocompleteProps | ||
}: AutocompleteProps) { | ||
const autocompleteContainer = useRef<HTMLDivElement>(null) | ||
const panelRootRef = useRef<Root | null>(null) | ||
const rootRef = useRef<HTMLElement | null>(null) | ||
|
||
const { query, refine: setQuery } = useSearchBox() | ||
|
||
const { refine: setPage } = usePagination() | ||
|
||
const [instantSearchUiState, setInstantSearchUiState] = | ||
useState<SetInstantSearchUiStateOptions>({ query }) | ||
const debouncedSetInstantSearchUiState = debounce( | ||
setInstantSearchUiState, | ||
500 | ||
) | ||
|
||
useEffect(() => { | ||
setQuery(instantSearchUiState.query) | ||
setPage(0) | ||
}, [instantSearchUiState, setQuery]) | ||
|
||
const plugins = useMemo(() => { | ||
const recentSearches = createLocalStorageRecentSearchesPlugin({ | ||
key: 'instantsearch', | ||
limit: 3, | ||
transformSource({ source }) { | ||
return { | ||
...source, | ||
onSelect({ item }) { | ||
setInstantSearchUiState({ query: item.label }) | ||
}, | ||
} | ||
}, | ||
}) | ||
|
||
const querySuggestions = createQuerySuggestionsPlugin({ | ||
searchClient, | ||
indexName: INSTANT_SEARCH_QUERY_SUGGESTIONS, | ||
getSearchParams() { | ||
return recentSearches.data!.getAlgoliaSearchParams({ | ||
hitsPerPage: 6, | ||
}) | ||
}, | ||
transformSource({ source }) { | ||
return { | ||
...source, | ||
sourceId: 'querySuggestionsPlugin', | ||
onSelect({ item }) { | ||
setInstantSearchUiState({ | ||
query: item.query, | ||
}) | ||
}, | ||
getItems(params) { | ||
if (!params.state.query) { | ||
return [] | ||
} | ||
|
||
return source.getItems(params) | ||
}, | ||
templates: { | ||
...source.templates, | ||
header({ items }) { | ||
if (items.length === 0) { | ||
return <Fragment /> | ||
} | ||
|
||
return ( | ||
<Fragment> | ||
<span className="aa-SourceHeaderTitle"> | ||
In other categories | ||
</span> | ||
<span className="aa-SourceHeaderLine" /> | ||
</Fragment> | ||
) | ||
}, | ||
}, | ||
} | ||
}, | ||
}) | ||
|
||
return [recentSearches, querySuggestions] | ||
}, []) | ||
|
||
useEffect(() => { | ||
if (!autocompleteContainer.current) { | ||
return | ||
} | ||
|
||
const autocompleteInstance = autocomplete({ | ||
...autocompleteProps, | ||
container: autocompleteContainer.current, | ||
initialState: { query }, | ||
insights: true, | ||
plugins, | ||
onReset() { | ||
setInstantSearchUiState({ | ||
query: '', | ||
}) | ||
}, | ||
onSubmit({ state }) { | ||
setInstantSearchUiState({ query: state.query }) | ||
}, | ||
onStateChange({ prevState, state }) { | ||
if (prevState.query !== state.query) { | ||
debouncedSetInstantSearchUiState({ query: state.query }) | ||
} | ||
}, | ||
renderer: { createElement, Fragment, render: () => {} }, | ||
render({ children }, root) { | ||
if (!panelRootRef.current || rootRef.current !== root) { | ||
rootRef.current = root | ||
panelRootRef.current?.unmount() | ||
panelRootRef.current = createRoot(root) | ||
} | ||
|
||
panelRootRef.current.render(children) | ||
}, | ||
}) | ||
|
||
return () => autocompleteInstance.destroy() | ||
}, [plugins]) | ||
|
||
return <div className={className} ref={autocompleteContainer} /> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import * as React from 'react' | ||
import { useInstantSearch } from 'react-instantsearch' | ||
|
||
type EmptyQueryBoundaryProps = { | ||
children: React.ReactNode | ||
fallback: React.ReactNode | ||
} | ||
|
||
const EmptyQueryBoundary: React.FC<EmptyQueryBoundaryProps> = ({ | ||
children, | ||
fallback, | ||
}) => { | ||
const { indexUiState } = useInstantSearch() | ||
|
||
// Render the fallback if the query is empty or too short | ||
if (!indexUiState.query || indexUiState.query.length <= 1) { | ||
return ( | ||
<> | ||
{fallback} | ||
<div hidden>{children}</div> | ||
</> | ||
) | ||
} | ||
|
||
// Render children if the query is valid | ||
return <>{children}</> | ||
} | ||
|
||
export default EmptyQueryBoundary |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import React from 'react' | ||
import { Snippet } from 'react-instantsearch' | ||
|
||
import { useRouter } from 'next/router' | ||
|
||
interface NodeHit { | ||
id: string | ||
name: string | ||
publisher_id: string | ||
total_install: number | ||
version: string | ||
} | ||
|
||
type HitProps = { | ||
hit: NodeHit | ||
} | ||
|
||
const Hit: React.FC<HitProps> = ({ hit }) => { | ||
const router = useRouter() | ||
|
||
const handleClick = () => { | ||
router.push(`/nodes/${hit.id}`) | ||
} | ||
|
||
return ( | ||
<div | ||
className="flex flex-col bg-gray-800 rounded-lg shadow cursor-pointer h-full dark:border-gray-700 lg:p-4" | ||
onClick={handleClick} | ||
> | ||
<div className="flex flex-col px-4"> | ||
<h6 className="mb-2 text-base font-bold tracking-tight text-white break-words"> | ||
{/* @ts-ignore */} | ||
<Snippet hit={hit} attribute="name" /> | ||
</h6> | ||
|
||
{hit.version && ( | ||
<p className="mb-1 text-xs tracking-tight text-white"> | ||
<span>v{hit.version}</span> | ||
</p> | ||
)} | ||
|
||
<p className="mb-1 text-xs font-light text-white text-nowrap mt-2"> | ||
{hit.publisher_id} | ||
</p> | ||
|
||
<div className="flex items-center flex-start align-center gap-1 mt-2"> | ||
{hit.total_install != 0 && ( | ||
<p className="flex justify-center text-center align-center"> | ||
<svg | ||
className="w-4 h-4 text-white" | ||
aria-hidden="true" | ||
xmlns="http://www.w3.org/2000/svg" | ||
width="24" | ||
height="24" | ||
fill="none" | ||
viewBox="0 0 24 24" | ||
> | ||
<path | ||
stroke="currentColor" | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
strokeWidth="2" | ||
d="M12 13V4M7 14H5a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-4a1 1 0 0 0-1-1h-2m-1-5-4 5-4-5m9 8h.01" | ||
/> | ||
</svg> | ||
<p className="ml-1 text-xs font-bold text-white"> | ||
{hit.total_install} | ||
</p> | ||
</p> | ||
)} | ||
</div> | ||
</div> | ||
</div> | ||
) | ||
} | ||
|
||
export default Hit |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './Autocomplete' | ||
export * from './EmptyQueryBoundary' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import React from 'react' | ||
import { Pagination as FlowbitePagination } from 'flowbite-react' | ||
import { usePagination, UsePaginationProps } from 'react-instantsearch' | ||
import { CustomThemePagination } from 'utils/comfyTheme' | ||
|
||
export default function CustomSearchPagination(props: UsePaginationProps) { | ||
const { | ||
pages, | ||
currentRefinement, | ||
nbPages, | ||
isFirstPage, | ||
isLastPage, | ||
refine, | ||
createURL, | ||
} = usePagination(props) | ||
|
||
const handlePageChange = (page: number) => { | ||
refine(page - 1) // Flowbite uses 1-based indexing, InstantSearch uses 0-based | ||
} | ||
|
||
return ( | ||
<div className="flex mt-2 sm:justify-center"> | ||
<FlowbitePagination | ||
theme={CustomThemePagination as any} // Add 'as any' to bypass type checking | ||
currentPage={currentRefinement + 1} | ||
totalPages={nbPages} | ||
onPageChange={handlePageChange} | ||
showIcons={true} | ||
previousLabel="Previous" | ||
nextLabel="Next" | ||
layout="pagination" | ||
/> | ||
</div> | ||
) | ||
} | ||
|
||
function isModifierClick(event: React.MouseEvent) { | ||
const isMiddleClick = event.button === 1 | ||
return Boolean( | ||
isMiddleClick || | ||
event.altKey || | ||
event.ctrlKey || | ||
event.metaKey || | ||
event.shiftKey | ||
) | ||
} |
Oops, something went wrong.