'use client' import { useState, useEffect, useRef, useCallback } from 'react' import { useRouter } from 'next/navigation' import { Search, X } from 'lucide-react' import type { Article } from '@/lib/types' export default function SearchOverlay() { const [open, setOpen] = useState(false) const [query, setQuery] = useState('') const [results, setResults] = useState([]) const [activeIndex, setActiveIndex] = useState(0) const [loading, setLoading] = useState(false) const inputRef = useRef(null) const debounceRef = useRef | null>(null) const router = useRouter() const close = useCallback(() => { setOpen(false) setQuery('') setResults([]) setActiveIndex(0) }, []) useEffect(() => { function onKeyDown(e: KeyboardEvent) { if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { e.preventDefault() setOpen(true) } if (e.key === 'Escape') close() } function onOpenSearch() { setOpen(true) } window.addEventListener('keydown', onKeyDown) window.addEventListener('kotobane:open-search', onOpenSearch) return () => { window.removeEventListener('keydown', onKeyDown) window.removeEventListener('kotobane:open-search', onOpenSearch) } }, [close]) useEffect(() => { if (open) setTimeout(() => inputRef.current?.focus(), 50) }, [open]) useEffect(() => { if (!query.trim()) { setResults([]) return } if (debounceRef.current) clearTimeout(debounceRef.current) debounceRef.current = setTimeout(async () => { setLoading(true) try { const params = new URLSearchParams({ 'filter[status][_eq]': 'published', 'filter[_or][0][title][_icontains]': query.trim(), 'filter[_or][1][excerpt][_icontains]': query.trim(), limit: '8', fields: 'id,title,slug,category.slug,category.name', access_token: process.env.NEXT_PUBLIC_DIRECTUS_TOKEN ?? '', }) const res = await fetch( `${process.env.NEXT_PUBLIC_DIRECTUS_URL}/items/articles?${params}` ) const data = await res.json() setResults(data.data ?? []) setActiveIndex(0) } finally { setLoading(false) } }, 300) return () => { if (debounceRef.current) clearTimeout(debounceRef.current) } }, [query]) function navigate(article: Article) { router.push(`/${article.category.slug}/${article.slug}`) close() } function onKeyDown(e: React.KeyboardEvent) { if (e.key === 'ArrowDown') { e.preventDefault() setActiveIndex((i) => Math.min(i + 1, results.length - 1)) } else if (e.key === 'ArrowUp') { e.preventDefault() setActiveIndex((i) => Math.max(i - 1, 0)) } else if (e.key === 'Enter' && results[activeIndex]) { navigate(results[activeIndex]) } } if (!open) return null return (
e.stopPropagation()} >
setQuery(e.target.value)} onKeyDown={onKeyDown} className="flex-1 bg-transparent text-sm text-text-primary placeholder:text-text-disabled outline-none" /> {query && ( )}
{results.length > 0 && (
    {results.map((article, i) => (
  • ))}
)} {loading && (

Searching…

)} {!loading && query.trim() && results.length === 0 && (

No results for "{query}"

)}
↑↓ navigate ↵ open esc close
) }