2
2
import { Button } from '@/components/ui/button' ;
3
3
import { Input } from '@/components/ui/input' ;
4
4
import { Settings } from 'lucide-react' ;
5
- import { useCallback , useEffect } from 'react' ;
6
- import { useSearchStore } from '@/state/useSearchStore' ;
7
5
import { FilterSelect } from './FilterSelect' ;
8
6
import { FilterBadge } from './FilterBadge' ;
9
- import { useQueryState , parseAsArrayOf , parseAsString } from 'nuqs' ;
7
+ import { TopicsPopover } from './TopicsPopover' ;
8
+ import { FilterState } from '@/hooks/useFilteredProblems' ;
9
+ import { useState , useEffect } from 'react' ;
10
+ import { useDebounce } from '@/hooks/useDebounce' ;
10
11
11
12
const DIFFICULTY_OPTIONS = [
12
13
{ value : '1' , label : 'Easy' } ,
@@ -19,86 +20,53 @@ const STATUS_OPTIONS = [
19
20
{ value : 'solved' , label : 'Solved' } ,
20
21
] ;
21
22
22
- // TODO: replace with backend fetched list
23
- const TOPIC_OPTIONS = [
24
- { value : 'array' , label : 'Array' } ,
25
- { value : 'string' , label : 'String' } ,
26
- { value : 'dp' , label : 'Dynamic Programming' } ,
27
- ] ;
28
-
29
23
interface FilterBarProps {
30
- fetchProblems : ( params : URLSearchParams ) => Promise < void > ;
24
+ filters : FilterState ;
25
+ updateFilter : (
26
+ key : keyof FilterState ,
27
+ value : string | string [ ] | null ,
28
+ ) => void ;
29
+ removeFilter : ( key : keyof FilterState , value ?: string ) => void ;
31
30
}
32
31
33
- export default function FilterBar ( { fetchProblems } : FilterBarProps ) {
34
- const [ difficulty , setDifficulty ] = useQueryState ( 'difficulty' ) ;
35
- const [ status , setStatus ] = useQueryState ( 'status' ) ;
36
- const [ topics , setTopics ] = useQueryState (
37
- 'topics' ,
38
- parseAsArrayOf ( parseAsString ) ,
39
- ) ;
40
- const { searchTerm, setSearchTerm } = useSearchStore ( ) ;
41
-
42
- const handleFilterChange = useCallback (
43
- ( key : string , value : string | string [ ] ) => {
44
- if ( key === 'difficulty' ) {
45
- setDifficulty ( value === 'all' ? null : ( value as string ) ) ;
46
- } else if ( key === 'status' ) {
47
- setStatus ( value === 'all' ? null : ( value as string ) ) ;
48
- } else if ( key === 'topics' ) {
49
- setTopics ( value . length ? ( value as string [ ] ) : null ) ;
50
- }
51
- } ,
52
- [ setDifficulty , setStatus , setTopics ] ,
53
- ) ;
32
+ export default function FilterBar ( {
33
+ filters,
34
+ updateFilter,
35
+ removeFilter,
36
+ } : FilterBarProps ) {
37
+ const [ searchTerm , setSearchTerm ] = useState ( '' ) ;
38
+ const debouncedSearchTerm = useDebounce ( searchTerm , 300 ) ; // 300ms delay
54
39
55
- const removeFilter = useCallback (
56
- ( key : string , value ?: string ) => {
57
- if ( key === 'difficulty' ) {
58
- setDifficulty ( null ) ;
59
- } else if ( key === 'status' ) {
60
- setStatus ( null ) ;
61
- } else if ( key === 'topics' ) {
62
- setTopics ( ( prev ) => prev ?. filter ( ( t ) => t !== value ) ?? null ) ;
63
- }
64
- } ,
65
- [ setDifficulty , setStatus , setTopics ] ,
66
- ) ;
40
+ /**
41
+ * Debounce so that search filters does not call backend for
42
+ * every single character input, but only after 300ms of no input
43
+ */
44
+ useEffect ( ( ) => {
45
+ updateFilter ( 'search' , debouncedSearchTerm ) ;
46
+ } , [ debouncedSearchTerm , updateFilter ] ) ;
67
47
68
48
const handleSearch = ( e : React . ChangeEvent < HTMLInputElement > ) => {
69
49
setSearchTerm ( e . target . value ) ;
70
50
} ;
71
51
72
- useEffect ( ( ) => {
73
- const params = new URLSearchParams ( ) ;
74
- if ( difficulty ) params . append ( 'difficulty' , difficulty ) ;
75
- if ( status ) params . append ( 'status' , status ) ;
76
- if ( topics ) topics . forEach ( ( topic ) => params . append ( 'topics' , topic ) ) ;
77
- if ( searchTerm ) params . append ( 'search' , searchTerm ) ;
78
- fetchProblems ( params ) ;
79
- } , [ difficulty , status , topics , searchTerm , fetchProblems ] ) ;
80
-
81
52
return (
82
53
< div className = "mb-6" >
83
54
< div className = "mb-4 flex flex-wrap gap-4" >
84
55
< FilterSelect
85
56
placeholder = "Difficulty"
86
57
options = { DIFFICULTY_OPTIONS }
87
- onChange = { ( value ) => handleFilterChange ( 'difficulty' , value ) }
88
- value = { difficulty || '' }
58
+ onChange = { ( value ) => updateFilter ( 'difficulty' , value ) }
59
+ value = { filters . difficulty || '' }
89
60
/>
90
61
< FilterSelect
91
62
placeholder = "Status"
92
63
options = { STATUS_OPTIONS }
93
- onChange = { ( value ) => handleFilterChange ( 'status' , value ) }
94
- value = { status || '' }
64
+ onChange = { ( value ) => updateFilter ( 'status' , value ) }
65
+ value = { filters . status || '' }
95
66
/>
96
- < FilterSelect
97
- placeholder = "Topics"
98
- options = { TOPIC_OPTIONS }
99
- onChange = { ( value ) => handleFilterChange ( 'topics' , value ) }
100
- value = { topics || [ ] }
101
- isMulti
67
+ < TopicsPopover
68
+ selectedTopics = { filters . topics || [ ] }
69
+ onChange = { ( value ) => updateFilter ( 'topics' , value ) }
102
70
/>
103
71
< div className = "flex-grow" >
104
72
< Input
@@ -120,33 +88,32 @@ export default function FilterBar({ fetchProblems }: FilterBarProps) {
120
88
</ Button >
121
89
</ div >
122
90
< div className = "flex flex-wrap gap-2" >
123
- { difficulty && (
91
+ { filters . difficulty && (
124
92
< FilterBadge
125
93
filterType = "difficulty"
126
94
value = {
127
- DIFFICULTY_OPTIONS . find ( ( opt ) => opt . value === difficulty )
95
+ DIFFICULTY_OPTIONS . find ( ( opt ) => opt . value === filters . difficulty )
128
96
?. label || ''
129
97
}
130
98
onRemove = { ( ) => removeFilter ( 'difficulty' ) }
131
99
/>
132
100
) }
133
- { status && (
101
+ { filters . status && (
134
102
< FilterBadge
135
103
filterType = "status"
136
104
value = {
137
- STATUS_OPTIONS . find ( ( opt ) => opt . value === status ) ?. label || ''
105
+ STATUS_OPTIONS . find ( ( opt ) => opt . value === filters . status )
106
+ ?. label || ''
138
107
}
139
108
onRemove = { ( ) => removeFilter ( 'status' ) }
140
109
/>
141
110
) }
142
- { topics &&
143
- topics . map ( ( topic ) => (
111
+ { filters . topics &&
112
+ filters . topics . map ( ( topic ) => (
144
113
< FilterBadge
145
114
key = { `topics-${ topic } ` }
146
115
filterType = "topics"
147
- value = {
148
- TOPIC_OPTIONS . find ( ( opt ) => opt . value === topic ) ?. label || topic
149
- }
116
+ value = { topic }
150
117
onRemove = { ( ) => removeFilter ( 'topics' , topic ) }
151
118
/>
152
119
) ) }
0 commit comments