first commit
This commit is contained in:
commit
6e354ba041
109 changed files with 12110 additions and 0 deletions
120
frontend/src/components/search/SearchOverlay.tsx
Normal file
120
frontend/src/components/search/SearchOverlay.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
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>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue