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