fix library tag search: dynamic tagGroups, clear tags, cache-bust
Some checks failed
ci/woodpecker/push/deploy Pipeline failed
ci/woodpecker/push/infra-ci Pipeline was successful
coverage 99% coverage
ci/woodpecker/push/ci Pipeline was successful

Three bugs in library.tsx (all previously fixed, lost in UX rewrite):

1. tagGroups now recomputed from items passing current collection +
   search filters — only shows tags that will produce results
2. Collection switching clears selectedTags (prevents stale empty)
3. Cache-buster on fetch("/library.json?v=" + Date.now())

fix #71
This commit is contained in:
kert
2026-03-21 16:03:51 -04:00
parent 0c56ef7b2a
commit 5e1ffb748f

View File

@@ -76,7 +76,7 @@ export default function Library(): JSX.Element {
const searchRef = useRef<HTMLInputElement>(null); const searchRef = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
fetch("/library.json") fetch("/library.json?v=" + Date.now())
.then((r) => r.json()) .then((r) => r.json())
.then((d: LibraryData) => setData(d)) .then((d: LibraryData) => setData(d))
.catch(() => setData({ items: [], collections: [], tags: [] })); .catch(() => setData({ items: [], collections: [], tags: [] }));
@@ -106,11 +106,6 @@ export default function Library(): JSX.Element {
return () => document.removeEventListener("keydown", handler); return () => document.removeEventListener("keydown", handler);
}, []); }, []);
const tagGroups = useMemo(
() => (data ? groupTags(data.tags) : {}),
[data],
);
const filtered = useMemo(() => { const filtered = useMemo(() => {
if (!data) return []; if (!data) return [];
let items = data.items; let items = data.items;
@@ -139,6 +134,38 @@ export default function Library(): JSX.Element {
return items; return items;
}, [data, debouncedSearch, selectedCollection, selectedTags]); }, [data, debouncedSearch, selectedCollection, selectedTags]);
// Recompute visible tags from items that pass collection + search filters
// so clicking a tag is guaranteed to produce results.
const tagGroups = useMemo(() => {
if (!data) return {};
let pool = data.items;
if (selectedCollection) {
pool = pool.filter((i) => i.collections.includes(selectedCollection));
}
if (debouncedSearch.trim()) {
const q = debouncedSearch.toLowerCase();
pool = pool.filter(
(i) =>
i.title.toLowerCase().includes(q) ||
i.abstract.toLowerCase().includes(q) ||
i.tags.some((t) => t.toLowerCase().includes(q)) ||
i.creators.some((c) => c.toLowerCase().includes(q)),
);
}
const counts: Record<string, number> = {};
for (const item of pool) {
for (const t of item.tags) {
counts[t] = (counts[t] || 0) + 1;
}
}
const liveTags: Tag[] = Object.entries(counts).map(([name, count]) => ({
name,
count,
}));
liveTags.sort((a, b) => a.name.localeCompare(b.name));
return groupTags(liveTags);
}, [data, selectedCollection, debouncedSearch]);
const pageItems = useMemo( const pageItems = useMemo(
() => filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE), () => filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE),
[filtered, page], [filtered, page],
@@ -182,6 +209,7 @@ export default function Library(): JSX.Element {
<button <button
onClick={() => { onClick={() => {
setSelectedCollection(""); setSelectedCollection("");
setSelectedTags(new Set());
setPage(0); setPage(0);
}} }}
style={{ style={{
@@ -202,6 +230,7 @@ export default function Library(): JSX.Element {
<button <button
onClick={() => { onClick={() => {
setSelectedCollection(c.key); setSelectedCollection(c.key);
setSelectedTags(new Set());
setPage(0); setPage(0);
}} }}
style={{ style={{