import { useEffect, useRef, useState } from 'react' import { Search, X } from 'lucide-react' import { API_URL, conAuth, manejar401 } from '../services/auth' /** Elemento de resultado de búsqueda global */ interface BuscadoItem { tipo: 'lead' | 'oportunidad' | 'contacto' | 'unidad' id: string nombre: string detalle: string url: string } const ETIQUETA_TIPO: Record = { lead: 'Lead', oportunidad: 'Oportunidad', contacto: 'Contacto', unidad: 'Unidad', } const COLOR_TIPO: Record = { lead: 'bg-gray-100 text-gray-700', oportunidad: 'bg-blue-100 text-blue-700', contacto: 'bg-green-100 text-green-700', unidad: 'bg-purple-100 text-purple-700', } export function CommandPalette() { const [abierto, setAbierto] = useState(false) const [query, setQuery] = useState('') const [resultados, setResultados] = useState([]) const [cargando, setCargando] = useState(false) const [indiceSeleccionado, setIndiceSeleccionado] = useState(0) const inputRef = useRef(null) const debounceRef = useRef | null>(null) // Atajo de teclado Ctrl+K / Cmd+K useEffect(() => { const handler = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault() setAbierto((v) => !v) } if (e.key === 'Escape' && abierto) { setAbierto(false) } } window.addEventListener('keydown', handler) return () => window.removeEventListener('keydown', handler) }, [abierto]) // Focus en input al abrir useEffect(() => { if (abierto) { setTimeout(() => inputRef.current?.focus(), 50) } else { setQuery('') setResultados([]) setIndiceSeleccionado(0) } }, [abierto]) // Búsqueda con debounce useEffect(() => { if (debounceRef.current) clearTimeout(debounceRef.current) if (query.trim().length < 2) { setResultados([]) return } setCargando(true) debounceRef.current = setTimeout(async () => { try { const resp = await fetch(`${API_URL}/buscar?q=${encodeURIComponent(query.trim())}&limite=20`, conAuth()) manejar401(resp) if (resp.ok) { setResultados(await resp.json()) } } catch { setResultados([]) } finally { setCargando(false) } }, 300) return () => { if (debounceRef.current) clearTimeout(debounceRef.current) } }, [query]) // Navegación con teclado const onKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'ArrowDown') { e.preventDefault() setIndiceSeleccionado((i) => Math.min(i + 1, resultados.length - 1)) } else if (e.key === 'ArrowUp') { e.preventDefault() setIndiceSeleccionado((i) => Math.max(i - 1, 0)) } else if (e.key === 'Enter' && resultados[indiceSeleccionado]) { window.location.href = resultados[indiceSeleccionado].url } } // Agrupar por tipo const agrupados = resultados.reduce>((acc, item) => { const key = item.tipo if (!acc[key]) acc[key] = [] acc[key].push(item) return acc }, {}) return ( <> {/* Botón trigger */} {/* Modal */} {abierto && (
{/* Backdrop */}
setAbierto(false)} /> {/* Panel */}
{/* Input */}
{ setQuery(e.target.value); setIndiceSeleccionado(0) }} onKeyDown={onKeyDown} className="flex-1 bg-transparent text-sm text-[#111827] dark:text-[#E2E8F0] placeholder-[#94A3B8] focus:outline-none" />
{/* Resultados */}
{query.trim().length < 2 ? (

Escribe al menos 2 caracteres para buscar

) : cargando ? (

Buscando…

) : resultados.length === 0 ? (

Sin resultados para "{query}"

) : ( Object.entries(agrupados).map(([tipo, items]) => (

{ETIQUETA_TIPO[tipo as BuscadoItem['tipo']]}

{items.map((item) => { const idxGlobal = resultados.indexOf(item) return ( {ETIQUETA_TIPO[item.tipo]} {item.nombre} {item.detalle && ( {item.detalle} )} ) })}
)) )}
{/* Footer */}
↑↓ navegar ↵ abrir esc cerrar
)} ) }