@@ -4,30 +4,86 @@ import {
44 MagnifyingGlassIcon
55} from '@heroicons/react/24/outline' ;
66import { Command , IconButton , Text } from '@raystack/apsara' ;
7- import type { SortedResult } from 'fumadocs-core/search' ;
8- import { useDocsSearch } from 'fumadocs-core/search/client' ;
9- import { useCallback , useEffect , useState } from 'react' ;
7+ import debounce from 'lodash/debounce' ;
8+ import { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
109import { useNavigate } from 'react-router' ;
1110import { MethodBadge } from '@/components/api/method-badge' ;
1211import { usePageContext } from '@/lib/page-context' ;
1312import styles from './search.module.css' ;
1413
14+ interface SearchResult {
15+ id : string ;
16+ url : string ;
17+ type : string ;
18+ content : string ;
19+ }
20+
1521interface SearchProps {
1622 classNames ?: { trigger ?: string } ;
1723}
1824
25+ function buildSearchUrl ( query : string , tag ?: string ) : string {
26+ const params = new URLSearchParams ( ) ;
27+ if ( query ) params . set ( 'query' , query ) ;
28+ if ( tag ) params . set ( 'tag' , tag ) ;
29+ const qs = params . toString ( ) ;
30+ return qs ? `/api/search?${ qs } ` : '/api/search' ;
31+ }
32+
1933export function Search ( { classNames } : SearchProps ) {
2034 const [ open , setOpen ] = useState ( false ) ;
35+ const [ search , setSearch ] = useState ( '' ) ;
36+ const [ results , setResults ] = useState < SearchResult [ ] > ( [ ] ) ;
37+ const [ suggestions , setSuggestions ] = useState < SearchResult [ ] > ( [ ] ) ;
38+ const [ isLoading , setIsLoading ] = useState ( false ) ;
2139 const navigate = useNavigate ( ) ;
2240 const { version } = usePageContext ( ) ;
41+ const tag = version . dir ?? undefined ;
42+ const abortRef = useRef < AbortController | null > ( null ) ;
2343
24- const { search, setSearch, query } = useDocsSearch ( {
25- type : 'fetch' ,
26- api : '/api/search' ,
27- tag : version . dir ?? undefined ,
28- delayMs : 100 ,
29- allowEmpty : true
30- } ) ;
44+ const fetchResults = useCallback ( async ( query : string , signal ?: AbortSignal ) => {
45+ setIsLoading ( true ) ;
46+ try {
47+ const res = await fetch ( buildSearchUrl ( query , tag ) , { signal } ) ;
48+ if ( ! res . ok || signal ?. aborted ) return ;
49+ const data : SearchResult [ ] = await res . json ( ) ;
50+ if ( signal ?. aborted ) return ;
51+ if ( query ) {
52+ setResults ( data ) ;
53+ } else {
54+ setSuggestions ( data ) ;
55+ }
56+ } catch ( err ) {
57+ if ( err instanceof DOMException && err . name === 'AbortError' ) return ;
58+ console . error ( 'Search fetch failed:' , err ) ;
59+ } finally {
60+ setIsLoading ( false ) ;
61+ }
62+ } , [ tag ] ) ;
63+
64+ const debouncedSearch = useMemo (
65+ ( ) => debounce ( ( query : string ) => {
66+ abortRef . current ?. abort ( ) ;
67+ const controller = new AbortController ( ) ;
68+ abortRef . current = controller ;
69+ fetchResults ( query , controller . signal ) ;
70+ } , 150 ) ,
71+ [ fetchResults ]
72+ ) ;
73+
74+ useEffect ( ( ) => {
75+ if ( ! open ) {
76+ setSearch ( '' ) ;
77+ setResults ( [ ] ) ;
78+ return ;
79+ }
80+ if ( ! search ) {
81+ fetchResults ( '' ) ;
82+ return ;
83+ }
84+ debouncedSearch ( search ) ;
85+ return ( ) => debouncedSearch . cancel ( ) ;
86+ } , [ open , search , fetchResults , debouncedSearch ] ) ;
3187
3288 const onSelect = useCallback (
3389 ( url : string ) => {
@@ -49,9 +105,7 @@ export function Search({ classNames }: SearchProps) {
49105 return ( ) => document . removeEventListener ( 'keydown' , down ) ;
50106 } , [ ] ) ;
51107
52- const results = deduplicateByUrl (
53- query . data === 'empty' ? [ ] : ( query . data ?? [ ] )
54- ) ;
108+ const displayResults = deduplicateByUrl ( search ? results : suggestions ) ;
55109
56110 return (
57111 < >
@@ -77,18 +131,17 @@ export function Search({ classNames }: SearchProps) {
77131 />
78132
79133 < Command . Content className = { styles . list } >
80- { query . isLoading && < Command . Empty > Loading...</ Command . Empty > }
81- { ! query . isLoading &&
134+ { isLoading && displayResults . length === 0 && < Command . Empty > Loading...</ Command . Empty > }
135+ { ! isLoading &&
82136 search . length > 0 &&
83- results . length === 0 && (
137+ displayResults . length === 0 && (
84138 < Command . Empty > No results found.</ Command . Empty >
85139 ) }
86- { ! query . isLoading &&
87- search . length === 0 &&
88- results . length > 0 && (
140+ { search . length === 0 &&
141+ displayResults . length > 0 && (
89142 < Command . Group >
90143 < Command . Label > Suggestions</ Command . Label >
91- { results . slice ( 0 , 8 ) . map ( ( result : SortedResult ) => (
144+ { displayResults . slice ( 0 , 8 ) . map ( ( result ) => (
92145 < Command . Item
93146 key = { result . id }
94147 value = { result . id }
@@ -108,7 +161,7 @@ export function Search({ classNames }: SearchProps) {
108161 </ Command . Group >
109162 ) }
110163 { search . length > 0 &&
111- results . map ( ( result : SortedResult ) => (
164+ displayResults . map ( ( result ) => (
112165 < Command . Item
113166 key = { result . id }
114167 value = { result . id }
@@ -149,7 +202,7 @@ export function Search({ classNames }: SearchProps) {
149202 ) ;
150203}
151204
152- function deduplicateByUrl ( results : SortedResult [ ] ) : SortedResult [ ] {
205+ function deduplicateByUrl ( results : SearchResult [ ] ) : SearchResult [ ] {
153206 const seen = new Set < string > ( ) ;
154207 return results . filter ( r => {
155208 const base = r . url . split ( '#' ) [ 0 ] ;
@@ -183,7 +236,7 @@ function HighlightedText({
183236 ) ;
184237}
185238
186- function getResultIcon ( result : SortedResult ) : React . ReactNode {
239+ function getResultIcon ( result : SearchResult ) : React . ReactNode {
187240 if ( ! result . url . startsWith ( '/apis/' ) ) {
188241 return result . type === 'page' ? (
189242 < DocumentIcon className = { styles . icon } />
0 commit comments