Silos/frontend/src/components/search/SearchOverlay.tsx
2026-10-08 02:44:15 +05:30

120 lines
4.4 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { FileText, Zap, PenTool, LayoutGrid, Search, Loader2 } from 'lucide-react'
import { searchDocuments, type DocumentRecord } from '../../lib/documents'
import { formatRelativeTime, getActivityDate, typeColors, typeLabels } from '../../lib/documentUtils'
import type { CreateType } from '../../data/createData'
const typeIcons: Record<CreateType, typeof FileText> = {
silo: FileText,
shed: Zap,
sketch: PenTool,
stead: LayoutGrid,
}
interface SearchOverlayProps {
open: boolean
onClose: () => void
initialQuery?: string
}
export default function SearchOverlay({ open, onClose, initialQuery = '' }: SearchOverlayProps) {
const [query, setQuery] = useState(initialQuery)
const [results, setResults] = useState<DocumentRecord[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const inputRef = useRef<HTMLInputElement>(null)
const navigate = useNavigate()
useEffect(() => {
if (open) {
setQuery(initialQuery)
setTimeout(() => inputRef.current?.focus(), 0)
}
}, [open, initialQuery])
const runSearch = useCallback(async (value: string) => {
if (!value.trim()) {
setResults([])
setError(null)
return
}
setLoading(true)
setError(null)
try {
const docs = await searchDocuments(value)
setResults(docs)
} catch {
setError('Could not search. Is MongoDB running?')
setResults([])
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
if (!open) return
const timer = setTimeout(() => void runSearch(query), 250)
return () => clearTimeout(timer)
}, [query, open, runSearch])
if (!open) return null
return (
<div className="fixed inset-0 z-[80] flex items-start justify-center bg-black/30 pt-24 px-4">
<button type="button" className="absolute inset-0 cursor-default" onClick={onClose} aria-label="Close search" />
<div className="relative z-10 w-full max-w-xl overflow-hidden rounded-2xl border border-silos-cream-dark bg-white shadow-2xl">
<div className="flex items-center gap-2 border-b border-silos-cream-dark px-4">
<Search className="h-4 w-4 text-silos-muted" />
<input
ref={inputRef}
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search pages, spaces, whiteboards..."
className="w-full bg-transparent py-3 text-sm text-silos-text outline-none"
/>
{loading && <Loader2 className="h-4 w-4 animate-spin text-silos-muted" />}
<kbd className="rounded border border-silos-cream-dark px-1.5 py-0.5 text-[10px] text-silos-muted">Esc</kbd>
</div>
<div className="max-h-80 overflow-y-auto">
{error && <p className="px-4 py-6 text-center text-sm text-amber-700">{error}</p>}
{!error && query.trim() && !loading && results.length === 0 && (
<p className="px-4 py-6 text-center text-sm text-silos-muted">No matches for “{query}”.</p>
)}
{!query.trim() && (
<p className="px-4 py-6 text-center text-sm text-silos-muted">
Type to search your documents.
</p>
)}
{results.map((doc) => {
const Icon = typeIcons[doc.type]
return (
<button
key={doc.id}
type="button"
onClick={() => {
onClose()
navigate(`/workspace/${doc.type}/${doc.id}`)
}}
className="flex w-full items-center gap-3 px-4 py-3 text-left hover:bg-silos-cream/60"
>
<div
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg"
style={{ backgroundColor: `${typeColors[doc.type]}22` }}
>
<Icon className="h-4 w-4" style={{ color: typeColors[doc.type] }} />
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-silos-text">{doc.title}</p>
<p className="text-xs text-silos-muted">
{typeLabels[doc.type]} · {formatRelativeTime(getActivityDate(doc))}
</p>
</div>
</button>
)
})}
</div>
</div>
</div>
)
}