205 lines
6.3 KiB
JavaScript
205 lines
6.3 KiB
JavaScript
import { useState, useRef, useEffect } from 'react'
|
|
import Link from 'next/link'
|
|
import { useRouter } from 'next/router'
|
|
|
|
import algoliasearch from 'algoliasearch/lite';
|
|
import { useHotkeys } from 'react-hotkeys-hook'
|
|
import { InstantSearch, connectHits, connectHighlight, connectSearchBox } from 'react-instantsearch-dom'
|
|
import config from 'site/freesewing.config.js'
|
|
|
|
const searchClient = algoliasearch(config.algolia.app, config.algolia.key)
|
|
|
|
const Hits = props => {
|
|
|
|
// When we hit enter in the text field, we want to navigate to the result
|
|
// which means we must make the result links available in the input somehow
|
|
// so let's stuff them in a data attribute
|
|
const links = props.hits.map(hit => hit.page)
|
|
props.input.current.setAttribute('data-links', JSON.stringify(links))
|
|
|
|
return props.hits.map((hit, index) => (
|
|
<Hit
|
|
key={hit.page}
|
|
{...props}
|
|
hit={hit}
|
|
index={index}
|
|
len={props.hits.length}
|
|
activeLink={links[props.active]}
|
|
/>
|
|
))
|
|
}
|
|
|
|
const CustomHits = connectHits(Hits);
|
|
|
|
const Highlight = ({ highlight, attribute, hit, snippet=false }) => {
|
|
const parsedHit = highlight({
|
|
highlightProperty: snippet ? '_snippetResult' : '_highlightResult',
|
|
attribute,
|
|
hit,
|
|
});
|
|
|
|
return parsedHit.map((part, index) => part.isHighlighted
|
|
? <mark className="text-base-content bg-secondary-focus bg-opacity-30" key={index}>{part.value}</mark>
|
|
: <span key={index}>{part.value}</span>
|
|
)
|
|
}
|
|
|
|
const CustomHighlight = connectHighlight(Highlight);
|
|
|
|
const Hit = props => (
|
|
<div
|
|
className={`
|
|
px-2 py-1 ounded mt-1
|
|
text-base text-base-content
|
|
sm:rounded
|
|
lg:px-4 lg:py-2
|
|
hover:bg-secondary hover:bg-opacity-10 hover:text-base-content
|
|
${props.index === props.active
|
|
? 'bg-secondary bg-opacity-30'
|
|
: 'bg-base-300 bg-opacity-10'
|
|
}
|
|
`}
|
|
>
|
|
<Link href={props.hit.page}>
|
|
<a href={props.hit.page} className="flex flex-row justify-between gap-2">
|
|
<span className="text-base sm:text-xl font-bold leading-5">
|
|
{props.hit?._highlightResult?.title
|
|
? <CustomHighlight hit={props.hit} attribute='title' />
|
|
: props.hit.title
|
|
}
|
|
</span>
|
|
<span className="text-xs pt-0.5 sm:text-base sm:pt-1 font-bold uppercase">{props.hit.page.split('/')[1]}</span>
|
|
</a>
|
|
</Link>
|
|
{props.hit?._snippetResult?.body && (
|
|
<Link href={props.hit.page}>
|
|
<a href={props.hit.page} className="text-sm sm:text-base block py-1">
|
|
<CustomHighlight hit={props.hit} attribute='body' snippet />
|
|
</a>
|
|
</Link>
|
|
)}
|
|
{props.hit?._highlightResult?.page && (
|
|
<Link href={props.hit.page}>
|
|
<a href={props.hit.page} className="text-xs sm:text-sm block opacity-70">
|
|
<CustomHighlight hit={props.hit} attribute='page' />
|
|
</a>
|
|
</Link>
|
|
)}
|
|
</div>
|
|
)
|
|
|
|
// We use this for trapping ctrl-c
|
|
let prev
|
|
const handleInputKeydown = (evt, setSearch, setActive, active, router) => {
|
|
if (evt.key === 'Escape') setSearch(false)
|
|
if (evt.key === 'ArrowDown') setActive(act => act + 1)
|
|
if (evt.key === 'ArrowUp') setActive(act => act - 1)
|
|
if (evt.key === 'Enter') {
|
|
// Trigger navigation
|
|
if (evt?.target?.dataset?.links) {
|
|
router.push(JSON.parse(evt.target.dataset.links)[active])
|
|
setSearch(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
const SearchBox = props => {
|
|
|
|
const input = useRef(null)
|
|
const router = useRouter()
|
|
useHotkeys('ctrl+x', () => {
|
|
input.current.value = ''
|
|
})
|
|
if (input.current && props.active < 0) input.current.focus()
|
|
|
|
const { currentRefinement, isSearchStalled, refine, setSearch, setActive } = props
|
|
|
|
return (
|
|
<div className="py-8">
|
|
<form noValidate action="" role="search" onSubmit={(evt) => evt.preventDefault()}>
|
|
<div className="form-control">
|
|
<div className="relative">
|
|
<input
|
|
ref={input}
|
|
type="search"
|
|
autoFocus={true}
|
|
value={currentRefinement}
|
|
onChange={event => refine(event.currentTarget.value)}
|
|
onKeyDown={(evt) => handleInputKeydown(evt, setSearch, setActive, props.active, router)}
|
|
className="input lg:input-lg input-bordered input-neutral w-full pr-16"
|
|
placeholder='Type to search'
|
|
/>
|
|
<button
|
|
className="absolute right-0 top-0 rounded-l-none btn btn-neutral lg:btn-lg"
|
|
onClick={() => props.setSearch(false)}
|
|
>X</button>
|
|
</div>
|
|
<label className="label hidden sm:block">
|
|
<div className="label-text flex flex-row gap-4 justify-between">
|
|
<div><b> Escape</b> to exit</div>
|
|
<div><b> Up</b> or <b>Down</b> to select</div>
|
|
<div><b> Enter</b> to navigate</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
<div
|
|
className="overscroll-auto overflow-y-auto mt-2"
|
|
style={{maxHeight: 'calc(100vh - 10rem)'}}
|
|
>
|
|
{
|
|
input.current
|
|
&& input.current.value.length > 0
|
|
&& <CustomHits hitComponent={Hit} {...props} input={input}/>
|
|
}
|
|
</div>
|
|
</form>
|
|
<div className={`
|
|
bg-neutral text-neutral-content
|
|
z-20 w-full mx-auto
|
|
lg:bg-base-100 lg:border-base-200
|
|
fixed bottom-0 left-0 border-t-2
|
|
lg:hidden
|
|
`}>
|
|
<div className='px-4 py-0 flex flex-row w-full lg:py-2'>
|
|
<button
|
|
className={`btn btn-ghost btn-block`}
|
|
onClick={() => props.setSearch(false)}
|
|
>
|
|
<span className='px-2 pt-2 pb-2'>Close Search</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const CustomSearchBox = connectSearchBox(SearchBox);
|
|
|
|
const Search = props => {
|
|
|
|
const [active, setActive] = useState(0)
|
|
useHotkeys('esc', () => props.setSearch(false))
|
|
useHotkeys('up', () => {
|
|
if (active) setActive(act => act - 1)
|
|
})
|
|
useHotkeys('down', () => {
|
|
setActive(act => act + 1)
|
|
})
|
|
useHotkeys('down', () => {
|
|
console.log('enter', active)
|
|
})
|
|
|
|
const stateProps = {
|
|
setSearch: props.setSearch,
|
|
setMenu: props.setMenu,
|
|
active, setActive
|
|
}
|
|
|
|
return (
|
|
<InstantSearch indexName={config.algolia.index} searchClient={searchClient}>
|
|
<CustomSearchBox {...stateProps}/>
|
|
</InstantSearch>
|
|
)
|
|
}
|
|
|
|
export default Search
|