diff --git a/components/Header.tsx b/components/Header.tsx
index 293aaea922..827630258a 100644
--- a/components/Header.tsx
+++ b/components/Header.tsx
@@ -11,6 +11,7 @@ import { HeaderNotifications } from 'components/HeaderNotifications'
import { MobileProductDropdown } from 'components/MobileProductDropdown'
import { useTranslation } from 'components/hooks/useTranslation'
import { HomepageVersionPicker } from 'components/landing/HomepageVersionPicker'
+import { Search } from 'components/Search'
export const Header = () => {
const router = useRouter()
@@ -98,17 +99,7 @@ export const Header = () => {
{/* */}
- {relativePath !== 'index.md' && error !== '404'}
-
-
-
- `,
- }}
- />
+ {relativePath !== 'index.md' && error !== '404' && }
diff --git a/components/Search.tsx b/components/Search.tsx
new file mode 100644
index 0000000000..2c8c2b4d38
--- /dev/null
+++ b/components/Search.tsx
@@ -0,0 +1,204 @@
+import { useState, useEffect, useRef } from 'react'
+import { useRouter } from 'next/router'
+import debounce from 'lodash/debounce'
+import { useTranslation } from 'components/hooks/useTranslation'
+import { sendEvent } from '../javascripts/events'
+import { useMainContext } from './context/MainContext'
+import { useVersion } from 'components/hooks/useVersion'
+
+type SearchResult = {
+ url: string
+ breadcrumbs: string
+ heading: string
+ title: string
+ content: string
+}
+
+// Homepage and 404 should be `isStandalone`, all others not
+// `updateSearchParams` should be false on the GraphQL explorer page
+export function Search({ isStandalone = false, updateSearchParams = true }) {
+ const [query, setQuery] = useState('')
+ const [results, setResults] = useState>([])
+ const [activeHit, setActiveHit] = useState(0)
+ const inputRef = useRef(null)
+ const { t } = useTranslation('search')
+ const { currentVersion } = useVersion()
+
+ // Figure out language and version for index
+ const { expose } = useMainContext()
+ const { searchOptions: { languages, versions, nonEnterpriseDefaultVersion } } = JSON.parse(expose)
+ const router = useRouter()
+ // fall back to the non-enterprise default version (FPT currently) on the homepage, 404 page, etc.
+ const version = versions[currentVersion] || versions[nonEnterpriseDefaultVersion]
+ const language = 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(() => {
+ const params = new URLSearchParams(location.search)
+ if (params.has('query')) {
+ const xquery = params.get('query')?.trim() || ''
+ setQuery(xquery)
+ /* await */ fetchSearchResults(xquery)
+ }
+ return () => setQuery('')
+ }, [])
+
+ // 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 the input is focused, `/` should have no special behavior
+ if ((event.target as HTMLInputElement)?.type === 'search') 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) {
+ 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([])
+ }
+
+ // Analytics tracking
+ if (xquery) {
+ sendEvent({
+ type: '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()
+ }
+
+ return (
+
+
+
+ {Boolean(results.length) && (
+
+
+ {results.map(({ url, breadcrumbs, heading, title, content }, index) => (
+ -
+
+
+ ))}
+
+
+ )}
+
+
+
+ )
+}
diff --git a/javascripts/events.js b/javascripts/events.js
index 6aa5697fc5..b9b68dc1a7 100644
--- a/javascripts/events.js
+++ b/javascripts/events.js
@@ -30,25 +30,26 @@ export function getUserEventsId () {
export function sendEvent ({
type,
version = '1.0.0',
- exit_render_duration,
- exit_first_paint,
- exit_dom_interactive,
- exit_dom_complete,
- exit_visit_duration,
- exit_scroll_length,
- link_url,
- search_query,
- search_context,
- navigate_label,
- survey_vote,
- survey_comment,
- survey_email,
- experiment_name,
- experiment_variation,
- experiment_success,
- clipboard_operation,
- preference_name,
- preference_value
+ // `= undefined` is a TypeScript hint.
+ exit_render_duration = undefined,
+ exit_first_paint = undefined,
+ exit_dom_interactive = undefined,
+ exit_dom_complete = undefined,
+ exit_visit_duration = undefined,
+ exit_scroll_length = undefined,
+ link_url = undefined,
+ search_query = undefined,
+ search_context = undefined,
+ navigate_label = undefined,
+ survey_vote = undefined,
+ survey_comment = undefined,
+ survey_email = undefined,
+ experiment_name = undefined,
+ experiment_variation = undefined,
+ experiment_success = undefined,
+ clipboard_operation = undefined,
+ preference_name = undefined,
+ preference_value = undefined
}) {
const body = {
_csrf: getCsrf(),
diff --git a/javascripts/search.js b/javascripts/search.js
index c884173d80..61b8f5ff4d 100644
--- a/javascripts/search.js
+++ b/javascripts/search.js
@@ -15,6 +15,8 @@ let version
let language
export default function search () {
+ if (window.next) return
+
// We don't want to mess with query params intended for the GraphQL Explorer
isExplorerPage = Boolean(document.getElementById('graphiql'))