novo fix
This commit is contained in:
1 parent
822ee2d557
commit
ad0025ef84
7 files changed
+173
-106
No files matched your search
@@ -1,38 +1,45 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import Fuse from "fuse.js";
|
||||
import type { Structure } from "../lib/api.js";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { api, type Structure } from "../lib/api.js";
|
||||
import { useAtlasStore } from "../store/useAtlasStore.js";
|
||||
import { useSystems } from "../hooks/useStructures.js";
|
||||
|
||||
export function SearchNav({ structures }: { structures: Structure[] }) {
|
||||
// Busca e árvore rodam no servidor (não pré-carregamos as ~5000 estruturas
|
||||
// no cliente — isso estourava timeout de proxy). A busca usa debounce +
|
||||
// `/api/structures?q=`; a árvore carrega os itens de cada sistema só
|
||||
// quando o usuário expande aquela seção.
|
||||
export function SearchNav() {
|
||||
const [query, setQuery] = useState("");
|
||||
const [results, setResults] = useState<Structure[]>([]);
|
||||
const [searching, setSearching] = useState(false);
|
||||
|
||||
const systems = useSystems();
|
||||
const [expanded, setExpanded] = useState<Record<string, Structure[] | "loading">>({});
|
||||
|
||||
const selectStructure = useAtlasStore((s) => s.selectStructure);
|
||||
const focusOnStructure = useAtlasStore((s) => s.focusOnStructure);
|
||||
const setVisibleSystems = useAtlasStore((s) => s.setVisibleSystems);
|
||||
const visibleSystems = useAtlasStore((s) => s.visibleSystems);
|
||||
|
||||
const fuse = useMemo(
|
||||
() =>
|
||||
new Fuse(structures, {
|
||||
keys: ["namePt", "nameEn", "synonyms"],
|
||||
threshold: 0.35,
|
||||
}),
|
||||
[structures],
|
||||
);
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
const results = query.trim() ? fuse.search(query).slice(0, 20).map((r) => r.item) : [];
|
||||
|
||||
const tree = useMemo(() => {
|
||||
const root = new Map<string, Map<string, Structure[]>>();
|
||||
for (const s of structures) {
|
||||
const system = s.system.namePt;
|
||||
const region = s.regionPath[s.regionPath.length - 1] ?? "Geral";
|
||||
if (!root.has(system)) root.set(system, new Map());
|
||||
const regionMap = root.get(system)!;
|
||||
if (!regionMap.has(region)) regionMap.set(region, []);
|
||||
regionMap.get(region)!.push(s);
|
||||
useEffect(() => {
|
||||
const q = query.trim();
|
||||
if (!q) {
|
||||
setResults([]);
|
||||
setSearching(false);
|
||||
return;
|
||||
}
|
||||
return root;
|
||||
}, [structures]);
|
||||
setSearching(true);
|
||||
clearTimeout(debounceRef.current);
|
||||
debounceRef.current = setTimeout(() => {
|
||||
api
|
||||
.structures({ q })
|
||||
.then((data) => setResults(data.slice(0, 40)))
|
||||
.catch(console.error)
|
||||
.finally(() => setSearching(false));
|
||||
}, 300);
|
||||
return () => clearTimeout(debounceRef.current);
|
||||
}, [query]);
|
||||
|
||||
function goTo(structure: Structure) {
|
||||
if (!visibleSystems.has(structure.system.slug)) {
|
||||
@@ -42,6 +49,28 @@ export function SearchNav({ structures }: { structures: Structure[] }) {
|
||||
focusOnStructure(structure.slug);
|
||||
}
|
||||
|
||||
function toggleExpand(systemSlug: string) {
|
||||
setExpanded((prev) => {
|
||||
if (prev[systemSlug]) {
|
||||
const next = { ...prev };
|
||||
delete next[systemSlug];
|
||||
return next;
|
||||
}
|
||||
return { ...prev, [systemSlug]: "loading" };
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
for (const [slug, value] of Object.entries(expanded)) {
|
||||
if (value === "loading") {
|
||||
api
|
||||
.structures({ system: slug })
|
||||
.then((data) => setExpanded((prev) => ({ ...prev, [slug]: data })))
|
||||
.catch(console.error);
|
||||
}
|
||||
}
|
||||
}, [expanded]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 h-full min-h-0">
|
||||
<input
|
||||
@@ -54,64 +83,60 @@ export function SearchNav({ structures }: { structures: Structure[] }) {
|
||||
<div className="overflow-y-auto flex-1 min-h-0 pr-1">
|
||||
{query.trim() ? (
|
||||
<ul className="space-y-1">
|
||||
{results.map((s) => (
|
||||
<li key={s.id}>
|
||||
<button
|
||||
onClick={() => goTo(s)}
|
||||
className="w-full text-left text-sm rounded px-2 py-1 hover:bg-white/5"
|
||||
>
|
||||
{s.namePt}
|
||||
<span className="text-slate-500 text-xs"> — {s.system.namePt}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{results.length === 0 && (
|
||||
{searching && <li className="text-sm text-slate-500 px-2">Buscando...</li>}
|
||||
{!searching &&
|
||||
results.map((s) => (
|
||||
<li key={s.id}>
|
||||
<button
|
||||
onClick={() => goTo(s)}
|
||||
className="w-full text-left text-sm rounded px-2 py-1 hover:bg-white/5"
|
||||
>
|
||||
{s.namePt}
|
||||
<span className="text-slate-500 text-xs"> — {s.system.namePt}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{!searching && results.length === 0 && (
|
||||
<li className="text-sm text-slate-500 px-2">Nenhuma estrutura encontrada.</li>
|
||||
)}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{[...tree.entries()].map(([system, regions]) => (
|
||||
<details key={system} open={false} className="text-sm">
|
||||
<summary className="cursor-pointer text-slate-300 hover:text-white">{system}</summary>
|
||||
<div className="pl-3 mt-1 space-y-2">
|
||||
{[...regions.entries()].map(([region, items]) =>
|
||||
// Estruturas reais (Z-Anatomy) não têm sub-região própria
|
||||
// — regionPath tem só o nome do sistema, então a região
|
||||
// aqui seria idêntica ao título acima. Nesse caso lista
|
||||
// direto, sem um nível de agrupamento redundante.
|
||||
region === system ? (
|
||||
<ul key={region} className="space-y-0.5">
|
||||
{items.map((s) => (
|
||||
<li key={s.id}>
|
||||
<button
|
||||
onClick={() => goTo(s)}
|
||||
className="w-full text-left text-sm rounded px-2 py-1 hover:bg-white/5"
|
||||
>
|
||||
{s.namePt}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<details key={region}>
|
||||
<summary className="cursor-pointer text-slate-400 hover:text-white text-xs uppercase tracking-wide">
|
||||
{region}
|
||||
</summary>
|
||||
<ul className="pl-3 mt-1 space-y-0.5">
|
||||
{items.map((s) => (
|
||||
<li key={s.id}>
|
||||
<button
|
||||
onClick={() => goTo(s)}
|
||||
className="w-full text-left text-sm rounded px-2 py-1 hover:bg-white/5"
|
||||
>
|
||||
{s.namePt}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
),
|
||||
<div className="space-y-1">
|
||||
{systems.map((sys) => (
|
||||
<details
|
||||
key={sys.slug}
|
||||
className="text-sm"
|
||||
onToggle={(e) => {
|
||||
if ((e.target as HTMLDetailsElement).open) toggleExpand(sys.slug);
|
||||
}}
|
||||
>
|
||||
<summary className="cursor-pointer text-slate-300 hover:text-white flex items-center gap-2">
|
||||
<span
|
||||
className="inline-block w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: sys.colorHex }}
|
||||
/>
|
||||
{sys.namePt}
|
||||
{sys._count && (
|
||||
<span className="text-xs text-slate-500">({sys._count.structures})</span>
|
||||
)}
|
||||
</summary>
|
||||
<div className="pl-4 mt-1">
|
||||
{expanded[sys.slug] === "loading" && (
|
||||
<p className="text-xs text-slate-500 px-2">Carregando...</p>
|
||||
)}
|
||||
{Array.isArray(expanded[sys.slug]) && (
|
||||
<ul className="space-y-0.5 max-h-64 overflow-y-auto">
|
||||
{(expanded[sys.slug] as Structure[]).map((s) => (
|
||||
<li key={s.id}>
|
||||
<button
|
||||
onClick={() => goTo(s)}
|
||||
className="w-full text-left text-sm rounded px-2 py-1 hover:bg-white/5"
|
||||
>
|
||||
{s.namePt}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
Reference in new issue
Block a user