From 3deea90a9dc48f0dfd94e9b1fe63bcd4afb5d82f Mon Sep 17 00:00:00 2001 From: alSN0W Date: Sun, 8 Mar 2026 00:10:58 +0530 Subject: [PATCH] fixed searchbar bug and implemented instant search results --- src/components/courses/Filters.tsx | 305 +++++++++++++++-------------- 1 file changed, 163 insertions(+), 142 deletions(-) diff --git a/src/components/courses/Filters.tsx b/src/components/courses/Filters.tsx index 239389c..a593f81 100644 --- a/src/components/courses/Filters.tsx +++ b/src/components/courses/Filters.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import { Accordion, AccordionContent, @@ -36,8 +36,8 @@ const difficultyLevels: DifficultyLevel[] = [ interface FilterProps { type: "course" | "professor"; - currentFilters: FiltersState; // Receive current filters - onFilterChange: (filters: FiltersState) => void; // Callback to update parent state + currentFilters: FiltersState; + onFilterChange: (filters: FiltersState) => void; } export default function Filters({ @@ -65,11 +65,41 @@ export default function Filters({ const [isSticky, setIsSticky] = useState(false); const filterRef = useRef(null); const [filterTop, setFilterTop] = useState(0); + + // Ref to store the debounce timeout for search + const debouncedSearchRef = useRef(); const handleChange = (newValue: string) => { setValue(value === newValue ? undefined : newValue); }; + // Debounced search handler + const handleSearchChange = useCallback((value: string) => { + setLocalSearchQuery(value); // Update UI immediately + + // Clear previous timeout + if (debouncedSearchRef.current) { + clearTimeout(debouncedSearchRef.current); + } + + // Set new timeout to update parent after 300ms + debouncedSearchRef.current = setTimeout(() => { + onFilterChange({ + ...currentFilters, + searchQuery: value + }); + }, 300); + }, [currentFilters, onFilterChange]); + + // Cleanup timeout on unmount + useEffect(() => { + return () => { + if (debouncedSearchRef.current) { + clearTimeout(debouncedSearchRef.current); + } + }; + }, []); + // Update local state if external filters change (e.g., clear all) useEffect(() => { setLocalSearchQuery(currentFilters.searchQuery); @@ -85,17 +115,14 @@ export default function Filters({ const rect = filterRef.current.getBoundingClientRect(); const scrollY = window.scrollY; - // Get the initial offset if not set if (filterTop === 0 && !isSticky) { setFilterTop(scrollY + rect.top); } - // Check if we've scrolled past the filter's initial position - setIsSticky(scrollY > filterTop - 80); // 80px = top-20 + setIsSticky(scrollY > filterTop - 80); } }; - // Calculate initial position if (filterRef.current && filterTop === 0) { const rect = filterRef.current.getBoundingClientRect(); setFilterTop(window.scrollY + rect.top); @@ -110,8 +137,6 @@ export default function Filters({ if (prev.includes(deptId)) { return prev.filter((id) => id !== deptId); } else { - // *** THIS IS THE FIX *** - // Add new departments to the FRONT of the array, not the back. return [deptId, ...prev]; } }); @@ -122,26 +147,29 @@ export default function Filters({ if (prev.includes(diffValue)) { return prev.filter((val) => val !== diffValue); } else { - // Also add new difficulties to the front return [diffValue, ...prev]; } }); }; const clearAllFilters = (): void => { - // Reset local state first + // Clear debounce timeout if active + if (debouncedSearchRef.current) { + clearTimeout(debouncedSearchRef.current); + } + setLocalSearchQuery(""); setLocalSelectedDepartments([]); setLocalSelectedDifficulties([]); - setLocalRatingFilter([1]); // Reset to minimum possible rating filter - // Then notify parent to clear global state + setLocalRatingFilter([1]); + onFilterChange({ searchQuery: "", departments: [], difficulties: [], - rating: 1, // Match the reset value + rating: 1, }); - setIsMobileFilterOpen(false); // Close mobile filter if open + setIsMobileFilterOpen(false); }; const removeDepartmentFilter = (deptId: string): void => { @@ -159,23 +187,22 @@ export default function Filters({ }; const applyFilters = (): void => { - // Call the callback function passed from the parent with the updated local state onFilterChange({ searchQuery: localSearchQuery, departments: localSelectedDepartments, difficulties: localSelectedDifficulties, rating: localRatingFilter[0], }); - setIsMobileFilterOpen(false); // Close mobile filter view after applying + setIsMobileFilterOpen(false); }; - // Calculate active filters count based on the *parent's* state + // Calculate active filters count based on the parent's state useEffect(() => { let count = 0; if (currentFilters.searchQuery) count++; count += currentFilters.departments.length; count += currentFilters.difficulties.length; - if (currentFilters.rating !== 1) count++; // Assuming 1 is the default/inactive state + if (currentFilters.rating !== 1) count++; setActiveFiltersCount(count); }, [currentFilters]); @@ -185,49 +212,9 @@ export default function Filters({ return departmentProperties.find((dept) => dept.id === deptId); }; - // Mobile Filters component - const MobileFilters = (): JSX.Element => ( -
-
-

Filters

- -
-
- -
-
- - -
-
- ); - - // FiltersContent component remains largely the same but uses local state/handlers - const FiltersContent = (): JSX.Element => ( + // Plain JSX variable — NOT a component — so React never remounts it on re-render, + // which keeps the search input focused between keystrokes. + const filtersContent = (
@@ -254,7 +241,6 @@ export default function Filters({ Department - {/* Department list already had overflow handling */}
{departmentProperties.map((department) => { const DeptIcon = department.icon; @@ -273,7 +259,7 @@ export default function Filters({ className="h-4 w-4" style={{ color: department.color }} /> - {department.name} ({department.id}) {/* Show name and ID */} + {department.name} ({department.id})
); @@ -282,7 +268,6 @@ export default function Filters({
- {/* Difficulty section can be conditionally rendered based on type */} {type === "course" && ( @@ -343,87 +328,123 @@ export default function Filters({
); - // ActiveFilters component now uses parent state (`currentFilters`) and calls parent update function - const ActiveFilters = (): JSX.Element | null => { - if (activeFiltersCount === 0) return null; - - return ( -
-
-

Active Filters

- -
- {/* The active filter badges will now appear in the order of selection */} -
- {currentFilters.searchQuery && ( - - Search: {currentFilters.searchQuery} + // Plain JSX variable — NOT a component — for the same reason as filtersContent above. + const activeFilters = activeFiltersCount === 0 ? null : ( +
+
+

Active Filters

+ +
+
+ {currentFilters.searchQuery && ( + + Search: {currentFilters.searchQuery} + + onFilterChange({ ...currentFilters, searchQuery: "" }) + } + /> + + )} + + {currentFilters.departments.map((deptId) => { + const dept = getDepartmentById(deptId); + if (!dept) return null; + + const DeptIcon = dept.icon; + return ( + + + {dept.id} - onFilterChange({ ...currentFilters, searchQuery: "" }) - } + onClick={() => removeDepartmentFilter(deptId)} /> - )} + ); + })} + + {currentFilters.difficulties.map((diff) => { + const diffLevel = difficultyLevels.find((d) => d.value === diff); + if (!diffLevel) return null; - {currentFilters.departments.map((deptId) => { - const dept = getDepartmentById(deptId); - if (!dept) return null; - - const DeptIcon = dept.icon; - return ( - - - {dept.id} - removeDepartmentFilter(deptId)} - /> - - ); - })} - - {currentFilters.difficulties.map((diff) => { - const diffLevel = difficultyLevels.find((d) => d.value === diff); - if (!diffLevel) return null; - - return ( - - {diffLevel.label} - removeDifficultyFilter(diff)} - /> - - ); - })} - - {currentFilters.rating !== 1 && ( - - Rating: {currentFilters.rating}+ stars + return ( + + {diffLevel.label} - onFilterChange({ ...currentFilters, rating: 1 }) - } // Reset rating to default + onClick={() => removeDifficultyFilter(diff)} /> - )} -
+ ); + })} + + {currentFilters.rating !== 1 && ( + + Rating: {currentFilters.rating}+ stars + + onFilterChange({ ...currentFilters, rating: 1 }) + } + /> + + )}
- ); - }; +
+ ); + + // Plain JSX variable — NOT a component — for the same reason as filtersContent above. + const mobileFilters = ( +
+
+

Filters

+ +
+
+ {filtersContent} +
+
+ + +
+
+ ); return ( <> @@ -445,15 +466,15 @@ export default function Filters({ )} - {isMobileFilterOpen && } - + {isMobileFilterOpen && mobileFilters} + {activeFilters}
- {/* Desktop Filters - Better positioning */} + {/* Desktop Filters */}

Filters

- + {filtersContent}
- + {activeFilters}