mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
141 lines
4.4 KiB
TypeScript
141 lines
4.4 KiB
TypeScript
'use client'
|
|
|
|
import { compact, debounce } from 'lodash'
|
|
import { useCallback, useMemo, useReducer, useRef } from 'react'
|
|
|
|
// This app's own base path, set only for apps deployed under a path prefix (docs' is '/docs').
|
|
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? ''
|
|
// Public URL of the docs deployment, which hosts the search API routes.
|
|
const DOCS_URL = process.env.NEXT_PUBLIC_DOCS_URL || 'https://supabase.com/docs'
|
|
// From inside the docs app itself, call our own routes relatively; from studio/www, call docs directly.
|
|
const SEARCH_API_BASE = BASE_PATH === '/docs' ? BASE_PATH : DOCS_URL
|
|
|
|
interface DocsSearchV2Result {
|
|
path: string
|
|
title: string
|
|
heading: string
|
|
excerpt: string
|
|
headingPath: string[]
|
|
score: number
|
|
}
|
|
|
|
type SearchState =
|
|
| { status: 'initial'; key: number }
|
|
| { status: 'loading'; key: number; staleResults: DocsSearchV2Result[] }
|
|
| { status: 'results'; key: number; results: DocsSearchV2Result[]; query: string }
|
|
| { status: 'noResults'; key: number; query: string }
|
|
| { status: 'error'; key: number; message: string }
|
|
|
|
type Action =
|
|
| { type: 'resultsReturned'; key: number; results: DocsSearchV2Result[]; query: string }
|
|
| { type: 'newSearchDispatched'; key: number }
|
|
| { type: 'reset'; key: number }
|
|
| { type: 'errored'; key: number; message: string }
|
|
|
|
function reshapeResult(row: unknown): DocsSearchV2Result | null {
|
|
if (typeof row !== 'object' || row === null) return null
|
|
if (
|
|
!(
|
|
'slug' in row &&
|
|
'page_title' in row &&
|
|
'heading' in row &&
|
|
'excerpt' in row &&
|
|
'heading_path' in row &&
|
|
'score' in row
|
|
)
|
|
)
|
|
return null
|
|
|
|
const slug = row.slug as string
|
|
return {
|
|
// Next's router already prepends the app's basePath ('/docs'), so don't add it here.
|
|
path: slug ? `/${slug}` : '/',
|
|
title: row.page_title as string,
|
|
heading: row.heading as string,
|
|
excerpt: row.excerpt as string,
|
|
headingPath: row.heading_path as string[],
|
|
score: row.score as number,
|
|
}
|
|
}
|
|
|
|
function reducer(state: SearchState, action: Action): SearchState {
|
|
// Ignore responses from outdated async functions
|
|
if (state.key > action.key) {
|
|
return state
|
|
}
|
|
switch (action.type) {
|
|
case 'resultsReturned':
|
|
return action.results.length
|
|
? { status: 'results', key: action.key, results: action.results, query: action.query }
|
|
: { status: 'noResults', key: action.key, query: action.query }
|
|
case 'newSearchDispatched':
|
|
return {
|
|
status: 'loading',
|
|
key: action.key,
|
|
staleResults:
|
|
'results' in state ? state.results : 'staleResults' in state ? state.staleResults : [],
|
|
}
|
|
case 'reset':
|
|
return { status: 'initial', key: action.key }
|
|
case 'errored':
|
|
return { status: 'error', key: action.key, message: action.message }
|
|
default:
|
|
return state
|
|
}
|
|
}
|
|
|
|
const useDocsSearchV2 = () => {
|
|
const [state, dispatch] = useReducer(reducer, { status: 'initial', key: 0 })
|
|
const key = useRef(0)
|
|
|
|
const handleSearch = useCallback(async (query: string) => {
|
|
key.current += 1
|
|
const localKey = key.current
|
|
dispatch({ type: 'newSearchDispatched', key: localKey })
|
|
|
|
try {
|
|
const params = new URLSearchParams({ q: query.trim() })
|
|
const response = await fetch(`${SEARCH_API_BASE}/api/search_v2?${params}`)
|
|
const data = await response.json()
|
|
if (!Array.isArray(data)) {
|
|
throw new Error(data?.error ?? "didn't get expected results array")
|
|
}
|
|
dispatch({
|
|
type: 'resultsReturned',
|
|
key: localKey,
|
|
results: compact(data.map(reshapeResult)),
|
|
query: query.trim(),
|
|
})
|
|
} catch (error) {
|
|
console.error(`[ERROR] Error fetching docs search v2 results: ${error}`)
|
|
dispatch({
|
|
type: 'errored',
|
|
key: localKey,
|
|
message: error instanceof Error ? error.message : '',
|
|
})
|
|
}
|
|
}, [])
|
|
|
|
const debouncedSearch = useMemo(() => debounce(handleSearch, 250), [handleSearch])
|
|
|
|
const debounceCancel = useCallback(() => {
|
|
debouncedSearch.cancel()
|
|
}, [debouncedSearch])
|
|
|
|
const resetSearch = useCallback(() => {
|
|
debounceCancel()
|
|
key.current += 1
|
|
dispatch({ type: 'reset', key: key.current })
|
|
}, [debounceCancel])
|
|
|
|
return {
|
|
searchState: state,
|
|
handleDocsSearch: handleSearch,
|
|
handleDocsSearchDebounced: debouncedSearch,
|
|
resetSearch,
|
|
}
|
|
}
|
|
|
|
export { useDocsSearchV2 }
|
|
export type { DocsSearchV2Result }
|