docs(08): create phase plan
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
---
|
||||
phase: 08-search-filter-and-candidate-status
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/db/schema.ts
|
||||
- src/shared/schemas.ts
|
||||
- src/server/services/thread.service.ts
|
||||
- src/client/hooks/useThreads.ts
|
||||
- src/client/hooks/useCandidates.ts
|
||||
- src/client/components/StatusBadge.tsx
|
||||
- src/client/components/CandidateCard.tsx
|
||||
- tests/helpers/db.ts
|
||||
- tests/services/thread.service.test.ts
|
||||
autonomous: true
|
||||
requirements: [CAND-01, CAND-02, CAND-03]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Each candidate displays a status badge showing one of three statuses: researching, ordered, or arrived"
|
||||
- "User can click a status badge to open a popup menu and change the candidate's status to any of the three options"
|
||||
- "New candidates automatically have status 'researching' without the user needing to set it"
|
||||
artifacts:
|
||||
- path: "src/db/schema.ts"
|
||||
provides: "status column on threadCandidates table"
|
||||
contains: "status: text(\"status\").notNull().default(\"researching\")"
|
||||
- path: "src/shared/schemas.ts"
|
||||
provides: "candidateStatusSchema Zod enum"
|
||||
exports: ["candidateStatusSchema"]
|
||||
- path: "src/server/services/thread.service.ts"
|
||||
provides: "status field in candidate CRUD operations"
|
||||
contains: "status: threadCandidates.status"
|
||||
- path: "src/client/components/StatusBadge.tsx"
|
||||
provides: "Clickable status badge with popup menu"
|
||||
exports: ["StatusBadge"]
|
||||
- path: "src/client/components/CandidateCard.tsx"
|
||||
provides: "CandidateCard renders StatusBadge in pill row"
|
||||
contains: "StatusBadge"
|
||||
- path: "tests/helpers/db.ts"
|
||||
provides: "status column in test helper CREATE TABLE"
|
||||
contains: "status TEXT NOT NULL DEFAULT 'researching'"
|
||||
key_links:
|
||||
- from: "src/client/components/StatusBadge.tsx"
|
||||
to: "/api/threads/:id/candidates/:candidateId"
|
||||
via: "useUpdateCandidate mutation"
|
||||
pattern: "onStatusChange"
|
||||
- from: "src/server/services/thread.service.ts"
|
||||
to: "src/db/schema.ts"
|
||||
via: "threadCandidates.status in select and update"
|
||||
pattern: "threadCandidates\\.status"
|
||||
- from: "src/client/components/CandidateCard.tsx"
|
||||
to: "src/client/components/StatusBadge.tsx"
|
||||
via: "StatusBadge component in pill row"
|
||||
pattern: "<StatusBadge"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add candidate status tracking (researching/ordered/arrived) as a full vertical slice: schema migration, service/Zod updates, tests, and clickable status badge UI on CandidateCard.
|
||||
|
||||
Purpose: Let users track purchase progress for candidates they are evaluating in planning threads.
|
||||
Output: Working status badge on each candidate card with popup menu to change status.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/jlmak/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/08-search-filter-and-candidate-status/08-CONTEXT.md
|
||||
@.planning/phases/08-search-filter-and-candidate-status/08-RESEARCH.md
|
||||
|
||||
@src/db/schema.ts
|
||||
@src/shared/schemas.ts
|
||||
@src/shared/types.ts
|
||||
@src/server/services/thread.service.ts
|
||||
@src/client/hooks/useThreads.ts
|
||||
@src/client/hooks/useCandidates.ts
|
||||
@src/client/components/CandidateCard.tsx
|
||||
@tests/helpers/db.ts
|
||||
@tests/services/thread.service.test.ts
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
|
||||
|
||||
From src/shared/types.ts:
|
||||
```typescript
|
||||
export type CreateCandidate = z.infer<typeof createCandidateSchema>;
|
||||
export type UpdateCandidate = z.infer<typeof updateCandidateSchema>;
|
||||
export type ThreadCandidate = typeof threadCandidates.$inferSelect;
|
||||
```
|
||||
|
||||
From src/client/hooks/useCandidates.ts:
|
||||
```typescript
|
||||
export function useUpdateCandidate(threadId: number) {
|
||||
// mutationFn: ({ candidateId, ...data }) => apiPut(...)
|
||||
// Already accepts partial updates. Use for status changes.
|
||||
}
|
||||
```
|
||||
|
||||
From src/client/hooks/useThreads.ts:
|
||||
```typescript
|
||||
interface CandidateWithCategory {
|
||||
id: number; threadId: number; name: string;
|
||||
weightGrams: number | null; priceCents: number | null;
|
||||
categoryId: number; notes: string | null;
|
||||
productUrl: string | null; imageFilename: string | null;
|
||||
createdAt: string; updatedAt: string;
|
||||
categoryName: string; categoryIcon: string;
|
||||
// status field NOT YET present -- Task 1 adds it
|
||||
}
|
||||
```
|
||||
|
||||
From src/client/components/CandidateCard.tsx:
|
||||
```typescript
|
||||
interface CandidateCardProps {
|
||||
id: number; name: string; weightGrams: number | null;
|
||||
priceCents: number | null; categoryName: string;
|
||||
categoryIcon: string; imageFilename: string | null;
|
||||
productUrl?: string | null; threadId: number; isActive: boolean;
|
||||
// status prop NOT YET present -- Task 2 adds it
|
||||
}
|
||||
```
|
||||
|
||||
From src/client/lib/iconData.tsx:
|
||||
```typescript
|
||||
export function LucideIcon({ name, size, className }: {
|
||||
name: string; size?: number; className?: string;
|
||||
}): JSX.Element;
|
||||
// Valid icon names for status: "search", "truck", "check"
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Add status column and update backend + tests</name>
|
||||
<files>src/db/schema.ts, src/shared/schemas.ts, src/server/services/thread.service.ts, src/client/hooks/useThreads.ts, src/client/hooks/useCandidates.ts, tests/helpers/db.ts, tests/services/thread.service.test.ts</files>
|
||||
<behavior>
|
||||
- Test: createCandidate without status returns a candidate with status "researching"
|
||||
- Test: createCandidate with status "ordered" returns a candidate with status "ordered"
|
||||
- Test: updateCandidate can change status from "researching" to "ordered"
|
||||
- Test: updateCandidate can change status from "ordered" to "arrived"
|
||||
- Test: getThreadWithCandidates includes status field on each candidate
|
||||
</behavior>
|
||||
<action>
|
||||
1. **Schema migration** -- Add status column to `threadCandidates` in `src/db/schema.ts`:
|
||||
```typescript
|
||||
status: text("status").notNull().default("researching"),
|
||||
```
|
||||
Then run `bun run db:generate && bun run db:push` to apply.
|
||||
|
||||
2. **Zod schemas** -- In `src/shared/schemas.ts`, add:
|
||||
```typescript
|
||||
export const candidateStatusSchema = z.enum(["researching", "ordered", "arrived"]);
|
||||
```
|
||||
Add `status: candidateStatusSchema.optional()` to `createCandidateSchema`. Since `updateCandidateSchema = createCandidateSchema.partial()`, it automatically includes status as optional.
|
||||
|
||||
3. **Service updates** -- In `src/server/services/thread.service.ts`:
|
||||
- In `getThreadWithCandidates`, add `status: threadCandidates.status` to the select object (between `imageFilename` and `createdAt`).
|
||||
- In `createCandidate`, add `status: data.status ?? "researching"` to the values object.
|
||||
- In `updateCandidate`, add `status` to the data type: `status: "researching" | "ordered" | "arrived"`.
|
||||
|
||||
4. **Client type updates** -- In `src/client/hooks/useThreads.ts`, add `status: "researching" | "ordered" | "arrived"` to `CandidateWithCategory` interface. In `src/client/hooks/useCandidates.ts`, add `status?: "researching" | "ordered" | "arrived"` to `CandidateResponse` interface.
|
||||
|
||||
5. **Test helper** -- In `tests/helpers/db.ts`, add `status TEXT NOT NULL DEFAULT 'researching'` to the `thread_candidates` CREATE TABLE statement (after `image_filename TEXT` line).
|
||||
|
||||
6. **Service tests** -- In `tests/services/thread.service.test.ts`, add a describe block "candidate status" with the tests from the behavior section above.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun test tests/services/thread.service.test.ts</automated>
|
||||
</verify>
|
||||
<done>Status column exists in schema, migration applied, all CRUD operations handle status field, all tests pass including new status tests.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Create StatusBadge component and wire into CandidateCard</name>
|
||||
<files>src/client/components/StatusBadge.tsx, src/client/components/CandidateCard.tsx</files>
|
||||
<action>
|
||||
1. **Create `src/client/components/StatusBadge.tsx`** -- A clickable pill badge with popup menu:
|
||||
- Props: `status: "researching" | "ordered" | "arrived"`, `onStatusChange: (status: "researching" | "ordered" | "arrived") => void`
|
||||
- Status config map:
|
||||
```typescript
|
||||
const STATUS_CONFIG = {
|
||||
researching: { icon: "search", label: "Researching" },
|
||||
ordered: { icon: "truck", label: "Ordered" },
|
||||
arrived: { icon: "check", label: "Arrived" },
|
||||
} as const;
|
||||
```
|
||||
- Render as a pill button (muted gray tones per user decision -- NOT semantic colors):
|
||||
- Use `bg-gray-100 text-gray-600` styling, similar neutral tone to the category pill
|
||||
- Show `LucideIcon` (size 14) + text label
|
||||
- On click: call `e.stopPropagation()` (prevent card click propagation per pitfall #3), toggle popup menu open/closed
|
||||
- Popup menu: `position: absolute` below the badge, `right-0`, with 3 options (each showing icon + label). Use a `containerRef` + `useEffect` mousedown listener for click-outside dismiss (same pattern as `CategoryPicker`). Pressing Escape also closes the menu.
|
||||
- When an option is clicked: call `onStatusChange(selectedStatus)`, close the menu.
|
||||
- Show a subtle checkmark or different background on the currently active status in the menu.
|
||||
|
||||
2. **Update `src/client/components/CandidateCard.tsx`**:
|
||||
- Add `status: "researching" | "ordered" | "arrived"` and `onStatusChange: (status: "researching" | "ordered" | "arrived") => void` to `CandidateCardProps`.
|
||||
- Import `StatusBadge` from `./StatusBadge`.
|
||||
- Add `<StatusBadge status={status} onStatusChange={onStatusChange} />` to the pill row (the `flex flex-wrap gap-1.5 mb-3` div), after the category pill.
|
||||
|
||||
3. **Update thread detail page caller** -- Find where `CandidateCard` is rendered (in the thread detail route). Add the `status` and `onStatusChange` props. For `onStatusChange`, use the existing `useUpdateCandidate` hook: `updateCandidate.mutate({ candidateId: candidate.id, status: newStatus })`.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun run lint && bun test</automated>
|
||||
</verify>
|
||||
<done>Each candidate card shows a gray status badge (icon + label) in the pill row. Clicking the badge opens a popup menu with all three status options. Selecting a status updates it via the API and the badge reflects the new status. New candidates show "Researching" by default.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. `bun test` -- all existing and new tests pass
|
||||
2. `bun run lint` -- no lint errors
|
||||
3. Start dev server (`bun run dev:server` + `bun run dev:client`), navigate to a thread detail page, verify:
|
||||
- Each candidate shows a gray "Researching" badge in the pill row
|
||||
- Clicking the badge opens a popup menu with Researching, Ordered, Arrived options
|
||||
- Selecting a different status updates the badge immediately
|
||||
- Refreshing the page shows the persisted status
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Status column exists on thread_candidates table with default "researching"
|
||||
- All candidate CRUD operations handle the status field
|
||||
- StatusBadge component renders in CandidateCard pill row with muted gray styling
|
||||
- Clicking badge opens popup menu, selecting an option changes status via API
|
||||
- New candidates show "researching" status by default
|
||||
- All tests pass including 5 new status-specific tests
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/08-search-filter-and-candidate-status/08-01-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,292 @@
|
||||
---
|
||||
phase: 08-search-filter-and-candidate-status
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/client/components/CategoryFilterDropdown.tsx
|
||||
- src/client/routes/collection/index.tsx
|
||||
autonomous: true
|
||||
requirements: [SRCH-01, SRCH-02, SRCH-03, SRCH-04, SRCH-05, PLAN-01]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "User can type in a search field on the gear tab and see items filtered instantly by name as they type"
|
||||
- "User can select a category from a searchable dropdown (with Lucide icons) to filter items on both gear and planning tabs"
|
||||
- "User can combine text search with category filter to narrow results"
|
||||
- "User sees 'Showing X of Y items' when filters are active on the gear tab"
|
||||
- "User clears search text and resets category dropdown individually (no combined clear button)"
|
||||
- "When filters are active, items display as a flat grid without category group headers"
|
||||
- "Empty filter results show 'No items match your search' message"
|
||||
- "Planning tab category filter shows Lucide icons alongside category names"
|
||||
artifacts:
|
||||
- path: "src/client/components/CategoryFilterDropdown.tsx"
|
||||
provides: "Shared searchable category filter dropdown with Lucide icons"
|
||||
exports: ["CategoryFilterDropdown"]
|
||||
min_lines: 60
|
||||
- path: "src/client/routes/collection/index.tsx"
|
||||
provides: "Search/filter toolbar in CollectionView, CategoryFilterDropdown in PlanningView"
|
||||
contains: "CategoryFilterDropdown"
|
||||
key_links:
|
||||
- from: "src/client/components/CategoryFilterDropdown.tsx"
|
||||
to: "src/client/hooks/useCategories.ts"
|
||||
via: "categories prop passed from parent (useCategories data)"
|
||||
pattern: "categories"
|
||||
- from: "src/client/routes/collection/index.tsx (CollectionView)"
|
||||
to: "src/client/components/CategoryFilterDropdown.tsx"
|
||||
via: "CategoryFilterDropdown in sticky toolbar"
|
||||
pattern: "<CategoryFilterDropdown"
|
||||
- from: "src/client/routes/collection/index.tsx (PlanningView)"
|
||||
to: "src/client/components/CategoryFilterDropdown.tsx"
|
||||
via: "CategoryFilterDropdown replacing native select"
|
||||
pattern: "<CategoryFilterDropdown"
|
||||
- from: "src/client/routes/collection/index.tsx (CollectionView)"
|
||||
to: "useItems data"
|
||||
via: "useMemo filter chain on searchText + categoryFilter"
|
||||
pattern: "filteredItems"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add search/filter toolbar to the gear tab and a shared icon-aware category filter dropdown to both gear and planning tabs. Users can search items by name, filter by category, see result counts, and clear filters individually.
|
||||
|
||||
Purpose: Help users find items quickly as collections grow, and upgrade the planning tab's plain `<select>` to a searchable icon-aware dropdown.
|
||||
Output: Sticky search/filter toolbar on gear tab, shared CategoryFilterDropdown component on both tabs.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/jlmak/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/08-search-filter-and-candidate-status/08-CONTEXT.md
|
||||
@.planning/phases/08-search-filter-and-candidate-status/08-RESEARCH.md
|
||||
|
||||
@src/client/routes/collection/index.tsx
|
||||
@src/client/components/CategoryPicker.tsx
|
||||
@src/client/hooks/useCategories.ts
|
||||
@src/client/hooks/useItems.ts
|
||||
@src/client/lib/iconData.tsx
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
|
||||
|
||||
From src/client/hooks/useItems.ts:
|
||||
```typescript
|
||||
// useItems() returns items with these fields:
|
||||
interface ItemWithCategory {
|
||||
id: number; name: string; weightGrams: number | null;
|
||||
priceCents: number | null; categoryId: number;
|
||||
notes: string | null; productUrl: string | null;
|
||||
imageFilename: string | null; createdAt: string; updatedAt: string;
|
||||
categoryName: string; categoryIcon: string;
|
||||
}
|
||||
```
|
||||
|
||||
From src/client/hooks/useCategories.ts:
|
||||
```typescript
|
||||
// useCategories() returns:
|
||||
interface CategoryItem {
|
||||
id: number; name: string; icon: string; createdAt: string;
|
||||
}
|
||||
```
|
||||
|
||||
From src/client/lib/iconData.tsx:
|
||||
```typescript
|
||||
export function LucideIcon({ name, size, className }: {
|
||||
name: string; size?: number; className?: string;
|
||||
}): JSX.Element;
|
||||
```
|
||||
|
||||
From src/client/routes/collection/index.tsx:
|
||||
```typescript
|
||||
// CollectionView currently:
|
||||
// - Uses useItems() for all items
|
||||
// - Groups items by categoryId into Map
|
||||
// - Renders CategoryHeader + grid per category group
|
||||
// - No search or filter state
|
||||
|
||||
// PlanningView currently:
|
||||
// - Has categoryFilter useState<number | null>(null)
|
||||
// - Uses a native <select> for category filtering (lines 277-291)
|
||||
// - Filters threads by activeTab and categoryFilter
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create CategoryFilterDropdown component</name>
|
||||
<files>src/client/components/CategoryFilterDropdown.tsx</files>
|
||||
<action>
|
||||
Create `src/client/components/CategoryFilterDropdown.tsx` -- a searchable dropdown showing categories with Lucide icons. This is a FILTER dropdown, NOT the form-based `CategoryPicker` (which handles creation). Keep them separate per user decision.
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface CategoryFilterDropdownProps {
|
||||
value: number | null; // selected category ID, null = "All categories"
|
||||
onChange: (value: number | null) => void;
|
||||
categories: Array<{ id: number; name: string; icon: string }>;
|
||||
}
|
||||
```
|
||||
|
||||
**Structure:**
|
||||
- **Trigger button**: Shows "All categories" with a chevron-down icon when `value` is null. Shows the selected category's `LucideIcon` (size 14) + name when a category is selected. Style: `px-3 py-2 border border-gray-200 rounded-lg text-sm bg-white` (matching search input height). Include a small clear "x" button on the right when a category is selected (clicking it calls `onChange(null)` without opening the dropdown).
|
||||
- **Dropdown panel**: Opens below the trigger, `position: absolute`, `z-20`, white bg, border, rounded-lg, shadow-lg, max-height with overflow-y-auto. Width matches trigger or has a reasonable min-width (~220px).
|
||||
- **Search input inside dropdown**: Text input at top of dropdown, placeholder "Search categories...", filters the category list as user types. Auto-focused when dropdown opens.
|
||||
- **Option list**: "All categories" as first option (selecting calls `onChange(null)` and closes). Then each category: `LucideIcon` (size 16) + category name. Highlight the currently selected option with a subtle bg color. Hover state on each option.
|
||||
- **Click-outside dismiss**: Use `containerRef` + `useEffect` mousedown listener pattern (same as `CategoryPicker`). Also close on Escape keydown.
|
||||
- **State reset**: Clear internal search text when dropdown closes.
|
||||
|
||||
**Do NOT:**
|
||||
- Reuse or modify `CategoryPicker.tsx`
|
||||
- Add category creation capability
|
||||
- Use Zustand for dropdown open/closed state (use local `useState`)
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun run lint</automated>
|
||||
</verify>
|
||||
<done>CategoryFilterDropdown.tsx exists with searchable dropdown, Lucide icons per option, "All categories" first option, click-outside dismiss, clear button on trigger, and Escape to close. Lint passes.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add search/filter toolbar to CollectionView and replace select in PlanningView</name>
|
||||
<files>src/client/routes/collection/index.tsx</files>
|
||||
<action>
|
||||
Modify `src/client/routes/collection/index.tsx` to add search and filtering to `CollectionView` and upgrade `PlanningView`'s category filter.
|
||||
|
||||
**CollectionView changes:**
|
||||
|
||||
1. Add filter state at the top of `CollectionView`:
|
||||
```typescript
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [categoryFilter, setCategoryFilter] = useState<number | null>(null);
|
||||
```
|
||||
|
||||
2. Add `useCategories` hook: `const { data: categories } = useCategories();`
|
||||
|
||||
3. Add filtered items computation with `useMemo`:
|
||||
```typescript
|
||||
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]);
|
||||
```
|
||||
Import `useMemo` from React, import `useCategories` from hooks.
|
||||
|
||||
4. Compute filter state:
|
||||
```typescript
|
||||
const hasActiveFilters = searchText !== "" || categoryFilter !== null;
|
||||
```
|
||||
|
||||
5. Add sticky toolbar ABOVE the existing item grid rendering (after loading/empty checks, before the grouped items). The toolbar only shows when there are items:
|
||||
```jsx
|
||||
<div className="sticky top-0 z-10 bg-white/95 backdrop-blur-sm border-b border-gray-100 -mx-4 px-4 py-3 sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8 mb-6">
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="relative flex-1">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search items..."
|
||||
value={searchText}
|
||||
onChange={(e) => 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 && (
|
||||
<button onClick={() => setSearchText("")} className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600">
|
||||
{/* small x icon */}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<CategoryFilterDropdown
|
||||
value={categoryFilter}
|
||||
onChange={setCategoryFilter}
|
||||
categories={categories ?? []}
|
||||
/>
|
||||
</div>
|
||||
{hasActiveFilters && (
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
Showing {filteredItems.length} of {items?.length ?? 0} items
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
```
|
||||
|
||||
6. Conditional rendering based on filter state:
|
||||
- **When `hasActiveFilters` is true**: Render `filteredItems` as a flat grid (no category grouping, no `CategoryHeader`). If `filteredItems.length === 0`, show "No items match your search" centered text message.
|
||||
- **When `hasActiveFilters` is false**: Keep existing category-grouped rendering exactly as-is (the `groupedItems` Map pattern), but use `filteredItems` as the source (which equals all items when no filters).
|
||||
|
||||
**PlanningView changes:**
|
||||
|
||||
1. Import `CategoryFilterDropdown` from `../../components/CategoryFilterDropdown`.
|
||||
2. Replace the native `<select>` element (lines ~277-291) with:
|
||||
```jsx
|
||||
<CategoryFilterDropdown
|
||||
value={categoryFilter}
|
||||
onChange={setCategoryFilter}
|
||||
categories={categories ?? []}
|
||||
/>
|
||||
```
|
||||
3. Remove the `useCategories` hook call if it's already called earlier, or keep it -- just make sure categories data is available.
|
||||
|
||||
**Important per user decisions:**
|
||||
- Search matches item names ONLY (not category names) -- the dropdown handles category filtering
|
||||
- No debounce on search input (per CONTEXT.md, <1000 items)
|
||||
- No combined "clear all" button -- user clears search and dropdown individually
|
||||
- Filters naturally reset on tab switch because `CollectionView` unmounts when tab changes (conditional rendering in `CollectionPage`). Verify this is the case -- if `CollectionView` stays mounted, add a `key={tab}` prop to force remount.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun run lint && bun test</automated>
|
||||
</verify>
|
||||
<done>Gear tab has a sticky search/filter toolbar with text input and CategoryFilterDropdown side by side. Typing filters items by name instantly. Selecting a category filters by category. Both filters combine. "Showing X of Y items" appears when filters are active. Empty results show message. Flat grid renders when filters active (no category headers). Planning tab uses CategoryFilterDropdown with Lucide icons instead of native select. All tests and lint pass.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. `bun run lint` -- no lint errors
|
||||
2. `bun test` -- all tests pass
|
||||
3. Start dev server, navigate to gear tab:
|
||||
- Sticky toolbar visible with search input + category dropdown
|
||||
- Type in search: items filter by name instantly
|
||||
- Select a category from dropdown (icons visible): items filter by category
|
||||
- Both filters combine correctly
|
||||
- "Showing X of Y items" text appears when filters active
|
||||
- Empty results show "No items match your search"
|
||||
- Filtered items show as flat grid (no category headers)
|
||||
- Clear search text: category filter still applies
|
||||
- Select "All categories": search filter still applies
|
||||
- Switch to planning tab: filters reset
|
||||
- Switch back to gear tab: filters reset (clean state)
|
||||
4. Navigate to planning tab:
|
||||
- Category filter dropdown shows Lucide icons alongside names
|
||||
- Searchable within the dropdown
|
||||
- "All categories" as first option
|
||||
- Selecting a category shows icon + name in trigger button
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Search input filters items by name on every keystroke (no debounce)
|
||||
- CategoryFilterDropdown shows icons, is searchable, has "All categories" option
|
||||
- Filters combine (text AND category)
|
||||
- Result count displayed when filters active
|
||||
- Flat grid (no category headers) when any filter active
|
||||
- "No items match your search" on empty results
|
||||
- Filters reset on tab switch
|
||||
- Planning tab uses shared CategoryFilterDropdown instead of native select
|
||||
- Lint and tests pass
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/08-search-filter-and-candidate-status/08-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user