11'use client' ;
22
33import React , { useState } from 'react' ;
4- import { ChevronLeft } from 'lucide-react' ;
5- import { Button } from '../ui/button' ;
4+ import type { Verb , Category } from '../../../types/entries' ;
65import verbData from '../../../data/verbs.json' ;
76import 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
5511interface 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