1
- import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
2
- import { useNavigate , useSearchParams } from "react-router-dom" ;
1
+ import { useCallback , useEffect , useRef } from "react" ;
2
+ import { useSearchParams } from "react-router-dom" ;
3
3
4
4
import { useAppContext } from "@contexts/AppContext" ;
5
- import { useFetch } from "@hooks/useFetch" ;
6
- import { AllSnippetsType , SearchItemType } from "@types" ;
7
5
import { QueryParams } from "@utils/enums" ;
8
- import { slugify } from "@utils/slugify" ;
9
6
10
- import Button from "./Button" ;
11
- import { CloseIcon , SearchIcon } from "./Icons" ;
7
+ import { SearchIcon } from "./Icons" ;
12
8
13
9
const SearchInput = ( ) => {
14
- const navigate = useNavigate ( ) ;
15
10
const [ searchParams , setSearchParams ] = useSearchParams ( ) ;
16
11
17
12
const { searchText, setSearchText } = useAppContext ( ) ;
18
- const { data } = useFetch < AllSnippetsType [ ] > ( `/consolidated/all.json` ) ;
19
-
20
- const filteredData : SearchItemType [ ] = useMemo ( ( ) => {
21
- if ( ! data ) {
22
- return [ ] ;
23
- }
24
-
25
- const searchTerm = searchText . toLowerCase ( ) ;
26
-
27
- return data
28
- . map ( ( language ) => {
29
- const filteredCategories = language . categories
30
- . map ( ( category ) => {
31
- const filteredSnippets = category . snippets . filter (
32
- ( snippet ) =>
33
- snippet . title . toLowerCase ( ) . includes ( searchTerm ) ||
34
- snippet . description . toLowerCase ( ) . includes ( searchTerm ) ||
35
- snippet . tags . some ( ( tag ) =>
36
- tag . toLowerCase ( ) . includes ( searchTerm )
37
- )
38
- ) ;
39
-
40
- if ( filteredSnippets . length > 0 ) {
41
- return {
42
- categoryName : category . name ,
43
- snippets : filteredSnippets ,
44
- } ;
45
- }
46
-
47
- return null ;
48
- } )
49
- . filter ( Boolean ) ; // Remove null categories
50
-
51
- if ( filteredCategories . length > 0 ) {
52
- return filteredCategories . map ( ( filteredCategory ) => ( {
53
- languageName : language . languageName ,
54
- languageIcon : language . languageIcon ,
55
- categoryName : filteredCategory ! . categoryName ,
56
- snippets : filteredCategory ! . snippets ,
57
- } ) ) ;
58
- }
59
-
60
- return [ ] ;
61
- } )
62
- . flat ( ) ;
63
- } , [ data , searchText ] ) ;
64
13
65
14
const inputRef = useRef < HTMLInputElement | null > ( null ) ;
66
15
67
- const [ searchOpen , setSearchOpen ] = useState < boolean > ( false ) ;
68
-
69
16
const handleSearchFieldClick = ( ) => {
70
- setSearchOpen ( true ) ;
71
- } ;
72
-
73
- const handleInnerSearchFieldClick = ( ) => {
74
17
inputRef . current ?. focus ( ) ;
75
18
} ;
76
19
@@ -80,13 +23,31 @@ const SearchInput = () => {
80
23
setSearchParams ( searchParams ) ;
81
24
} , [ searchParams , setSearchParams , setSearchText ] ) ;
82
25
26
+ const performSearch = useCallback ( ( ) => {
27
+ // Check if the input element is focused.
28
+ if ( document . activeElement !== inputRef . current ) {
29
+ return ;
30
+ }
31
+
32
+ const formattedVal = searchText . toLowerCase ( ) ;
33
+
34
+ setSearchText ( formattedVal ) ;
35
+ if ( ! formattedVal ) {
36
+ searchParams . delete ( QueryParams . SEARCH ) ;
37
+ setSearchParams ( searchParams ) ;
38
+ } else {
39
+ searchParams . set ( QueryParams . SEARCH , formattedVal ) ;
40
+ setSearchParams ( searchParams ) ;
41
+ }
42
+ } , [ searchParams , searchText , setSearchParams , setSearchText ] ) ;
43
+
83
44
/**
84
45
* Focus the search input when the user presses the `/` key.
85
46
*/
86
47
const handleSearchKeyPress = ( e : KeyboardEvent ) => {
87
48
if ( e . key === "/" ) {
88
49
e . preventDefault ( ) ;
89
- setSearchOpen ( true ) ;
50
+ inputRef . current ?. focus ( ) ;
90
51
}
91
52
} ;
92
53
@@ -99,30 +60,18 @@ const SearchInput = () => {
99
60
return ;
100
61
}
101
62
102
- setSearchOpen ( false ) ;
63
+ // Check if the input element is focused.
64
+ if ( document . activeElement !== inputRef . current ) {
65
+ return ;
66
+ }
67
+
68
+ inputRef . current ?. blur ( ) ;
69
+
103
70
clearSearch ( ) ;
104
71
} ,
105
72
[ clearSearch ]
106
73
) ;
107
74
108
- const handleSearchItemClick =
109
- ( {
110
- languageName,
111
- categoryName,
112
- snippetName,
113
- } : {
114
- languageName : string ;
115
- categoryName : string ;
116
- snippetName : string ;
117
- } ) =>
118
- ( ) => {
119
- navigate (
120
- `/${ slugify ( languageName ) } /${ slugify ( categoryName ) } ?${ QueryParams . SEARCH } =${ searchText . toLowerCase ( ) } &${ QueryParams . SNIPPET } =${ slugify ( snippetName ) } ` ,
121
- { replace : true }
122
- ) ;
123
- setSearchOpen ( false ) ;
124
- } ;
125
-
126
75
useEffect ( ( ) => {
127
76
window . addEventListener ( "keydown" , handleSearchKeyPress ) ;
128
77
window . addEventListener ( "keyup" , handleEscapeKeyPress ) ;
@@ -133,6 +82,13 @@ const SearchInput = () => {
133
82
} ;
134
83
} , [ handleEscapeKeyPress ] ) ;
135
84
85
+ /**
86
+ * Update the search query in the URL when the search text changes.
87
+ */
88
+ useEffect ( ( ) => {
89
+ performSearch ( ) ;
90
+ } , [ searchText , performSearch ] ) ;
91
+
136
92
/**
137
93
* Set the search text to the search query from the URL on mount.
138
94
*/
@@ -146,108 +102,30 @@ const SearchInput = () => {
146
102
// eslint-disable-next-line react-hooks/exhaustive-deps
147
103
} , [ ] ) ;
148
104
149
- useEffect ( ( ) => {
150
- if ( searchOpen ) {
151
- inputRef . current ?. focus ( ) ;
152
- }
153
- } , [ searchOpen ] ) ;
154
-
155
105
return (
156
- < >
157
- < div className = "search-field" onClick = { handleSearchFieldClick } >
158
- < SearchIcon />
159
- < input
160
- disabled
161
- id = "search"
162
- type = "text"
163
- value = { searchText }
164
- onChange = { ( ) => { } }
165
- />
166
- { ! searchText && (
167
- < label htmlFor = "search" >
168
- Type < kbd > /</ kbd > to search
169
- </ label >
170
- ) }
171
- { searchText && (
172
- < Button
173
- isIcon = { true }
174
- className = "search-field__clear"
175
- onClick = { ( e : React . MouseEvent ) => {
176
- e . stopPropagation ( ) ;
177
- clearSearch ( ) ;
178
- } }
179
- >
180
- < CloseIcon width = "20" height = "20" />
181
- </ Button >
182
- ) }
183
- </ div >
184
-
185
- < div
186
- className = { `search-field__results search-field__results${ searchOpen ? "--open" : "--closed" } ` }
187
- >
188
- < div
189
- className = "search-field search-field--inner"
190
- onClick = { handleInnerSearchFieldClick }
191
- >
192
- < SearchIcon />
193
- < input
194
- ref = { inputRef }
195
- value = { searchText }
196
- type = "text"
197
- autoComplete = "off"
198
- onChange = { ( e ) => {
199
- const newValue = e . target . value ;
200
- if ( ! newValue ) {
201
- clearSearch ( ) ;
202
- return ;
203
- }
204
- setSearchText ( newValue ) ;
205
- } }
206
- />
207
- < Button
208
- isIcon = { true }
209
- onClick = { ( ) => {
210
- setSearchOpen ( false ) ;
211
- clearSearch ( ) ;
212
- } }
213
- >
214
- < CloseIcon />
215
- </ Button >
216
- </ div >
217
-
218
- < div className = "search-field__results__list" >
219
- { filteredData . map (
220
- (
221
- { languageName, languageIcon, categoryName, snippets } ,
222
- languageIndex
223
- ) => (
224
- < div key = { `${ languageName } -${ languageIndex } ` } >
225
- < ul >
226
- { snippets . map ( ( snippet , snippetIndex ) => (
227
- < li
228
- key = { `${ languageName } -${ categoryName } -${ snippetIndex } ` }
229
- onClick = { handleSearchItemClick ( {
230
- languageName,
231
- categoryName,
232
- snippetName : snippet . title ,
233
- } ) }
234
- >
235
- < img src = { languageIcon } alt = { languageName } />
236
- < div >
237
- < h4 >
238
- { snippet . title } ({ languageName } )
239
- </ h4 >
240
- < p > { snippet . description } </ p >
241
- </ div >
242
- </ li >
243
- ) ) }
244
- </ ul >
245
- </ div >
246
- )
247
- ) }
248
- </ div >
249
- </ div >
250
- </ >
106
+ < div className = "search-field" onClick = { handleSearchFieldClick } >
107
+ < SearchIcon />
108
+ < input
109
+ ref = { inputRef }
110
+ value = { searchText }
111
+ type = "search"
112
+ id = "search"
113
+ autoComplete = "off"
114
+ onChange = { ( e ) => {
115
+ const newValue = e . target . value ;
116
+ if ( ! newValue ) {
117
+ clearSearch ( ) ;
118
+ return ;
119
+ }
120
+ setSearchText ( newValue ) ;
121
+ } }
122
+ />
123
+ { ! searchText && (
124
+ < label htmlFor = "search" >
125
+ Type < kbd > /</ kbd > to search
126
+ </ label >
127
+ ) }
128
+ </ div >
251
129
) ;
252
130
} ;
253
131
0 commit comments