1- import React , { FC , useEffect , useState } from "react" ;
1+ import React , { FC , useEffect , useState , useCallback } from "react" ;
22import { useLocation , useNavigate } from "react-router" ;
33import { FaArrowRight , FaSearch } from "react-icons/fa" ;
4-
4+ import { API_ENDPOINTS } from "../lib/config" ;
5+ import { useNotifications } from "../lib/notifications" ;
56import Footer from "../components/Footer" ;
67import { getErrorMessage } from "../lib/errors" ;
7- import { useNotifications } from "../lib/notifications" ;
8+ import debounce from 'lodash/debounce' ;
9+
10+ interface TopicSuggestion {
11+ name : string ;
12+ count : number ;
13+ }
814
915const HomeView : FC = ( ) => {
1016 const navigate = useNavigate ( ) ;
1117 const location = useLocation ( ) ;
1218 const { notify } = useNotifications ( ) ;
1319 const error = ( ( location . state as { error ?: unknown } | undefined ) ?. error || "" ) + "" ;
1420
15- // Add state for the search term
21+ // Add state for the search term and suggestions
1622 const [ searchTerm , setSearchTerm ] = useState ( "" ) ;
23+ const [ suggestions , setSuggestions ] = useState < TopicSuggestion [ ] > ( [ ] ) ;
24+ const [ showSuggestions , setShowSuggestions ] = useState ( false ) ;
25+ const [ isLoadingSuggestions , setIsLoadingSuggestions ] = useState ( false ) ;
1726
1827 useEffect ( ( ) => {
1928 if ( error )
@@ -23,21 +32,65 @@ const HomeView: FC = () => {
2332 } ) ;
2433 } , [ error , notify ] ) ;
2534
35+ // Debounced function to fetch suggestions
36+ const fetchSuggestions = useCallback (
37+ debounce ( async ( query : string ) => {
38+ if ( ! query . trim ( ) ) {
39+ setSuggestions ( [ ] ) ;
40+ return ;
41+ }
42+
43+ setIsLoadingSuggestions ( true ) ;
44+ try {
45+ const response = await fetch ( `${ API_ENDPOINTS . SUGGEST_TOPICS } ?query=${ encodeURIComponent ( query ) } ` ) ;
46+ const data = await response . json ( ) ;
47+ if ( data . success ) {
48+ setSuggestions ( data . suggestions ) ;
49+ } else {
50+ throw new Error ( data . message || 'Failed to get suggestions' ) ;
51+ }
52+ } catch ( error ) {
53+ console . error ( 'Error fetching suggestions:' , error ) ;
54+ setSuggestions ( [ ] ) ;
55+ } finally {
56+ setIsLoadingSuggestions ( false ) ;
57+ }
58+ } , 300 ) ,
59+ [ ]
60+ ) ;
61+
62+ // Update suggestions when search term changes
63+ useEffect ( ( ) => {
64+ fetchSuggestions ( searchTerm ) ;
65+ } , [ searchTerm , fetchSuggestions ] ) ;
66+
2667 // Function to handle search submission
2768 const handleSearch = ( e ?: React . FormEvent ) => {
2869 if ( e ) e . preventDefault ( ) ;
70+ setShowSuggestions ( false ) ;
2971
3072 if ( searchTerm . trim ( ) ) {
31- // Navigate to frequency page with both search term and topic
3273 navigate ( '/topics' , {
3374 state : {
3475 searchTerm : searchTerm . trim ( ) ,
35- userTopic : searchTerm . trim ( ) // Add the user topic
76+ userTopic : searchTerm . trim ( )
3677 }
3778 } ) ;
3879 }
3980 } ;
4081
82+ // Function to handle suggestion click
83+ const handleSuggestionClick = ( suggestion : TopicSuggestion ) => {
84+ setSearchTerm ( suggestion . name ) ;
85+ setShowSuggestions ( false ) ;
86+ navigate ( '/topics' , {
87+ state : {
88+ searchTerm : suggestion . name ,
89+ userTopic : suggestion . name
90+ }
91+ } ) ;
92+ } ;
93+
4194 return (
4295 < main className = "home-view d-flex flex-column justify-content-center" style = { { padding : "0 2rem" , minHeight : "100vh" , paddingTop : "10vh" } } >
4396 < div className = "title-block text-center" >
@@ -59,17 +112,18 @@ const HomeView: FC = () => {
59112 Discover and explore research software using large scale graphs
60113 </ h2 >
61114
62- { /* Wrap the search bar in a form to handle Enter key submission */ }
63- < form onSubmit = { handleSearch } className = "search-bar mb-4 d-flex justify-content-center" >
115+ { /* Search form with suggestions */ }
116+ < form onSubmit = { handleSearch } className = "search-bar mb-4 d-flex justify-content-center position-relative " >
64117 < div
65118 className = "input-group align-items-center"
66119 style = { {
67120 border : "1px solid #ddd" ,
68121 borderRadius : "20px" ,
69122 overflow : "hidden" ,
70- width : "800px" , // Fixed width
123+ width : "800px" ,
71124 boxShadow : "0 1px 3px rgba(0, 0, 0, 0.1)" ,
72- backgroundColor : "#fff" , // Unified background
125+ backgroundColor : "#fff" ,
126+ position : "relative" ,
73127 } }
74128 >
75129 < span
@@ -78,7 +132,7 @@ const HomeView: FC = () => {
78132 padding : "0.75rem" ,
79133 fontSize : "1rem" ,
80134 color : "#6c757d" ,
81- backgroundColor : "transparent" , // Transparent icon background
135+ backgroundColor : "transparent" ,
82136 } }
83137 >
84138 < FaSearch />
@@ -91,10 +145,16 @@ const HomeView: FC = () => {
91145 boxShadow : "none" ,
92146 fontSize : "1rem" ,
93147 padding : "0.75rem" ,
94- backgroundColor : "transparent" , // Transparent input background
148+ backgroundColor : "transparent" ,
95149 } }
96150 value = { searchTerm }
97- onChange = { ( e ) => setSearchTerm ( e . target . value . replace ( / \s + / g, '-' ) ) }
151+ onChange = { ( e ) => {
152+ const value = e . target . value . replace ( / \s + / g, '-' ) ;
153+ setSearchTerm ( value ) ;
154+ setShowSuggestions ( true ) ;
155+ } }
156+ onFocus = { ( ) => setShowSuggestions ( true ) }
157+ onBlur = { ( ) => setTimeout ( ( ) => setShowSuggestions ( false ) , 200 ) }
98158 />
99159 < button
100160 type = "submit"
@@ -113,6 +173,87 @@ const HomeView: FC = () => {
113173 < FaArrowRight />
114174 </ button >
115175 </ div >
176+
177+ { /* Suggestions dropdown */ }
178+ { showSuggestions && ( searchTerm . trim ( ) || isLoadingSuggestions ) && (
179+ < div
180+ className = "position-absolute bg-white rounded-3 shadow-lg"
181+ style = { {
182+ top : "calc(100% + 8px)" ,
183+ left : "50%" ,
184+ transform : "translateX(-50%)" ,
185+ width : "700px" ,
186+ maxHeight : "300px" ,
187+ overflowY : "auto" ,
188+ zIndex : 1000 ,
189+ border : "1px solid rgba(0,0,0,0.08)" ,
190+ backdropFilter : "blur(8px)" ,
191+ backgroundColor : "rgba(255, 255, 255, 0.98)" ,
192+ } }
193+ >
194+ { isLoadingSuggestions ? (
195+ < div className = "p-4 text-center" >
196+ < div className = "spinner-border spinner-border-sm text-primary me-2" role = "status" >
197+ < span className = "visually-hidden" > Loading...</ span >
198+ </ div >
199+ < span className = "text-muted" > Finding relevant topics...</ span >
200+ </ div >
201+ ) : suggestions . length > 0 ? (
202+ < div className = "list-group list-group-flush" >
203+ { suggestions . map ( ( suggestion , index ) => (
204+ < button
205+ key = { suggestion . name }
206+ className = "list-group-item list-group-item-action d-flex justify-content-between align-items-center py-3 px-4"
207+ onClick = { ( ) => handleSuggestionClick ( suggestion ) }
208+ style = { {
209+ border : "none" ,
210+ cursor : "pointer" ,
211+ transition : "all 0.2s ease" ,
212+ backgroundColor : "transparent" ,
213+ borderBottom : index !== suggestions . length - 1 ? "1px solid rgba(0,0,0,0.05)" : "none" ,
214+ } }
215+ onMouseEnter = { ( e ) => {
216+ e . currentTarget . style . backgroundColor = "rgba(13, 110, 253, 0.05)" ;
217+ } }
218+ onMouseLeave = { ( e ) => {
219+ e . currentTarget . style . backgroundColor = "transparent" ;
220+ } }
221+ >
222+ < div className = "d-flex align-items-center" >
223+ < span className = "me-2" style = { { fontSize : "1.1rem" } } > { suggestion . name } </ span >
224+ { index === 0 && suggestion . name . toLowerCase ( ) === searchTerm . toLowerCase ( ) && (
225+ < span className = "badge bg-success rounded-pill" style = { { fontSize : "0.7rem" } } >
226+ Exact match
227+ </ span >
228+ ) }
229+ </ div >
230+ < div className = "d-flex align-items-center" >
231+ < span
232+ className = "badge rounded-pill px-3 py-2"
233+ style = { {
234+ backgroundColor : "rgba(108, 117, 125, 0.1)" ,
235+ color : "#495057" ,
236+ fontSize : "0.9rem" ,
237+ fontWeight : "500"
238+ } }
239+ >
240+ { suggestion . count . toLocaleString ( ) } repos
241+ </ span >
242+ </ div >
243+ </ button >
244+ ) ) }
245+ </ div >
246+ ) : (
247+ < div className = "p-4 text-center" >
248+ < div className = "text-muted mb-2" >
249+ < i className = "fas fa-search me-2" > </ i >
250+ No matching topics found
251+ </ div >
252+ < small className = "text-muted" > Try a different search term</ small >
253+ </ div >
254+ ) }
255+ </ div >
256+ ) }
116257 </ form >
117258
118259 < div className = "tags d-flex flex-wrap justify-content-center mb-4" style = { { maxWidth : "600px" , margin : "0 auto" } } >
0 commit comments