Skip to content

Commit f68287f

Browse files
committed
feat: implemented filters to verbPicker
1 parent 1b2fadc commit f68287f

File tree

6 files changed

+276
-197
lines changed

6 files changed

+276
-197
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import { Button } from '../ui/button';
5+
import type { Category } from '../../../types/entries';
6+
import { getContrastColor } from '../../../utils/colorUtils';
7+
8+
interface FilterBarProps {
9+
rootCategory: Category;
10+
currentCategory: Category | null;
11+
path: Category[];
12+
onSelectCategory: (cat: Category) => void;
13+
onBreadcrumbClick: (index: number) => void;
14+
}
15+
16+
const FilterBar: React.FC<FilterBarProps> = ({
17+
rootCategory,
18+
currentCategory,
19+
path,
20+
onSelectCategory,
21+
onBreadcrumbClick,
22+
}) => {
23+
const topCategories = rootCategory.children ?? [];
24+
25+
return (
26+
<div className='flex flex-col gap-2'>
27+
{/* Breadcrumbs Row */}
28+
<div className='flex items-center gap-2 px-4 py-2 border-b bg-gray-50 flex-wrap'>
29+
<span
30+
onClick={() => onBreadcrumbClick(-1)}
31+
className='cursor-pointer text-blue-600 hover:underline'
32+
>
33+
All
34+
</span>
35+
{path.map((cat, index) => (
36+
<React.Fragment key={cat.id}>
37+
<span className='text-gray-500'>/</span>
38+
<span
39+
onClick={() => onBreadcrumbClick(index)}
40+
className='cursor-pointer text-blue-600 hover:underline'
41+
>
42+
{cat.displayName}
43+
</span>
44+
</React.Fragment>
45+
))}
46+
</div>
47+
48+
{/* Filter Bar Row using grid layout */}
49+
<div
50+
className='
51+
grid
52+
w-full
53+
gap-4
54+
px-4
55+
py-2
56+
border-b
57+
bg-gray-100
58+
grid-cols-[repeat(auto-fill,minmax(150px,1fr))]
59+
'
60+
>
61+
{/* CASE 1: No filter selected → show top-level categories */}
62+
{!currentCategory &&
63+
topCategories.map((cat) => (
64+
<Button
65+
key={cat.id}
66+
onClick={() => onSelectCategory(cat)}
67+
variant='outline'
68+
className='flex items-center gap-1 text-sm'
69+
style={{
70+
backgroundColor: cat.color,
71+
color: getContrastColor(cat.color),
72+
borderColor: cat.color,
73+
}}
74+
>
75+
<span>{cat.icon}</span>
76+
<span>{cat.displayName}</span>
77+
</Button>
78+
))}
79+
80+
{/* CASE 2: A filter is selected and it has children */}
81+
{currentCategory &&
82+
currentCategory.children &&
83+
currentCategory.children.length > 0 &&
84+
currentCategory.children.map((child) => {
85+
// If there's exactly one child at this level, add col-span-full so it fills the row.
86+
const spanClass =
87+
currentCategory.children?.length === 1 ? 'col-span-full' : '';
88+
return (
89+
<Button
90+
key={child.id}
91+
onClick={() => onSelectCategory(child)}
92+
variant='outline'
93+
className={`flex items-center gap-1 text-sm ${spanClass}`}
94+
style={{
95+
backgroundColor: child.color,
96+
color: getContrastColor(child.color),
97+
borderColor: child.color,
98+
}}
99+
>
100+
<span>{child.icon}</span>
101+
<span>{child.displayName}</span>
102+
</Button>
103+
);
104+
})}
105+
106+
{/* CASE 3: A filter is selected and it's a leaf (no children) */}
107+
{/* CASE 3: A filter is selected and it's a leaf (no children) */}
108+
{currentCategory &&
109+
(!currentCategory.children ||
110+
currentCategory.children.length === 0) && (
111+
<div
112+
className='flex items-center gap-1 text-sm col-span-full cursor-default select-none rounded-lg shadow-md px-4 py-2'
113+
style={{
114+
backgroundColor: currentCategory.color,
115+
color: getContrastColor(currentCategory.color),
116+
border: `1px solid ${currentCategory.color}`,
117+
}}
118+
>
119+
<span>{currentCategory.icon}</span>
120+
<span>{currentCategory.displayName}</span>
121+
</div>
122+
)}
123+
</div>
124+
</div>
125+
);
126+
};
127+
128+
export default FilterBar;

src/components/statementWizard/SentimentVerbPicker.tsx

Lines changed: 40 additions & 194 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,12 @@
11
'use client';
22

33
import React, { useState } from 'react';
4-
import { ChevronLeft } from 'lucide-react';
5-
import { Button } from '../ui/button';
4+
import type { Verb, Category } from '../../../types/entries';
65
import verbData from '../../../data/verbs.json';
76
import categoryStructure from '../../../data/categoryStructure.json';
8-
import type { Verb, Category } from '../../../types/entries';
9-
import { getContrastColor } from '../../../utils/colorUtils';
10-
11-
/**
12-
* findCategoryByName:
13-
* Recursively searches the category tree for a node matching the given "name".
14-
*/
15-
function findCategoryByName(root: Category, name: string): Category | null {
16-
if (root.name === name) return root;
17-
if (root.children) {
18-
for (const child of root.children) {
19-
const found = findCategoryByName(child, name);
20-
if (found) return found;
21-
}
22-
}
23-
return null;
24-
}
25-
26-
/**
27-
* getAllDescendants:
28-
* Returns an array of all category names under "node" (including the node's own).
29-
*/
30-
function getAllDescendants(node: Category): string[] {
31-
const result: string[] = [node.name];
32-
if (node.children) {
33-
for (const child of node.children) {
34-
result.push(...getAllDescendants(child));
35-
}
36-
}
37-
return result;
38-
}
39-
40-
/**
41-
* getVerbColor:
42-
* For a given verb, returns the color of the first matching category in the entire tree.
43-
* If no match is found, returns 'transparent'.
44-
*/
45-
function getVerbColor(verb: Verb, root: Category): string {
46-
for (const catName of verb.categories) {
47-
const cat = findCategoryByName(root, catName);
48-
if (cat) {
49-
return cat.color;
50-
}
51-
}
52-
return 'transparent';
53-
}
7+
import FilterBar from './FilterBar';
8+
import VerbGrid from './VerbGrid';
9+
import { getAllDescendants } from '../../../utils/categoryUtils';
5410

5511
interface SentimentVerbPickerProps {
5612
selectedVerb: string;
@@ -61,165 +17,55 @@ const SentimentVerbPicker: React.FC<SentimentVerbPickerProps> = ({
6117
selectedVerb,
6218
onVerbSelect,
6319
}) => {
64-
// Root of your category structure
20+
// Use a navigation stack (path) to track filter levels.
21+
// An empty path means "All" is selected.
22+
const [path, setPath] = useState<Category[]>([]);
6523
const rootCategory = categoryStructure.root as Category;
24+
const currentCategory = path.length > 0 ? path[path.length - 1] : null;
25+
26+
// When a category is selected, push it onto the path.
27+
const handleSelectCategory = (cat: Category) => {
28+
setPath([...path, cat]);
29+
};
30+
31+
// Handler for breadcrumb clicks:
32+
// Clicking a breadcrumb sets the path to that level.
33+
const handleBreadcrumbClick = (index: number) => {
34+
if (index === -1) {
35+
setPath([]);
36+
} else {
37+
setPath(path.slice(0, index + 1));
38+
}
39+
};
6640

67-
// If currentCategory is null => "All" is selected (no filter).
68-
const [currentCategory, setCurrentCategory] = useState<Category | null>(null);
69-
70-
// ---------------------------------
71-
// FILTERING LOGIC
72-
// ---------------------------------
73-
/**
74-
* If no category is selected => show all verbs.
75-
* Otherwise => gather the union of the current category's descendants.
76-
*/
41+
// Filter verbs:
42+
// If no category is selected, show all verbs.
43+
// Otherwise, show verbs that have at least one category included in the union
44+
// of currentCategory's descendants.
7745
let allowedNames: string[] = [];
7846
if (currentCategory) {
7947
allowedNames = getAllDescendants(currentCategory);
8048
}
81-
8249
const filteredVerbs = (verbData.verbs as Verb[]).filter((verb) => {
83-
// "All" => no filtering
8450
if (!currentCategory) return true;
85-
// Otherwise => any intersection with the category's descendant names
8651
return verb.categories.some((catName) => allowedNames.includes(catName));
8752
});
8853

89-
// ---------------------------------
90-
// FILTER BAR
91-
// ---------------------------------
92-
/**
93-
* Layout logic:
94-
* - If no category => we show [All (highlighted)] + top-level children of root
95-
* - If a category has children => show [Back Icon] + that category's children
96-
* - If a category is a leaf => show [Back Icon] + leaf name
97-
*/
98-
function renderFilterBar() {
99-
const topLevelChildren = rootCategory.children ?? [];
100-
const isAllSelected = !currentCategory;
101-
const hasChildren =
102-
currentCategory?.children && currentCategory.children.length > 0;
103-
104-
return (
105-
<div className='flex items-center gap-2 px-4 py-2 border-b bg-gray-100 overflow-x-auto flex-nowrap whitespace-nowrap'>
106-
{/* CASE 1: No category => show "All" + top-level categories */}
107-
{isAllSelected && (
108-
<>
109-
<Button variant='default' className='text-sm'>
110-
All
111-
</Button>
112-
{topLevelChildren.map((cat) => (
113-
<Button
114-
key={cat.id}
115-
variant='outline'
116-
className='flex items-center gap-1 text-sm whitespace-nowrap'
117-
style={{
118-
backgroundColor: cat.color,
119-
color: getContrastColor(cat.color),
120-
borderColor: cat.color,
121-
}}
122-
onClick={() => setCurrentCategory(cat)}
123-
>
124-
<span>{cat.icon}</span>
125-
<span>{cat.displayName}</span>
126-
</Button>
127-
))}
128-
</>
129-
)}
130-
131-
{/* CASE 2: A category with children => show [Back] + child categories */}
132-
{!isAllSelected && hasChildren && currentCategory && (
133-
<>
134-
<Button
135-
variant='ghost'
136-
className='p-2'
137-
onClick={() => setCurrentCategory(null)}
138-
>
139-
<ChevronLeft size={24} />
140-
</Button>
141-
{currentCategory.children.map((child) => (
142-
<Button
143-
key={child.id}
144-
variant='outline'
145-
className='flex items-center gap-1 text-sm'
146-
style={{
147-
backgroundColor: child.color,
148-
color: getContrastColor(child.color),
149-
borderColor: child.color,
150-
}}
151-
onClick={() => setCurrentCategory(child)}
152-
>
153-
<span>{child.icon}</span>
154-
<span>{child.displayName}</span>
155-
</Button>
156-
))}
157-
</>
158-
)}
159-
160-
{/* CASE 3: A leaf category => show [Back] + the leaf's name */}
161-
{!isAllSelected && !hasChildren && currentCategory && (
162-
<>
163-
<Button
164-
variant='ghost'
165-
className='p-2'
166-
onClick={() => setCurrentCategory(null)}
167-
>
168-
<ChevronLeft size={24} />
169-
</Button>
170-
<Button
171-
variant='default'
172-
className='flex items-center gap-1 text-sm'
173-
style={{
174-
backgroundColor: currentCategory.color,
175-
color: getContrastColor(currentCategory.color),
176-
borderColor: currentCategory.color,
177-
}}
178-
>
179-
<span>{currentCategory.icon}</span>
180-
<span>{currentCategory.displayName}</span>
181-
</Button>
182-
</>
183-
)}
184-
</div>
185-
);
186-
}
187-
188-
// ---------------------------------
189-
// VERB GRID
190-
// ---------------------------------
191-
function renderVerbGrid() {
192-
return (
193-
<div className='grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 gap-4 p-4 overflow-auto'>
194-
{filteredVerbs
195-
.sort((a, b) => a.name.localeCompare(b.name))
196-
.map((verb) => {
197-
const tileColor = getVerbColor(verb, rootCategory);
198-
const isSelected = verb.name === selectedVerb;
199-
return (
200-
<Button
201-
key={verb.name}
202-
onClick={() => onVerbSelect(verb)}
203-
variant={isSelected ? 'default' : 'outline'}
204-
className='flex items-center justify-center p-4 rounded-lg shadow-md'
205-
style={{
206-
backgroundColor: isSelected ? tileColor : 'transparent',
207-
color: isSelected ? getContrastColor(tileColor) : 'inherit',
208-
borderColor: tileColor,
209-
}}
210-
>
211-
<span className='font-medium'>{verb.name}</span>
212-
</Button>
213-
);
214-
})}
215-
</div>
216-
);
217-
}
218-
21954
return (
22055
<div className='flex flex-col h-full'>
221-
{renderFilterBar()}
222-
{renderVerbGrid()}
56+
<FilterBar
57+
rootCategory={rootCategory}
58+
currentCategory={currentCategory}
59+
path={path}
60+
onSelectCategory={handleSelectCategory}
61+
onBreadcrumbClick={handleBreadcrumbClick}
62+
/>
63+
<VerbGrid
64+
verbs={filteredVerbs}
65+
rootCategory={rootCategory}
66+
selectedVerb={selectedVerb}
67+
onVerbSelect={onVerbSelect}
68+
/>
22369
</div>
22470
);
22571
};

0 commit comments

Comments
 (0)