import { useState, useEffect, useRef, ReactNode } from 'react' import { useRouter } from 'next/router' import debounce from 'lodash/debounce' import cx from 'classnames' import { useTranslation } from 'components/hooks/useTranslation' import { sendEvent, EventType } from 'components/lib/events' import { useMainContext } from './context/MainContext' import { useVersion } from 'components/hooks/useVersion' import { useLanguages } from './context/LanguagesContext' import styles from './Search.module.scss' type SearchResult = { url: string breadcrumbs: string heading: string title: string content: string } type Props = { isOverlay?: boolean variant?: 'compact' | 'expanded' autoFocus?: boolean updateSearchParams?: boolean children?: (props: { SearchInput: ReactNode; SearchResults: ReactNode }) => ReactNode } export function Search({ autoFocus = false, isOverlay = false, updateSearchParams = true, variant = 'compact', children, }: Props) { const router = useRouter() const [query, setQuery] = useState(router.query.query || '') const [results, setResults] = useState>([]) const [isLoading, setIsLoading] = useState(false) const [activeHit, setActiveHit] = useState(0) const inputRef = useRef(null) const { t } = useTranslation('search') const { currentVersion } = useVersion() const { languages } = useLanguages() // Figure out language and version for index const { searchVersions, nonEnterpriseDefaultVersion } = useMainContext() // fall back to the non-enterprise default version (FPT currently) on the homepage, 404 page, etc. const version = searchVersions[currentVersion] || searchVersions[nonEnterpriseDefaultVersion] const language = (Object.keys(languages).includes(router.locale || '') && router.locale) || 'en' // If the user shows up with a query in the URL, go ahead and search for it useEffect(() => { if (updateSearchParams && router.query.query) { /* await */ fetchSearchResults((router.query.query as string).trim()) } }, []) // Search with your keyboard useEffect(() => { document.addEventListener('keydown', searchWithYourKeyboard) return () => document.removeEventListener('keydown', searchWithYourKeyboard) }, [results, activeHit]) function searchWithYourKeyboard(event: KeyboardEvent) { switch (event.key) { case '/': // when an input is focused, `/` should have no special behavior if (['INPUT', 'TEXTAREA', 'SEARCH'].includes(document?.activeElement?.tagName || '')) break event.preventDefault() // prevent slash from being typed into input inputRef.current?.focus() break case 'Escape': closeSearch() break case 'ArrowDown': if (!results.length) break event.preventDefault() // prevent window scrolling if (activeHit >= results.length) break setActiveHit(activeHit + 1) break case 'ArrowUp': if (!results.length) break event.preventDefault() // prevent window scrolling if (activeHit === 0) break setActiveHit(activeHit - 1) break case 'Enter': // look for a link in the given hit, then visit it if (activeHit === 0 || !results.length) break window.location.href = results[activeHit - 1]?.url break } } // When the user finishes typing, update the results async function onSearch(e: React.ChangeEvent) { const xquery = e.target?.value?.trim() setQuery(xquery) // Update the URL with the search parameters in the query string if (updateSearchParams) { const pushUrl = new URL(location.toString()) pushUrl.searchParams.set('query', xquery) history.pushState({}, '', pushUrl.toString()) } // deactivate any active hit when typing in search box setActiveHit(0) return await fetchSearchResults(xquery) } // If there's a query, call the endpoint // Otherwise, there's no results by default async function fetchSearchResults(xquery: string) { setIsLoading(true) try { if (xquery) { const endpointUrl = new URL(location.origin) endpointUrl.pathname = '/search' const endpointParams: Record = { language, version, query: xquery, } endpointUrl.search = new URLSearchParams(endpointParams).toString() const response = await fetch(endpointUrl.toString(), { method: 'GET', headers: { 'Content-Type': 'application/json' }, }) setResults(response.ok ? await response.json() : []) } else { setResults([]) } } finally { setIsLoading(false) } // Analytics tracking if (xquery) { sendEvent({ type: EventType.search, search_query: xquery, // search_context }) } } // Close panel if overlay is clicked function closeSearch() { setQuery('') setResults([]) } // Prevent the page from refreshing when you "submit" the form function preventRefresh(evt: React.FormEvent) { evt.preventDefault() } const SearchResults = ( <>
{results.length > 0 ? (
    {results.map(({ url, breadcrumbs, heading, title, content }, index) => (
  1. ))}
) : ( isOverlay && (
{isLoading ? {t('loading')}... : {t('no_results')}.}
) )}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
) const SearchInput = ( ) return ( <> {typeof children === 'function' ? ( children({ SearchInput, SearchResults }) ) : ( <> {SearchInput} {SearchResults} )} ) }