"use client"; import { useState, useEffect, useRef, useMemo, useCallback } from "react"; import { createPortal } from "react-dom"; import { useRouter, usePathname } 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_PT = { 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 TIPOS_EN = { resposta: { label: "Answer", Icon: Sparkles, cor: "bg-accent text-accent-contrast" }, pino: { label: "Pins", Icon: Plug, cor: "bg-accent text-accent-contrast" }, pagina: { label: "Page", Icon: FileText, cor: "bg-surface-2 text-ink-soft" }, serie: { label: "Series", Icon: Cpu, cor: "bg-accent-soft text-accent" }, framework: { label: "Framework", Icon: Terminal, cor: "bg-teal-soft text-teal" }, glossario: { label: "Glossary", Icon: BookOpen, cor: "bg-copper-soft text-copper" }, erro: { label: "Error", Icon: AlertTriangle, cor: "bg-gold-soft text-gold" }, componente: { label: "Component", Icon: Box, cor: "bg-surface-2 text-ink-soft" }, }; const INTELIGENTES = new Set(["resposta", "pino"]); const EXEMPLOS_EN = ["flash s3", "usb pins c6", "gpio0 esp32", "brownout", "LVGL", "FreeRTOS"]; function Destaque({ texto, termos }) { if (!termos.length) return texto; 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 pathname = usePathname() || ""; const locale = pathname === "/en" || pathname.startsWith("/en/") ? "en" : "pt"; const isEn = locale === "en"; const TIPOS = isEn ? TIPOS_EN : TIPOS_PT; const EXEMPLOS = isEn ? EXEMPLOS_EN : EXEMPLOS_DE_BUSCA; 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; const url = isEn ? "/api/busca/en?v=2" : "/api/busca?v=2"; fetch(url, { cache: "no-cache" }) .then((r) => { if (!r.ok) { // Fallback para /api/busca caso a rota dinâmica do locale falhe return fetch("/api/busca?v=2", { cache: "no-cache" }).then((r2) => { if (!r2.ok) throw new Error(`HTTP ${r2.status}`); return r2.json(); }); } 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; }; }, [isEn]); // 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); 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(
{isEn ? "Could not load search index." : "Não foi possível carregar o índice de busca."}
{isEn ? "Loading…" : "Carregando…"}
} {indice && vazia && ({isEn ? "Try asking" : "Experimente perguntar"}
{isEn ? "Go to" : "Ir para"}
{isEn ? `No results for “${consulta}”` : `Nenhum resultado para “${consulta}”`}
{isEn ? "Try “brownout”, “flash s3”, “usb pins c6” or “LVGL”." : "Tente “brownout”, “flash s3”, “pinos usb c6” ou “LVGL”."}