tldraw/apps/docs/components/Search.tsx

136 lines
3.4 KiB
TypeScript
Raw Normal View History

2023-04-25 11:01:25 +00:00
import { SearchResult } from '@/types/search-types'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Icon } from './Icon'
export function Search({ activeId }: { activeId: string | null }) {
const [query, setQuery] = useState('')
const [results, setResults] = useState<SearchResult[]>([])
const rResultsList = useRef<HTMLOListElement>(null)
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value)
}, [])
// eslint-disable-next-line react-hooks/exhaustive-deps
const sendQuery = useCallback(
throttle(async (query: string) => {
const res = await fetch(`/api/search?q=${query}&s=${activeId}`)
const json = await res.json()
setResults(json.results)
}, 150),
[activeId]
)
useEffect(() => {
const query = rInput.current!.value
if (query.length > 2) {
sendQuery(query)
} else {
setResults([])
}
}, [sendQuery])
const hasQuery = query.length > 0
const hasResults = query.length > 0
useEffect(() => {
function handleKeyUp(e: KeyboardEvent) {
if (e.key === 'Escape' && hasResults) {
setResults([])
}
}
function handleMouseUp(e: MouseEvent) {
if (rResultsList.current && !rResultsList.current.contains(e.target as Node)) {
setResults([])
}
}
document.body.addEventListener('mouseup', handleMouseUp)
document.body.addEventListener('keyup', handleKeyUp)
return () => {
document.body.removeEventListener('mouseup', handleMouseUp)
document.body.removeEventListener('keyup', handleKeyUp)
}
}, [hasResults])
const rInput = useRef<HTMLInputElement>(null)
const router = useRouter()
useEffect(() => {
setQuery('')
setResults([])
}, [router.asPath])
const handleFocus = useCallback(() => {
if (hasQuery && !hasResults) {
sendQuery(rInput.current!.value)
}
}, [sendQuery, hasQuery, hasResults])
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
router.push(`/search-results?q=${rInput.current!.value}`)
}
},
[router]
)
return (
<div className="search__wrapper">
<div className="search">
<Icon className="search__icon" icon="search" />
<input
ref={rInput}
type="text"
className="search__input"
placeholder="Search..."
value={query}
onChange={handleChange}
onFocus={handleFocus}
onKeyDown={handleKeyDown}
autoCapitalize="off"
autoComplete="off"
autoCorrect="off"
/>
</div>
{results.length > 0 && (
<div className="search__results__wrapper">
<div className="search__results">
<ol ref={rResultsList} className="search__results__list">
{results.map((result) => (
<Link key={result.id} href={result.url}>
<li className="sidebar__article search__results__article">
<h4>{result.subtitle}</h4>
<h3>{result.title}</h3>
</li>
</Link>
))}
</ol>
</div>
</div>
)}
</div>
)
}
function throttle<T extends (...args: any) => any>(
func: T,
limit: number
): (...args: Parameters<T>) => ReturnType<T> {
let inThrottle: boolean
let lastResult: ReturnType<T>
return function (this: any, ...args: any[]): ReturnType<T> {
if (!inThrottle) {
inThrottle = true
setTimeout(() => (inThrottle = false), limit)
lastResult = func(...args)
}
return lastResult
}
}