import { SearchIcon } from '@chakra-ui/icons'
import {
Box,
Center,
Flex,
Modal,
ModalBody,
ModalContent,
ModalOverlay,
chakra,
useDisclosure,
useEventListener,
useUpdateEffect,
} from '@chakra-ui/react'
import { findAll } from 'highlight-words-core'
import { matchSorter } from 'match-sorter'
import Link from 'next/link'
import { useRouter } from 'next/router'
import * as React from 'react'
import MultiRef from 'react-multi-ref'
import scrollIntoView from 'scroll-into-view-if-needed'
import { SearchButton } from './algolia-search'
import searchData from 'configs/search-meta.json'
interface OptionTextProps {
searchWords: string[]
textToHighlight: string
}
function OptionText({ searchWords, textToHighlight }: OptionTextProps) {
const chunks = findAll({
searchWords,
textToHighlight,
autoEscape: true,
})
const highlightedText = chunks.map((chunk, id) => {
const { end, highlight, start } = chunk
const text = textToHighlight.substr(start, end - start)
if (highlight) {
return (
{text}
)
} else {
return text
}
})
return highlightedText
}
function DocIcon(props) {
return (
)
}
function EnterIcon(props) {
return (
)
}
function HashIcon(props) {
return (
)
}
function OmniSearch() {
const router = useRouter()
const [query, setQuery] = React.useState('')
const [active, setActive] = React.useState(0)
const [shouldCloseModal, setShouldCloseModal] = React.useState(true)
const menu = useDisclosure()
const modal = useDisclosure()
const [menuNodes] = React.useState(() => new MultiRef())
const menuRef = React.useRef(null)
const eventRef = React.useRef<'mouse' | 'keyboard'>(null)
React.useEffect(() => {
router.events.on('routeChangeComplete', modal.onClose)
return () => {
router.events.off('routeChangeComplete', modal.onClose)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// @ts-expect-error @segunadebayo not sure what this should be?!
useEventListener('keydown', (event) => {
const isMac = /(Mac|iPhone|iPod|iPad)/i.test(navigator?.platform)
const hotkey = isMac ? 'metaKey' : 'ctrlKey'
if (event?.key?.toLowerCase() === 'k' && event[hotkey]) {
event.preventDefault()
modal.isOpen ? modal.onClose() : modal.onOpen()
}
})
React.useEffect(() => {
if (modal.isOpen && query.length > 0) {
setQuery('')
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [modal.isOpen])
const results = React.useMemo(
function getResults() {
if (query.length < 2) return []
return matchSorter(searchData, query, {
keys: ['hierarchy.lvl1', 'hierarchy.lvl2', 'hierarchy.lvl3', 'content'],
}).slice(0, 20) // There is probably a filter needed to filter for current locale
},
[query],
)
const onKeyDown = React.useCallback(
(e: React.KeyboardEvent) => {
eventRef.current = 'keyboard'
switch (e.key) {
case 'ArrowDown': {
e.preventDefault()
if (active + 1 < results.length) {
setActive(active + 1)
}
break
}
case 'ArrowUp': {
e.preventDefault()
if (active - 1 >= 0) {
setActive(active - 1)
}
break
}
case 'Control':
case 'Alt':
case 'Shift': {
e.preventDefault()
setShouldCloseModal(true)
break
}
case 'Enter': {
if (results?.length <= 0) {
break
}
modal.onClose()
router.push(results[active].url)
break
}
}
},
[active, modal, results, router],
)
const onKeyUp = React.useCallback((e: React.KeyboardEvent) => {
eventRef.current = 'keyboard'
switch (e.key) {
case 'Control':
case 'Alt':
case 'Shift': {
e.preventDefault()
setShouldCloseModal(false)
}
}
}, [])
useUpdateEffect(() => {
setActive(0)
}, [query])
useUpdateEffect(() => {
if (!menuRef.current || eventRef.current === 'mouse') return
const node = menuNodes.map.get(active)
if (!node) return
scrollIntoView(node, {
scrollMode: 'if-needed',
block: 'nearest',
inline: 'nearest',
boundary: menuRef.current,
})
}, [active])
const open = menu.isOpen && results.length > 0
return (
<>
{
setQuery(e.target.value)
menu.onOpen()
}}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
/>
{open && (
{results.map((item, index) => {
const selected = index === active
const isLvl1 = item.type === 'lvl1'
return (
{
setActive(index)
eventRef.current = 'mouse'
}}
onClick={() => {
if (shouldCloseModal) {
modal.onClose()
}
}}
ref={menuNodes.ref(index)}
role='option'
key={item.url}
sx={{
display: 'flex',
alignItems: 'center',
minH: 16,
mt: 2,
px: 4,
py: 2,
rounded: 'lg',
bg: 'gray.100',
'.chakra-ui-dark &': { bg: 'gray.600' },
_selected: {
bg: 'teal.500',
color: 'white',
mark: {
color: 'white',
textDecoration: 'underline',
},
},
}}
>
{isLvl1 ? (
) : (
)}
{!isLvl1 && (
{item.hierarchy.lvl1}
)}
)
})}
)}
>
)
}
export default OmniSearch