diff --git a/.planning/phases/08-search-filter-and-candidate-status/08-RESEARCH.md b/.planning/phases/08-search-filter-and-candidate-status/08-RESEARCH.md new file mode 100644 index 0000000..05681c0 --- /dev/null +++ b/.planning/phases/08-search-filter-and-candidate-status/08-RESEARCH.md @@ -0,0 +1,491 @@ +# Phase 8: Search, Filter, and Candidate Status - Research + +**Researched:** 2026-03-16 +**Domain:** Client-side filtering, searchable dropdown components, schema migration, status badges +**Confidence:** HIGH + +## Summary + +Phase 8 adds three capabilities to GearBox: (1) a search and category filter toolbar on the gear tab with result counts, (2) an icon-aware searchable category filter dropdown shared between gear and planning tabs, and (3) candidate status tracking (researching/ordered/arrived) with clickable status badges. The work spans all layers: schema migration (adding `status` column to `thread_candidates`), service/route updates (CRUD for status field), Zod schema updates, and several new client components. + +The codebase is well-structured for these additions. Client-side filtering is straightforward since `useItems()` already returns all items with category info. The `CategoryPicker` component provides a reference pattern for the searchable dropdown, though the new `CategoryFilterDropdown` is simpler (no creation flow). The candidate status feature requires a schema migration, but Drizzle Kit and the existing migration infrastructure handle this cleanly. + +**Primary recommendation:** Build in two waves -- (1) backend schema migration + candidate status (smaller, foundational), then (2) search/filter toolbar and shared category dropdown (larger, UI-focused). Both waves are pure client-side filtering with minimal server changes. + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions +- Sticky toolbar above the item grid on the gear tab, stays visible on scroll +- Search input + category dropdown side by side in the toolbar +- Client-side filtering on every keystroke (no debounce needed for <1000 items) +- Search matches item names only (not category names) -- category filtering is the dropdown's job +- When any filter is active, items display as a flat grid (no category group headers) +- Filters reset when switching between gear/planning/setups tabs +- Three statuses: researching (default), ordered, arrived +- Status badge appears in the existing pill row alongside weight/price/category pills +- Badge shows icon + text label (e.g., magnifying glass + "researching", truck + "ordered", check + "arrived") +- Muted/neutral color scheme for status badges -- gray tones, not semantic colors +- Click the status badge to open a small popup menu showing all three status options +- New candidates default to "researching" status +- Requires `status` column on `thread_candidates` table (schema migration) +- "Showing X of Y items" count displayed when filters are active +- No combined "clear all" button -- user clears search text and resets category dropdown individually +- "No items match your search" simple text message for empty filter results +- Shared `CategoryFilterDropdown` component used on both gear tab and planning tab +- Separate from existing `CategoryPicker` component +- "All categories" as the first option -- selecting it clears the category filter +- Searchable dropdown with search input inside +- Trigger button shows selected category's Lucide icon + name when selected + +### Claude's Discretion +- Exact toolbar styling (padding, borders, background) +- Filter result count placement (in toolbar or above grid) +- Status popup menu implementation details +- Specific gray tone values for status badges +- Keyboard accessibility patterns for the dropdown and status menu +- Icon choices for status badges (magnifying glass, truck, check are suggestions) + +### Deferred Ideas (OUT OF SCOPE) +None + + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|-----------------| +| SRCH-01 | User can search items by name with instant filtering as they type | Client-side `useState` + `.filter()` on `useItems()` data. Pattern documented in Architecture section | +| SRCH-02 | User can filter collection items by category via dropdown | New `CategoryFilterDropdown` component using `useCategories()` data. Pattern from existing `CategoryPicker` | +| SRCH-03 | User can combine text search with category filter simultaneously | Chain `.filter()` calls -- search text AND category ID. Both stored as `useState` in `CollectionView` | +| SRCH-04 | User can see result count when filters are active | Computed from `filteredItems.length` vs `items.length`. Conditional rendering when filters active | +| SRCH-05 | User can clear all active filters with one action | Per CONTEXT.md: no combined button. User clears search text and resets dropdown individually. Both inputs have clear affordances | +| PLAN-01 | Planning category filter dropdown shows Lucide icons alongside names | Replace existing ` in PlanningView + server/ + services/ + thread.service.ts # MODIFIED - handle status field in create/update candidate + routes/ + threads.ts # NO CHANGES - already delegates to service + shared/ + schemas.ts # MODIFIED - add status to candidate schemas + types.ts # NO CHANGES - types auto-infer from schemas + db/ + schema.ts # MODIFIED - add status column to threadCandidates +tests/ + helpers/ + db.ts # MODIFIED - add status column to thread_candidates CREATE TABLE + services/ + thread.service.test.ts # MODIFIED - add tests for status field +``` + +### Pattern 1: Client-Side Filtering with useState +**What:** Filter items in-memory using React state, no server round-trips +**When to use:** Small datasets (<1000 items), instant feedback needed +**Example:** +```typescript +// In CollectionView +function CollectionView() { + const [searchText, setSearchText] = useState(""); + const [categoryFilter, setCategoryFilter] = useState(null); + const { data: items } = useItems(); + + 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; + // ... +} +``` + +### Pattern 2: Searchable Dropdown with Click-Outside Dismiss +**What:** Dropdown with internal search input, opens on click, closes on click-outside or Escape +**When to use:** Category filter dropdowns where a native ` setSearchText(e.target.value)} + className="flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm ..." + /> + + + +``` + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| Native `` replaced with `CategoryFilterDropdown` | +| No candidate status tracking | `status` column with badge UI | This phase | Candidates now track purchase progress | +| Category-grouped items only | Conditional flat grid when filtering | This phase | Better UX when searching/filtering | + +## Open Questions + +1. **Sticky toolbar `top` offset** + - What we know: The toolbar should be `sticky top-0` but needs to account for any fixed header/navbar if one exists. + - What's unclear: Whether there's a fixed navbar above the collection page that would require a `top-[Npx]` offset instead of `top-0`. + - Recommendation: Start with `top-0`. If there's a fixed navbar, adjust the top value to match its height. The current layout appears to not have a fixed navbar based on the route structure. + +2. **useCandidates hook status mutation** + - What we know: `useUpdateCandidate` already exists and can be used for status changes via `apiPut`. + - What's unclear: Whether a dedicated `useUpdateCandidateStatus` hook is cleaner than reusing the general `useUpdateCandidate`. + - Recommendation: Reuse `useUpdateCandidate` -- it already accepts partial updates. Adding a dedicated hook would be unnecessary abstraction. + +## Validation Architecture + +### Test Framework +| Property | Value | +|----------|-------| +| Framework | Bun test runner (built-in) | +| Config file | None (uses bun defaults) | +| Quick run command | `bun test tests/services/thread.service.test.ts` | +| Full suite command | `bun test` | + +### Phase Requirements to Test Map +| Req ID | Behavior | Test Type | Automated Command | File Exists? | +|--------|----------|-----------|-------------------|-------------| +| SRCH-01 | Search items by name with instant filtering | manual-only | N/A -- client-side `useState` + `filter()`, no testable service | N/A | +| SRCH-02 | Filter by category via dropdown | manual-only | N/A -- client-side component logic | N/A | +| SRCH-03 | Combine text search with category filter | manual-only | N/A -- client-side filtering logic | N/A | +| SRCH-04 | Show result count when filters active | manual-only | N/A -- computed in render | N/A | +| SRCH-05 | Clear filters individually | manual-only | N/A -- UI interaction | N/A | +| PLAN-01 | Category dropdown shows icons | manual-only | N/A -- component rendering | N/A | +| CAND-01 | Candidate displays status badge | unit | `bun test tests/services/thread.service.test.ts` | Needs update | +| CAND-02 | User can change candidate status | unit | `bun test tests/services/thread.service.test.ts` | Needs update | +| CAND-03 | New candidates default to "researching" | unit | `bun test tests/services/thread.service.test.ts` | Needs update | + +### Sampling Rate +- **Per task commit:** `bun test tests/services/thread.service.test.ts` +- **Per wave merge:** `bun test` +- **Phase gate:** Full suite green before `/gsd:verify-work` + +### Wave 0 Gaps +- [ ] `tests/helpers/db.ts` -- add `status TEXT NOT NULL DEFAULT 'researching'` to thread_candidates CREATE TABLE +- [ ] `tests/services/thread.service.test.ts` -- add tests for: (1) createCandidate returns status "researching" by default, (2) updateCandidate can change status, (3) getThreadWithCandidates includes status field + +## Sources + +### Primary (HIGH confidence) +- **Codebase analysis** -- direct reading of all relevant source files: + - `src/db/schema.ts` -- current threadCandidates table definition (no status column) + - `src/client/routes/collection/index.tsx` -- CollectionView (where toolbar goes) and PlanningView (where `