1
- import { useCallback , useEffect , useRef } from "react" ;
2
- import { useSearchParams } from "react-router-dom" ;
1
+ import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
2
+ import { useNavigate , 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" ;
5
7
import { QueryParams } from "@utils/enums" ;
8
+ import { slugify } from "@utils/slugify" ;
6
9
7
- import { SearchIcon } from "./Icons" ;
10
+ import Button from "./Button" ;
11
+ import { CloseIcon , SearchIcon } from "./Icons" ;
8
12
9
13
const SearchInput = ( ) => {
14
+ const navigate = useNavigate ( ) ;
10
15
const [ searchParams , setSearchParams ] = useSearchParams ( ) ;
11
16
12
17
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 ] ) ;
13
64
14
65
const inputRef = useRef < HTMLInputElement | null > ( null ) ;
15
66
67
+ const [ searchOpen , setSearchOpen ] = useState < boolean > ( false ) ;
68
+
16
69
const handleSearchFieldClick = ( ) => {
70
+ setSearchOpen ( true ) ;
71
+ } ;
72
+
73
+ const handleInnerSearchFieldClick = ( ) => {
17
74
inputRef . current ?. focus ( ) ;
18
75
} ;
19
76
@@ -23,31 +80,13 @@ const SearchInput = () => {
23
80
setSearchParams ( searchParams ) ;
24
81
} , [ searchParams , setSearchParams , setSearchText ] ) ;
25
82
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
-
44
83
/**
45
84
* Focus the search input when the user presses the `/` key.
46
85
*/
47
86
const handleSearchKeyPress = ( e : KeyboardEvent ) => {
48
87
if ( e . key === "/" ) {
49
88
e . preventDefault ( ) ;
50
- inputRef . current ?. focus ( ) ;
89
+ setSearchOpen ( true ) ;
51
90
}
52
91
} ;
53
92
@@ -60,18 +99,30 @@ const SearchInput = () => {
60
99
return ;
61
100
}
62
101
63
- // Check if the input element is focused.
64
- if ( document . activeElement !== inputRef . current ) {
65
- return ;
66
- }
67
-
68
- inputRef . current ?. blur ( ) ;
69
-
102
+ setSearchOpen ( false ) ;
70
103
clearSearch ( ) ;
71
104
} ,
72
105
[ clearSearch ]
73
106
) ;
74
107
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
+
75
126
useEffect ( ( ) => {
76
127
window . addEventListener ( "keydown" , handleSearchKeyPress ) ;
77
128
window . addEventListener ( "keyup" , handleEscapeKeyPress ) ;
@@ -82,13 +133,6 @@ const SearchInput = () => {
82
133
} ;
83
134
} , [ handleEscapeKeyPress ] ) ;
84
135
85
- /**
86
- * Update the search query in the URL when the search text changes.
87
- */
88
- useEffect ( ( ) => {
89
- performSearch ( ) ;
90
- } , [ searchText , performSearch ] ) ;
91
-
92
136
/**
93
137
* Set the search text to the search query from the URL on mount.
94
138
*/
@@ -102,30 +146,108 @@ const SearchInput = () => {
102
146
// eslint-disable-next-line react-hooks/exhaustive-deps
103
147
} , [ ] ) ;
104
148
149
+ useEffect ( ( ) => {
150
+ if ( searchOpen ) {
151
+ inputRef . current ?. focus ( ) ;
152
+ }
153
+ } , [ searchOpen ] ) ;
154
+
105
155
return (
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 >
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
+ </ >
129
251
) ;
130
252
} ;
131
253
0 commit comments