1- import React , { useState , useEffect , useContext } from 'react' ;
1+ import React , { useState , useEffect , useContext , useRef , useCallback } from 'react' ;
22import styled from 'styled-components' ;
33import { apiAutocomplete , apiReviewData , apiCourseSearch } from '../utils/api' ;
44import { useHistory } from 'react-router-dom' ;
@@ -20,7 +20,9 @@ const Container = styled.div`
2020 gap: 12px;
2121 border-radius: 12px;
2222 background: #FFF;
23- height: fit-content;
23+ flex: 1;
24+ min-height: 0;
25+ overflow: hidden;
2426` ;
2527
2628const BrowsingTitle = styled . span `
@@ -127,6 +129,20 @@ const SpecialPromptContainer = styled.div`
127129 padding: 40px 0;
128130` ;
129131
132+ const SemesterBanner = styled . div `
133+ display: flex;
134+ align-items: center;
135+ gap: 8px;
136+ width: 100%;
137+ padding: 10px 14px;
138+ border-radius: 8px;
139+ background: #EBF5FF;
140+ border: 1px solid #B3D7F5;
141+ color: #1A6FAF;
142+ font-size: 14px;
143+ font-weight: 400;
144+ ` ;
145+
130146const numFiltersChanged = ( filters ) => {
131147 let count = 0 ;
132148 for ( const [ key , value ] of Object . entries ( filters ) ) {
@@ -144,20 +160,44 @@ const numFiltersChanged = (filters) => {
144160 return count ;
145161}
146162
163+ const SEMESTER_SPECIFIC_FILTERS = [ 'instructor_quality' , 'days' , 'time' ] ;
164+
165+ const getActiveSemesterFilters = ( filters ) => {
166+ return SEMESTER_SPECIFIC_FILTERS . filter ( key => ! isDefault ( key , filters [ key ] ) ) ;
167+ } ;
168+
169+ const SEMESTER_FILTER_LABELS = {
170+ instructor_quality : 'Instructor Quality' ,
171+ days : 'Days Offered' ,
172+ time : 'Time Offered' ,
173+ } ;
174+
175+ const isDefault = ( key , value ) => {
176+ const def = DEFAULT_FILTERS [ key ] ;
177+ if ( Array . isArray ( value ) ) {
178+ return value . length === def . length && value . every ( v => def . includes ( v ) ) ;
179+ }
180+ return value === def ;
181+ } ;
182+
147183const formatFiltersForAPI = ( filters ) => {
148184 const formatted = { } ;
149185 for ( const [ key , value ] of Object . entries ( filters ) ) {
150- if ( key === 'difficulty' || key === 'course_quality' || key === 'instructor_quality' ) {
186+ if ( key === 'semester' ) {
187+ formatted [ key ] = value === 'Any' ? "all" : "current" ;
188+ } else if ( isDefault ( key , value ) ) {
189+ continue ;
190+ } else if ( key === 'difficulty' || key === 'course_quality' || key === 'instructor_quality' ) {
151191 formatted [ key ] = `${ value [ 0 ] } -${ value [ 1 ] } ` ;
152192 } else if ( key === 'days' ) {
153193 formatted [ key ] = value . join ( '' ) ;
154194 } else if ( key === 'time' ) {
155195 formatted [ key ] = value ;
156- } else if ( key === 'semester' ) {
157- formatted [ key ] = value === 'Any' ? "all" : "current" ;
158196 } else if ( key === 'attributes' ) {
159197 formatted [ key ] = value . join ( '|' ) ;
160- } //ALSO ADD DEPARTMENT WHEN IT BECOMES SUPPORTED
198+ } else if ( key === 'departments' ) {
199+ formatted [ key ] = value . join ( '|' ) ;
200+ }
161201 }
162202 return formatted ;
163203}
@@ -168,7 +208,13 @@ const CourseResults = ({ filters, setFilters }) => {
168208 const [ departments , setDepartments ] = useState ( [ ] ) ;
169209 const [ filteredResults , setFilteredResults ] = useState ( numFiltersChanged ( filters ) > 0 ? { } : null ) ;
170210 const [ isLoading , setIsLoading ] = useState ( false ) ;
211+ const [ isLoadingMore , setIsLoadingMore ] = useState ( false ) ;
171212 const [ isAverage , setIsAverage ] = useState ( true ) ;
213+ const [ totalCount , setTotalCount ] = useState ( 0 ) ;
214+ const [ hasMore , setHasMore ] = useState ( false ) ;
215+ const nextPageRef = useRef ( 2 ) ;
216+ const sentinelRef = useRef ( null ) ;
217+ const isLoadingRef = useRef ( false ) ;
172218
173219 const [ recencyOption , setRecencyOption ] = useState ( 'Average Rating' ) ;
174220
@@ -184,48 +230,75 @@ const CourseResults = ({ filters, setFilters }) => {
184230 } ) ;
185231 } , [ ] ) ;
186232
187- useEffect ( ( ) => {
188- let isActivelyFiltering = false ;
189- isActivelyFiltering = numFiltersChanged ( filters ) > 0 ;
190- console . log ( 'Filters changed:' , filters , 'Actively filtering:' , isActivelyFiltering ) ;
191-
192- if ( isActivelyFiltering ) {
193- //TEMPORARY: replace with actual backend route for filtering based on all filters, not just department
233+ const fetchCourses = useCallback ( ( filters , page , append = false ) => {
234+ if ( isLoadingRef . current ) return ;
235+ isLoadingRef . current = true ;
236+ if ( append ) {
237+ setIsLoadingMore ( true ) ;
238+ } else {
194239 setIsLoading ( true ) ;
195- console . log ( 'loading true' )
196- // apiReviewData('department', filters.departments[0], '')
197- // .then(data => {
198- // console.log("Fetched review data for filtering:", data);
199- // setFilteredResults(data);
200- // })
201- // .catch(error => {
202- // console.error("Error fetching review data:", error);
203- // setFilteredResults({});
204- // })
205- // .finally(() => {
206- // setIsLoading(false);
207- // console.log('loading false')
208- // });
209- const ff = formatFiltersForAPI ( filters ) ;
210- console . log ( 'Formatted filters for API:' , ff ) ;
211- apiCourseSearch ( ff . semester , ff . attributes , ff . difficulty , ff . course_quality , ff . days , ff . time )
212- . then ( data => {
213- console . log ( "Fetched course search data:" , data ) ;
214- setFilteredResults ( data ) ;
215- } )
216- . catch ( error => {
217- console . error ( "Error fetching course search data:" , error ) ;
240+ }
241+ const ff = formatFiltersForAPI ( filters ) ;
242+ apiCourseSearch ( ff . semester , ff . attributes , ff . difficulty , ff . course_quality , ff . instructor_quality , ff . days , ff . time , ff . departments , page )
243+ . then ( data => {
244+ const newResults = ( data . results || [ ] ) . reduce ( ( acc , course ) => {
245+ acc [ course . id ] = course ;
246+ return acc ;
247+ } , { } ) ;
248+ if ( append ) {
249+ setFilteredResults ( prev => ( { ...prev , ...newResults } ) ) ;
250+ } else {
251+ setFilteredResults ( newResults ) ;
252+ }
253+ setTotalCount ( data . count || 0 ) ;
254+ setHasMore ( data . next !== null ) ;
255+ nextPageRef . current = page + 1 ;
256+ } )
257+ . catch ( error => {
258+ console . error ( "Error fetching course search data:" , error ) ;
259+ if ( ! append ) {
218260 setFilteredResults ( { } ) ;
219- } )
220- . finally ( ( ) => {
221- setIsLoading ( false ) ;
222- console . log ( 'loading false' )
223- } ) ;
261+ setTotalCount ( 0 ) ;
262+ }
263+ setHasMore ( false ) ;
264+ } )
265+ . finally ( ( ) => {
266+ isLoadingRef . current = false ;
267+ setIsLoading ( false ) ;
268+ setIsLoadingMore ( false ) ;
269+ } ) ;
270+ } , [ ] ) ;
271+
272+ useEffect ( ( ) => {
273+ const isActivelyFiltering = numFiltersChanged ( filters ) > 0 ;
274+
275+ if ( isActivelyFiltering ) {
276+ nextPageRef . current = 2 ;
277+ fetchCourses ( filters , 1 ) ;
224278 } else {
225279 setFilteredResults ( null ) ;
280+ setTotalCount ( 0 ) ;
281+ setHasMore ( false ) ;
226282 }
283+ } , [ filters , fetchCourses ] ) ;
284+
285+ // Infinite scroll — sentinel is inside the table's scroll area
286+ useEffect ( ( ) => {
287+ const sentinel = sentinelRef . current ;
288+ if ( ! sentinel || ! hasMore ) return ;
289+
290+ const observer = new IntersectionObserver (
291+ ( entries ) => {
292+ if ( entries [ 0 ] . isIntersecting && ! isLoadingRef . current ) {
293+ fetchCourses ( filters , nextPageRef . current , true ) ;
294+ }
295+ } ,
296+ { threshold : 0.1 }
297+ ) ;
227298
228- } , [ filters ] ) ;
299+ observer . observe ( sentinel ) ;
300+ return ( ) => observer . disconnect ( ) ;
301+ } , [ hasMore , filters , fetchCourses ] ) ;
229302
230303 const history = useHistory ( ) ;
231304
@@ -247,7 +320,7 @@ const CourseResults = ({ filters, setFilters }) => {
247320 Object . entries ( filteredResults ) . length > 0 ? (
248321 < >
249322 < SearchResultsHeader >
250- < span > Showing < b > { Object . entries ( filteredResults ) . length } </ b > Search Results ({ numFiltersChanged ( filters ) } filter{ numFiltersChanged ( filters ) !== 1 ? "s" : "" } )</ span >
323+ < span > Showing < b > { Object . keys ( filteredResults ) . length } </ b > of < b > { totalCount } </ b > Search Results ({ numFiltersChanged ( filters ) } filter{ numFiltersChanged ( filters ) !== 1 ? "s" : "" } )</ span >
251324 < CustomDropdown
252325 style = { { width : '180px' , selfAlign : 'center' } }
253326 options = { [ 'Average Rating' , 'Most Recent Rating' ] }
@@ -258,10 +331,20 @@ const CourseResults = ({ filters, setFilters }) => {
258331 } }
259332 />
260333 </ SearchResultsHeader >
261- < div style = { { width : '100%' , marginBottom : '16px' , height : 'fit-content' } } >
334+ { getActiveSemesterFilters ( filters ) . length > 0 && (
335+ < SemesterBanner >
336+ < i className = "fa fa-info-circle" />
337+ < span >
338+ Filtering by { getActiveSemesterFilters ( filters ) . map ( k => SEMESTER_FILTER_LABELS [ k ] ) . join ( ', ' ) } — results limited to current semester offerings.
339+ </ span >
340+ </ SemesterBanner >
341+ ) }
342+ < div style = { { width : '100%' } } >
262343 < CourseResultsTable
263344 filteredResults = { filteredResults }
264345 isAverage = { isAverage }
346+ sentinelRef = { sentinelRef }
347+ isLoadingMore = { isLoadingMore }
265348 />
266349 </ div >
267350 </ >
0 commit comments