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" ;
33
44import { useAppContext } from "@contexts/AppContext" ;
5+ import { useFetch } from "@hooks/useFetch" ;
6+ import { AllSnippetsType , SearchItemType } from "@types" ;
57import { QueryParams } from "@utils/enums" ;
8+ import { slugify } from "@utils/slugify" ;
69
7- import { SearchIcon } from "./Icons" ;
10+ import Button from "./Button" ;
11+ import { CloseIcon , SearchIcon } from "./Icons" ;
812
913const SearchInput = ( ) => {
14+ const navigate = useNavigate ( ) ;
1015 const [ searchParams , setSearchParams ] = useSearchParams ( ) ;
1116
1217 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 ] ) ;
1364
1465 const inputRef = useRef < HTMLInputElement | null > ( null ) ;
1566
67+ const [ searchOpen , setSearchOpen ] = useState < boolean > ( false ) ;
68+
1669 const handleSearchFieldClick = ( ) => {
70+ setSearchOpen ( true ) ;
71+ } ;
72+
73+ const handleInnerSearchFieldClick = ( ) => {
1774 inputRef . current ?. focus ( ) ;
1875 } ;
1976
@@ -23,31 +80,13 @@ const SearchInput = () => {
2380 setSearchParams ( searchParams ) ;
2481 } , [ searchParams , setSearchParams , setSearchText ] ) ;
2582
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-
4483 /**
4584 * Focus the search input when the user presses the `/` key.
4685 */
4786 const handleSearchKeyPress = ( e : KeyboardEvent ) => {
4887 if ( e . key === "/" ) {
4988 e . preventDefault ( ) ;
50- inputRef . current ?. focus ( ) ;
89+ setSearchOpen ( true ) ;
5190 }
5291 } ;
5392
@@ -60,18 +99,30 @@ const SearchInput = () => {
6099 return ;
61100 }
62101
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 ) ;
70103 clearSearch ( ) ;
71104 } ,
72105 [ clearSearch ]
73106 ) ;
74107
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+
75126 useEffect ( ( ) => {
76127 window . addEventListener ( "keydown" , handleSearchKeyPress ) ;
77128 window . addEventListener ( "keyup" , handleEscapeKeyPress ) ;
@@ -82,13 +133,6 @@ const SearchInput = () => {
82133 } ;
83134 } , [ handleEscapeKeyPress ] ) ;
84135
85- /**
86- * Update the search query in the URL when the search text changes.
87- */
88- useEffect ( ( ) => {
89- performSearch ( ) ;
90- } , [ searchText , performSearch ] ) ;
91-
92136 /**
93137 * Set the search text to the search query from the URL on mount.
94138 */
@@ -102,30 +146,108 @@ const SearchInput = () => {
102146 // eslint-disable-next-line react-hooks/exhaustive-deps
103147 } , [ ] ) ;
104148
149+ useEffect ( ( ) => {
150+ if ( searchOpen ) {
151+ inputRef . current ?. focus ( ) ;
152+ }
153+ } , [ searchOpen ] ) ;
154+
105155 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+ </ >
129251 ) ;
130252} ;
131253
0 commit comments