feat: site search with a dialog in the header and a /api/busca endpoint
CI / Lint, design e build (push) Successful in 1m52s
CI / Trivy (dependências e segredos) (push) Successful in 12s
CI / Imagem Docker (push) Successful in 2m26s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-11-01 08:02:57 -03:00
1 parent d8323c653b
commit 7d0edf13c4
7 files changed
+1115 -5

No files matched your search

+39 -5
View File
@@ -1,7 +1,8 @@
"use client";
import { useState, useEffect, useMemo, useRef } from "react";
import { useState, useEffect, useMemo, useRef, Suspense } from "react";
import { createPortal } from "react-dom";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { AlertTriangle, CheckCircle, List, Grid, Search, Sliders, X, Zap } from "lucide-react";
/*
@@ -41,7 +42,25 @@ const type_colors = {
nc: "#94a3b8", // muted gray
};
export default function ConnectionsDiagram({ connections, serie }) {
/**
* Aceita ?pinos=28,30,31&motivo=... (gerado pela busca do site): mostra só
* esses pinos, pelo número físico, com um aviso para voltar à lista completa.
* O Suspense é exigido por useSearchParams em páginas estáticas.
*/
export default function ConnectionsDiagram(props) {
return (
<Suspense fallback={null}>
<ConnectionsDiagramInner {...props} />
</Suspense>
);
}
function ConnectionsDiagramInner({ connections, serie }) {
const params = useSearchParams();
const pathname = usePathname();
const router = useRouter();
const pinosFiltro = useMemo(() => new Set((params.get("pinos") ?? "").split(",").filter(Boolean)), [params]);
const motivoFiltro = params.get("motivo") ?? "";
const [activeFilter, setActiveFilter] = useState("All");
const [selectedConnection, setSelectedConnection] = useState(null);
const [searchQuery, setSearchQuery] = useState("");
@@ -60,7 +79,8 @@ export default function ConnectionsDiagram({ connections, serie }) {
searchQuery === "" ||
connection.nome.toLowerCase().includes(searchQuery.toLowerCase()) ||
connection.numero.toString().toLowerCase().includes(searchQuery.toLowerCase());
return matchesFilter && matchesSearch;
const matchesPinos = pinosFiltro.size === 0 || pinosFiltro.has(String(connection.numero));
return matchesFilter && matchesSearch && matchesPinos;
};
const getCategoryColor = (category) => {
@@ -70,7 +90,9 @@ export default function ConnectionsDiagram({ connections, serie }) {
const nomeCategoria = (cat) => (cat === "Strapping" ? "Inicialização" : cat);
const visibleConnections = useMemo(() => connections.filter(isConnectionVisible), [connections, activeFilter, searchQuery]);
const visibleConnections = useMemo(() => connections.filter(isConnectionVisible), [connections, activeFilter, searchQuery, pinosFiltro]);
const mostrarTodos = () => router.replace(`${pathname}#connections`, { scroll: false });
const abrir = (connection, evento) => {
gatilhoRef.current = evento.currentTarget;
@@ -137,6 +159,18 @@ export default function ConnectionsDiagram({ connections, serie }) {
</p>
</div>
{pinosFiltro.size > 0 && (
<div role="status" className="callout flex flex-wrap items-center justify-between gap-3">
<p className="text-sm text-ink">
<strong className="font-semibold">Filtrado pela busca:</strong> {motivoFiltro || "pinos selecionados"} ({pinosFiltro.size}{" "}
{pinosFiltro.size === 1 ? "pino" : "pinos"})
</p>
<button type="button" onClick={mostrarTodos} className="btn btn-ghost btn-sm">
Mostrar todos os pinos
</button>
</div>
)}
<div className="flex flex-col gap-3 sm:flex-row">
<div className="relative flex-1">
<label htmlFor="busca-pino" className="sr-only">Buscar conexão por nome ou número</label>
@@ -191,7 +225,7 @@ export default function ConnectionsDiagram({ connections, serie }) {
<Search className="mx-auto h-8 w-8 text-muted" aria-hidden="true" />
<p className="mt-3 font-semibold text-ink">Nenhuma conexão encontrada</p>
<p className="mt-1 text-sm text-muted">Ajuste a busca ou volte para todas as funções.</p>
<button type="button" onClick={() => { setActiveFilter("All"); setSearchQuery(""); }} className="btn btn-primary btn-sm mt-5">
<button type="button" onClick={() => { setActiveFilter("All"); setSearchQuery(""); if (pinosFiltro.size) mostrarTodos(); }} className="btn btn-primary btn-sm mt-5">
Limpar busca e filtros
</button>
</div>