DocsSuggestions.tsx88 lines · main
| 1 | import { useDocsSearch, type DocsSearchResult } from 'common' |
| 2 | import { Book, Github, Loader2 } from 'lucide-react' |
| 3 | import { cn } from 'ui' |
| 4 | |
| 5 | import { useChangedSync } from '@/hooks/misc/useChanged' |
| 6 | import { DOCS_URL } from '@/lib/constants' |
| 7 | |
| 8 | function useDocsSuggestions(subject: string) { |
| 9 | const { handleDocsSearchDebounced, resetSearch, searchState } = useDocsSearch() |
| 10 | |
| 11 | const trimmedSubject = subject.trim() |
| 12 | const subjectChanged = useChangedSync(trimmedSubject) |
| 13 | |
| 14 | if (subjectChanged && trimmedSubject) { |
| 15 | handleDocsSearchDebounced(trimmedSubject) |
| 16 | } else if (subjectChanged && !trimmedSubject) { |
| 17 | resetSearch() |
| 18 | } |
| 19 | |
| 20 | return searchState |
| 21 | } |
| 22 | |
| 23 | interface DocsSuggestionsProps { |
| 24 | searchString: string |
| 25 | } |
| 26 | |
| 27 | export function DocsSuggestions({ searchString }: DocsSuggestionsProps) { |
| 28 | const searchState = useDocsSuggestions(searchString) |
| 29 | const results = |
| 30 | 'results' in searchState |
| 31 | ? searchState.results |
| 32 | : 'staleResults' in searchState |
| 33 | ? searchState.staleResults |
| 34 | : [] |
| 35 | const resultsStale = searchState.status === 'loading' |
| 36 | |
| 37 | return ( |
| 38 | <> |
| 39 | {searchState.status === 'loading' && <DocsSuggestions_Loading />} |
| 40 | {results.length > 0 && <DocsSuggestions_Results results={results} isStale={resultsStale} />} |
| 41 | </> |
| 42 | ) |
| 43 | } |
| 44 | |
| 45 | function DocsSuggestions_Loading() { |
| 46 | return ( |
| 47 | <div className="flex items-center gap-2 text-sm text-foreground-light"> |
| 48 | <Loader2 className="animate-spin" size={14} /> |
| 49 | <span>Searching for relevant resources...</span> |
| 50 | </div> |
| 51 | ) |
| 52 | } |
| 53 | |
| 54 | interface DocsSuggestions_ResultsProps { |
| 55 | results: DocsSearchResult[] |
| 56 | isStale: boolean |
| 57 | } |
| 58 | |
| 59 | function DocsSuggestions_Results({ results, isStale }: DocsSuggestions_ResultsProps) { |
| 60 | return ( |
| 61 | <ul |
| 62 | className={cn( |
| 63 | 'flex flex-col gap-y-0.5 transition-opacity duration-200', |
| 64 | isStale ? 'opacity-50' : 'opacity-100' |
| 65 | )} |
| 66 | > |
| 67 | {results.slice(0, 5).map((page) => { |
| 68 | return ( |
| 69 | <li key={page.id} className="flex items-center gap-x-1"> |
| 70 | {page.type === 'github-discussions' ? ( |
| 71 | <Github size={16} className="text-foreground-muted" /> |
| 72 | ) : ( |
| 73 | <Book size={16} className="text-foreground-muted" /> |
| 74 | )} |
| 75 | <a |
| 76 | href={page.type === 'github-discussions' ? page.path : `${DOCS_URL}${page.path}`} |
| 77 | target="_blank" |
| 78 | rel="noreferrer" |
| 79 | className="text-sm text-foreground-light hover:text-foreground transition" |
| 80 | > |
| 81 | {page.title} |
| 82 | </a> |
| 83 | </li> |
| 84 | ) |
| 85 | })} |
| 86 | </ul> |
| 87 | ) |
| 88 | } |