From 5f89acd503925352d92623e7637f02937ed40830 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 16 Mar 2026 14:09:51 +0100 Subject: [PATCH] feat(08-02): add search/filter toolbar to gear tab and upgrade planning filter - Sticky search/filter toolbar with text input and CategoryFilterDropdown - useMemo-based filtering by name (search) and categoryId (dropdown) - "Showing X of Y items" count when filters active - Flat grid (no category headers) when any filter is active - "No items match your search" empty state for filtered results - Replace PlanningView native select with CategoryFilterDropdown --- src/client/routes/collection/index.tsx | 219 +++++++++++++++++-------- 1 file changed, 153 insertions(+), 66 deletions(-) diff --git a/src/client/routes/collection/index.tsx b/src/client/routes/collection/index.tsx index dcd8cae..36e81a3 100644 --- a/src/client/routes/collection/index.tsx +++ b/src/client/routes/collection/index.tsx @@ -1,6 +1,7 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { z } from "zod"; +import { CategoryFilterDropdown } from "../../components/CategoryFilterDropdown"; import { CategoryHeader } from "../../components/CategoryHeader"; import { CreateThreadModal } from "../../components/CreateThreadModal"; import { ItemCard } from "../../components/ItemCard"; @@ -51,8 +52,26 @@ function CollectionPage() { function CollectionView() { const { data: items, isLoading: itemsLoading } = useItems(); const { data: totals } = useTotals(); + const { data: categories } = useCategories(); const openAddPanel = useUIStore((s) => s.openAddPanel); + const [searchText, setSearchText] = useState(""); + const [categoryFilter, setCategoryFilter] = useState(null); + + const filteredItems = useMemo(() => { + if (!items) return []; + return items.filter((item) => { + const matchesSearch = + searchText === "" || + item.name.toLowerCase().includes(searchText.toLowerCase()); + const matchesCategory = + categoryFilter === null || item.categoryId === categoryFilter; + return matchesSearch && matchesCategory; + }); + }, [items, searchText, categoryFilter]); + + const hasActiveFilters = searchText !== "" || categoryFilter !== null; + if (itemsLoading) { return (
@@ -110,25 +129,6 @@ function CollectionView() { ); } - // Group items by categoryId - const groupedItems = new Map< - number, - { items: typeof items; categoryName: string; categoryIcon: string } - >(); - - for (const item of items) { - const group = groupedItems.get(item.categoryId); - if (group) { - group.items.push(item); - } else { - groupedItems.set(item.categoryId, { - items: [item], - categoryName: item.categoryName, - categoryIcon: item.categoryIcon, - }); - } - } - // Build category totals lookup const categoryTotalsMap = new Map< number, @@ -144,42 +144,138 @@ function CollectionView() { } } + // Group filtered items by categoryId (used when no active filters) + const groupedItems = new Map< + number, + { + items: typeof filteredItems; + categoryName: string; + categoryIcon: string; + } + >(); + + for (const item of filteredItems) { + const group = groupedItems.get(item.categoryId); + if (group) { + group.items.push(item); + } else { + groupedItems.set(item.categoryId, { + items: [item], + categoryName: item.categoryName, + categoryIcon: item.categoryIcon, + }); + } + } + return ( <> - {Array.from(groupedItems.entries()).map( - ([ - categoryId, - { items: categoryItems, categoryName, categoryIcon }, - ]) => { - const catTotals = categoryTotalsMap.get(categoryId); - return ( -
- -
- {categoryItems.map((item) => ( - +
+
+ setSearchText(e.target.value)} + className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent" + /> + {searchText && ( + + )} +
+ +
+ {hasActiveFilters && ( +

+ Showing {filteredItems.length} of {items.length} items +

+ )} +
+ + {/* Filtered results */} + {hasActiveFilters ? ( + filteredItems.length === 0 ? ( +
+

+ No items match your search +

+
+ ) : ( +
+ {filteredItems.map((item) => ( + + ))} +
+ ) + ) : ( + Array.from(groupedItems.entries()).map( + ([ + categoryId, + { items: categoryItems, categoryName, categoryIcon }, + ]) => { + const catTotals = categoryTotalsMap.get(categoryId); + return ( +
+ +
+ {categoryItems.map((item) => ( + + ))} +
-
- ); - }, + ); + }, + ) )} ); @@ -274,20 +370,11 @@ function PlanningView() {
{/* Category filter */} - + {/* Content: empty state or thread grid */}