"use client"; import { useState, useEffect, useRef, useMemo, useCallback } from "react"; import { createPortal } from "react-dom"; import { useRouter } from "next/navigation"; import { Search, X, CornerDownLeft, ArrowUp, ArrowDown, FileText, Cpu, Terminal, BookOpen, AlertTriangle, Box, Sparkles, Plug, } from "lucide-react"; import { preparar, buscar, EXEMPLOS_DE_BUSCA } from "@/lib/searchEngine"; import { normalizar } from "@/lib/searchTopics"; const TIPOS = { resposta: { label: "Resposta", Icon: Sparkles, cor: "bg-accent text-accent-contrast" }, pino: { label: "Pinos", Icon: Plug, cor: "bg-accent text-accent-contrast" }, pagina: { label: "Página", Icon: FileText, cor: "bg-surface-2 text-ink-soft" }, serie: { label: "Série", Icon: Cpu, cor: "bg-accent-soft text-accent" }, framework: { label: "Framework", Icon: Terminal, cor: "bg-teal-soft text-teal" }, glossario: { label: "Glossário", Icon: BookOpen, cor: "bg-copper-soft text-copper" }, erro: { label: "Erro", Icon: AlertTriangle, cor: "bg-gold-soft text-gold" }, componente: { label: "Componente", Icon: Box, cor: "bg-surface-2 text-ink-soft" }, }; const INTELIGENTES = new Set(["resposta", "pino"]); function Destaque({ texto, termos }) { if (!termos.length) return texto; // Posições do texto normalizado só valem para o original se o comprimento não mudou. const norm = normalizar(texto); if (norm.length !== texto.length) return texto; const marcas = new Array(texto.length).fill(false); for (const t of termos) { let i = norm.indexOf(t); while (i !== -1) { for (let k = i; k < i + t.length; k++) marcas[k] = true; i = norm.indexOf(t, i + t.length); } } const partes = []; let atual = ""; let marcado = false; for (let i = 0; i < texto.length; i++) { if (marcas[i] !== marcado && atual) { partes.push({ t: atual, m: marcado }); atual = ""; } marcado = marcas[i]; atual += texto[i]; } if (atual) partes.push({ t: atual, m: marcado }); return partes.map((p, i) => p.m ? ( {p.t} ) : ( {p.t} ) ); } function Pilula({ children, onClick, forte = false }) { const base = "rounded-full px-2.5 py-1 text-xs font-semibold"; if (!onClick) return {children}; return ( ); } export default function SearchDialog() { const [aberto, setAberto] = useState(false); const [montado, setMontado] = useState(false); const [consulta, setConsulta] = useState(""); const [indice, setIndice] = useState(null); const [erro, setErro] = useState(false); const [ativo, setAtivo] = useState(0); const inputRef = useRef(null); const listaRef = useRef(null); const router = useRouter(); useEffect(() => setMontado(true), []); const fechar = useCallback(() => { setAberto(false); setConsulta(""); setAtivo(0); }, []); // Atalhos globais: Ctrl/⌘+K abre; "/" abre fora de campos de texto. useEffect(() => { function onKey(e) { const emCampo = /^(INPUT|TEXTAREA|SELECT)$/.test(e.target?.tagName) || e.target?.isContentEditable; if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { e.preventDefault(); setAberto((v) => !v); } else if (e.key === "/" && !emCampo && !e.ctrlKey && !e.metaKey && !e.altKey) { e.preventDefault(); setAberto(true); } else if (e.key === "Escape") { fechar(); } } document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [fechar]); const carregarIndice = useCallback(() => { setErro(false); let cancelado = false; fetch("/api/busca?v=2", { cache: "no-cache" }) .then((r) => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }) .then((dados) => { if (!cancelado) { setIndice(preparar(dados)); } }) .catch((err) => { console.error("Erro ao carregar índice de busca:", err); if (!cancelado) setErro(true); }); return () => { cancelado = true; }; }, []); // Carrega o índice (textos + especificações + pinos) na primeira abertura. useEffect(() => { if (!aberto || indice) return; return carregarIndice(); }, [aberto, indice, carregarIndice]); useEffect(() => { if (aberto) { document.body.style.overflow = "hidden"; inputRef.current?.focus(); } return () => { document.body.style.overflow = ""; }; }, [aberto]); const termos = useMemo(() => normalizar(consulta).split(/\s+/).filter((t) => t.length > 1), [consulta]); const busca = useMemo(() => { if (!indice) return { resultados: [], interpretacao: { chips: [], topicos: [] }, sugestoes: [], totalRespostas: 0 }; if (!consulta.trim()) { return { resultados: indice.itens.filter((i) => i.tipo === "pagina"), interpretacao: { chips: [], topicos: [] }, sugestoes: [], totalRespostas: 0 }; } return buscar(indice, consulta); }, [indice, consulta]); const { resultados, interpretacao, sugestoes } = busca; const vazia = !consulta.trim(); useEffect(() => setAtivo(0), [consulta]); useEffect(() => { listaRef.current?.querySelector(`[data-idx="${ativo}"]`)?.scrollIntoView({ block: "nearest" }); }, [ativo]); function ir(item) { if (!item) return; fechar(); router.push(item.href); // Se já estamos na página de destino, só a âncora muda: avisa quem escuta (abas da série). if (item.href.includes("#")) setTimeout(() => window.dispatchEvent(new HashChangeEvent("hashchange")), 80); } function anexar(termo) { setConsulta((c) => `${c.trim()} ${termo}`.trim()); inputRef.current?.focus(); } function onInputKey(e) { if (e.key === "ArrowDown") { e.preventDefault(); setAtivo((i) => Math.min(i + 1, resultados.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setAtivo((i) => Math.max(i - 1, 0)); } else if (e.key === "Enter") { e.preventDefault(); ir(resultados[ativo]); } } const entendeu = interpretacao.chips.length > 0 || interpretacao.topicos.length > 0; return ( <> {montado && aberto && createPortal(
{entendeu && (
Entendi: {interpretacao.chips.map((c) => ( {c.nome} ))} {interpretacao.topicos.map((t) => ( {t.nome} ))}
)} {sugestoes.length > 0 && (
{interpretacao.chips.length ? "Perguntar sobre:" : "Para qual chip?"} {sugestoes.map((s) => ( anexar(s.anexar)}> {s.rotulo} ))}
)}
{erro && (

Não foi possível carregar o índice de busca.

)} {!erro && !indice &&

Carregando…

} {indice && vazia && (

Experimente perguntar

{EXEMPLOS_DE_BUSCA.map((ex) => ( setConsulta(ex)}> {ex} ))}

Ir para

)} {indice && !vazia && resultados.length === 0 && (

Nenhum resultado para “{consulta}”

Tente “brownout”, “flash s3”, “pinos usb c6” ou “LVGL”.

)} {resultados.map((item, idx) => { const { Icon, label, cor } = TIPOS[item.tipo] ?? TIPOS.pagina; const sel = idx === ativo; const inteligente = INTELIGENTES.has(item.tipo); return ( ); })}
navegar abrir Esc fechar {vazia ? "Dica: pressione / em qualquer página" : `${resultados.length} resultado(s)`}
, document.body )} ); }