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" ;
33
44import { useAppContext } from "@contexts/AppContext" ;
5- import { useFetch } from "@hooks/useFetch" ;
6- import { AllSnippetsType , SearchItemType } from "@types" ;
75import { QueryParams } from "@utils/enums" ;
8- import { slugify } from "@utils/slugify" ;
96
10- import Button from "./Button" ;
11- import { CloseIcon , SearchIcon } from "./Icons" ;
7+ import { SearchIcon } from "./Icons" ;
128
139const SearchInput = ( ) => {
14- const navigate = useNavigate ( ) ;
1510 const [ searchParams , setSearchParams ] = useSearchParams ( ) ;
1611
1712 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 ] ) ;
6413
6514 const inputRef = useRef < HTMLInputElement | null > ( null ) ;
6615
67- const [ searchOpen , setSearchOpen ] = useState < boolean > ( false ) ;
68-
6916 const handleSearchFieldClick = ( ) => {
70- setSearchOpen ( true ) ;
71- } ;
72-
73- const handleInnerSearchFieldClick = ( ) => {
7417 inputRef . current ?. focus ( ) ;
7518 } ;
7619
@@ -80,13 +23,31 @@ const SearchInput = () => {
8023 setSearchParams ( searchParams ) ;
8124 } , [ searchParams , setSearchParams , setSearchText ] ) ;
8225
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+
8344 /**
8445 * Focus the search input when the user presses the `/` key.
8546 */
8647 const handleSearchKeyPress = ( e : KeyboardEvent ) => {
8748 if ( e . key === "/" ) {
8849 e . preventDefault ( ) ;
89- setSearchOpen ( true ) ;
50+ inputRef . current ?. focus ( ) ;
9051 }
9152 } ;
9253
@@ -99,30 +60,18 @@ const SearchInput = () => {
9960 return ;
10061 }
10162
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+
10370 clearSearch ( ) ;
10471 } ,
10572 [ clearSearch ]
10673 ) ;
10774
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-
12675 useEffect ( ( ) => {
12776 window . addEventListener ( "keydown" , handleSearchKeyPress ) ;
12877 window . addEventListener ( "keyup" , handleEscapeKeyPress ) ;
@@ -133,6 +82,13 @@ const SearchInput = () => {
13382 } ;
13483 } , [ handleEscapeKeyPress ] ) ;
13584
85+ /**
86+ * Update the search query in the URL when the search text changes.
87+ */
88+ useEffect ( ( ) => {
89+ performSearch ( ) ;
90+ } , [ searchText , performSearch ] ) ;
91+
13692 /**
13793 * Set the search text to the search query from the URL on mount.
13894 */
@@ -146,108 +102,30 @@ const SearchInput = () => {
146102 // eslint-disable-next-line react-hooks/exhaustive-deps
147103 } , [ ] ) ;
148104
149- useEffect ( ( ) => {
150- if ( searchOpen ) {
151- inputRef . current ?. focus ( ) ;
152- }
153- } , [ searchOpen ] ) ;
154-
155105 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 >
251129 ) ;
252130} ;
253131
0 commit comments