import { forwardRef, Fragment, useEffect, useId, useRef, useState } from 'react'
import { useRouter } from 'next/router'
import { createAutocomplete } from '@algolia/autocomplete-core'
import { getAlgoliaResults } from '@algolia/autocomplete-preset-algolia'
import { Dialog, Transition } from '@headlessui/react'
import algoliasearch from 'algoliasearch/lite'
import clsx from 'clsx'
const searchClient = algoliasearch(
process.env.NEXT_PUBLIC_DOCSEARCH_APP_ID,
process.env.NEXT_PUBLIC_DOCSEARCH_API_KEY
)
function useAutocomplete() {
const id = useId()
const router = useRouter()
const [autocompleteState, setAutocompleteState] = useState({})
const [autocomplete] = useState(() =>
createAutocomplete({
id,
placeholder: 'Find something...',
defaultActiveItemId: 0,
onStateChange({ state }) {
setAutocompleteState(state)
},
shouldPanelOpen({ state }) {
return state.query !== ''
},
navigator: {
navigate({ itemUrl }) {
autocomplete.setIsOpen(true)
router.push(itemUrl)
},
},
getSources() {
return [
{
sourceId: 'documentation',
getItemInputValue({ item }) {
return item.query
},
getItemUrl({ item }) {
const url = new URL(item.url)
return `${url.pathname}${url.hash}`
},
onSelect({ itemUrl }) {
router.push(itemUrl)
},
getItems({ query }) {
return getAlgoliaResults({
searchClient,
queries: [
{
query,
indexName: process.env.NEXT_PUBLIC_DOCSEARCH_INDEX_NAME,
params: {
hitsPerPage: 5,
highlightPreTag:
'',
highlightPostTag: ' ',
},
},
],
})
},
},
]
},
})
)
return { autocomplete, autocompleteState }
}
function resolveResult(result) {
const allLevels = Object.keys(result.hierarchy)
const hierarchy = Object.entries(result._highlightResult.hierarchy).filter(
([, { value }]) => Boolean(value)
)
const levels = hierarchy.map(([level]) => level)
const level =
result.type === 'content'
? levels.pop()
: levels
.filter(
(level) =>
allLevels.indexOf(level) <= allLevels.indexOf(result.type)
)
.pop()
return {
titleHtml: result._highlightResult.hierarchy[level].value,
hierarchyHtml: hierarchy
.slice(0, levels.indexOf(level))
.map(([, { value }]) => value),
}
}
function SearchIcon(props) {
return (
)
}
function NoResultsIcon(props) {
return (
)
}
function LoadingIcon(props) {
const id = useId()
return (
)
}
function SearchResult({ result, resultIndex, autocomplete, collection }) {
const id = useId()
const { titleHtml, hierarchyHtml } = resolveResult(result)
return (
0 && 'border-t border-zinc-100 dark:border-zinc-800'
)}
aria-labelledby={`${id}-hierarchy ${id}-title`}
{...autocomplete.getItemProps({
item: result,
source: collection.source,
})}
>
{hierarchyHtml.length > 0 && (
{hierarchyHtml.map((item, itemIndex, items) => (
/
))}
)}
)
}
function SearchResults({ autocomplete, query, collection }) {
if (collection.items.length === 0) {
return (
Nothing found for{' '}
‘{query}’
. Please try again.
)
}
return (
{collection.items.map((result, resultIndex) => (
))}
)
}
const SearchInput = forwardRef(function SearchInput(
{ autocomplete, autocompleteState, onClose },
inputRef
) {
const inputProps = autocomplete.getInputProps({})
return (
{
if (
event.key === 'Escape' &&
!autocompleteState.isOpen &&
autocompleteState.query === ''
) {
// In Safari, closing the dialog with the escape key can sometimes cause the scroll position to jump to the
// bottom of the page. This is a workaround for that until we can figure out a proper fix in Headless UI.
document.activeElement?.blur()
onClose()
} else {
inputProps.onKeyDown(event)
}
}}
/>
{autocompleteState.status === 'stalled' && (
)}
)
})
function AlgoliaLogo(props) {
return (
)
}
function SearchButton(props) {
const [modifierKey, setModifierKey] = useState()
useEffect(() => {
setModifierKey(
/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? '⌘' : 'Ctrl '
)
}, [])
return (
<>
Find something...
{modifierKey}
K
>
)
}
function SearchDialog({ open, setOpen, className }) {
const router = useRouter()
const formRef = useRef()
const panelRef = useRef()
const inputRef = useRef()
const { autocomplete, autocompleteState } = useAutocomplete()
useEffect(() => {
if (!open) {
return
}
function onRouteChange() {
setOpen(false)
}
router.events.on('routeChangeStart', onRouteChange)
router.events.on('hashChangeStart', onRouteChange)
return () => {
router.events.off('routeChangeStart', onRouteChange)
router.events.off('hashChangeStart', onRouteChange)
}
}, [open, setOpen, router])
useEffect(() => {
if (open) {
return
}
function onKeyDown(event) {
if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
event.preventDefault()
setOpen(true)
}
}
window.addEventListener('keydown', onKeyDown)
return () => {
window.removeEventListener('keydown', onKeyDown)
}
}, [open, setOpen])
return (
autocomplete.setQuery('')}
>
)
}
function useSearchProps() {
const buttonRef = useRef()
const [open, setOpen] = useState(false)
return {
buttonProps: {
ref: buttonRef,
onClick() {
setOpen(true)
},
},
dialogProps: {
open,
setOpen(open) {
const { width, height } = buttonRef.current.getBoundingClientRect()
if (!open || (width !== 0 && height !== 0)) {
setOpen(open)
}
},
},
}
}
export function Search() {
const [modifierKey, setModifierKey] = useState()
const { buttonProps, dialogProps } = useSearchProps()
useEffect(() => {
setModifierKey(
/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? '⌘' : 'Ctrl '
)
}, [])
return (
Find something...
{modifierKey}
K
)
}
export function MobileSearch() {
const { buttonProps, dialogProps } = useSearchProps()
return (
)
}