From 77c05718d662a3bab59bf50808ff340e9eb4f97f Mon Sep 17 00:00:00 2001 From: sunilkumar2170 Date: Thu, 22 Jan 2026 00:32:44 +0530 Subject: [PATCH] fix: professor filter by name and department --- src/components/courses/ItemList.tsx | 306 +++++++++++++--------------- 1 file changed, 139 insertions(+), 167 deletions(-) diff --git a/src/components/courses/ItemList.tsx b/src/components/courses/ItemList.tsx index 4f7fc5c..e46f756 100644 --- a/src/components/courses/ItemList.tsx +++ b/src/components/courses/ItemList.tsx @@ -19,81 +19,71 @@ import { Button } from "@/components/ui/button"; interface ItemListProps { type: "course" | "professor"; - filters: FiltersState; // Receive filters as props + filters: FiltersState; } -// Helper function to map difficulty string to numeric range +// Helper function to map difficulty label → numeric range const getDifficultyRange = (difficultyLabel: string): [number, number] => { switch (difficultyLabel) { - case 'beginner': return [0, 2]; // Corresponds to 'Easy' in badge (0-1.9) - case 'intermediate': return [2, 3]; // Corresponds to 'Moderate' in badge (2-2.9) - case 'advanced': return [3, 4]; // Corresponds to 'Challenging' in badge (3-3.9) - case 'expert': return [4, 5.1]; // Corresponds to 'Difficult' in badge (4-5) - use 5.1 to include 5 - default: return [0, 5.1]; // Default to all if unknown + case 'beginner': return [0, 2]; + case 'intermediate': return [2, 3]; + case 'advanced': return [3, 4]; + case 'expert': return [4, 5.1]; + default: return [0, 5.1]; } }; export default function ItemList({ type, filters }: ItemListProps) { - // Add sortBy state const [sortBy, setSortBy] = useState("best-rated"); - - // 1. Get data from hooks + + // ── Data ──────────────────────────────────────────────── const { courses, isLoading: isCoursesLoading, error: coursesError, - } = useCourses(); // This hook now returns courses WITH ratings + } = useCourses(); const { professors, isLoading: isProfessorsLoading, error: professorsError, - } = useProfessors(); // This hook only returns static professor data + } = useProfessors(); - // 2. This state will hold the final list of items WITH averages const [itemsWithAvg, setItemsWithAvg] = useState<(Course | Professor)[]>([]); - - // 3. Determine loading and error state const [isAggregating, setIsAggregating] = useState(false); + const isLoading = (type === "course" ? isCoursesLoading : isProfessorsLoading) || isAggregating; const error = type === "course" ? coursesError : professorsError; - // 4. Handle DATA population - // EFFECT 1: For 'course' type + // ── Populate itemsWithAvg ─────────────────────────────── useEffect(() => { - if (type === 'course') { + if (type === "course") { setItemsWithAvg(courses); } - }, [type, courses]); // Run when courses array changes + }, [type, courses]); - // EFFECT 2: For 'professor' type useEffect(() => { - if (type === 'professor' && professors.length > 0) { + if (type !== "professor" || professors.length === 0) return; + + const fetchProfessorAverages = async () => { setIsAggregating(true); - - const fetchAverages = async () => { - const ids = professors.map((i) => i.id); - if (ids.length === 0) { - setItemsWithAvg(professors); - setIsAggregating(false); - return; - } + const ids = professors.map((p) => p.id); - const { data: ratings, error } = await supabase - .from("ratings") - .select("target_id, overall_rating, workload_rating, difficulty_rating") - .eq("target_type", "professor") // Fetch ratings for professors - .in("target_id", ids); - - if (error) { - console.error("Error fetching professor averages:", error.message); - setItemsWithAvg(professors); // fallback to static data - setIsAggregating(false); - return; - } + const { data: ratings, error } = await supabase + .from("ratings") + .select("target_id, overall_rating, workload_rating, difficulty_rating") + .eq("target_type", "professor") + .in("target_id", ids); + + if (error) { + console.error("Failed to load professor ratings:", error); + setItemsWithAvg(professors); // fallback + setIsAggregating(false); + return; + } - // Aggregate averages - const averages = (ratings || []).reduce((acc, r) => { + const averages = (ratings || []).reduce( + (acc, r) => { if (!acc[r.target_id]) { acc[r.target_id] = { overall: [], workload: [], difficulty: [] }; } @@ -101,152 +91,135 @@ export default function ItemList({ type, filters }: ItemListProps) { if (r.workload_rating !== null) acc[r.target_id].workload.push(r.workload_rating); if (r.difficulty_rating !== null) acc[r.target_id].difficulty.push(r.difficulty_rating); return acc; - }, {} as Record); - - // Merge averages with professors - const calculateAverage = (arr: number[]) => arr.length > 0 ? arr.reduce((a, b) => a + b, 0) / arr.length : 0; - - const merged = professors.map((prof) => { - const avgData = averages[prof.id]; - return { - ...prof, - overall_rating: calculateAverage(avgData?.overall || []), - workload_rating: calculateAverage(avgData?.workload || []), - difficulty_rating: calculateAverage(avgData?.difficulty || []), - }; - }); - - setItemsWithAvg(merged); - setIsAggregating(false); - }; + }, + {} as Record, + ); + + const calcAvg = (arr: number[]) => (arr.length > 0 ? arr.reduce((a, b) => a + b, 0) / arr.length : 0); + + const merged = professors.map((prof) => { + const avg = averages[prof.id]; + return { + ...prof, + overall_rating: calcAvg(avg?.overall || []), + workload_rating: calcAvg(avg?.workload || []), + difficulty_rating: calcAvg(avg?.difficulty || []), + }; + }); - fetchAverages(); - } else if (type === 'professor') { - // Handle case where professors array is empty - setItemsWithAvg(professors); - } - }, [type, professors]); // Run when professors array changes + setItemsWithAvg(merged); + setIsAggregating(false); + }; + fetchProfessorAverages(); + }, [type, professors]); - // 5. Apply filtering and sorting logic + // ── Filtering + Department-order sorting for multi-dept ── const filteredItems = useMemo(() => { - if (!itemsWithAvg) return []; - - const filtered = itemsWithAvg.filter((item) => { - // 1. Filter by Search Query + let result = itemsWithAvg.filter((item) => { + // Search const query = filters.searchQuery.toLowerCase().trim(); if (query) { - let match = false; - if (type === 'course') { - const course = item as Course; - match = course.title.toLowerCase().includes(query) || course.code.toLowerCase().includes(query); + if (type === "course") { + const c = item as Course; + if (!c.title.toLowerCase().includes(query) && !c.code.toLowerCase().includes(query)) { + return false; + } } else { - const professor = item as Professor; - match = professor.name.toLowerCase().includes(query); + const p = item as Professor; + if (!p.name.toLowerCase().includes(query)) return false; } - if (!match) return false; } - // 2. Filter by Department + // Department filter if (filters.departments.length > 0) { - const itemDeptId = departmentProperties.find(dp => dp.name === item.department)?.id; - if (!itemDeptId || !filters.departments.includes(itemDeptId)) { + const deptId = departmentProperties.find((d) => d.name === item.department)?.id; + if (!deptId || !filters.departments.includes(deptId)) { return false; } } - // 3. Filter by Difficulty (only for courses) - if (type === 'course' && filters.difficulties.length > 0) { + // Difficulty (courses only) + if (type === "course" && filters.difficulties.length > 0) { const course = item as Course; - const difficultyRating = course.difficulty_rating || 0; - let difficultyMatch = false; - for (const difficultyLabel of filters.difficulties) { - const [min, max] = getDifficultyRange(difficultyLabel); - if (difficultyRating >= min && difficultyRating < max) { - difficultyMatch = true; - break; - } + const diff = course.difficulty_rating ?? 0; + let matchesAny = false; + for (const label of filters.difficulties) { + const [min, max] = getDifficultyRange(label); + if (diff >= min && diff < max) { + matchesAny = true; + break; + } } - if (!difficultyMatch) return false; + if (!matchesAny) return false; } - // 4. Filter by Minimum Rating + // Minimum overall rating if (item.overall_rating > 0 && item.overall_rating < filters.rating) { return false; } - // If all checks pass, include the item return true; }); - // *** THIS IS THE FIX *** - // Sort by the click-order array from filters - if (type === 'course' && filters.departments.length > 1) { - // Use toSorted() to create a new array, ensuring stability - const sorted = filtered.toSorted((a, b) => { - // Map full department name (on item) to department ID (in filter array) - const deptIdA = departmentProperties.find(dp => dp.name === a.department)?.id; - const deptIdB = departmentProperties.find(dp => dp.name === b.department)?.id; - - if (!deptIdA || !deptIdB) return 0; + // Special case: when multiple departments are selected → sort by click order + if (type === "course" && filters.departments.length > 1) { + result = result.toSorted((a, b) => { + const idA = departmentProperties.find((d) => d.name === (a as Course).department)?.id; + const idB = departmentProperties.find((d) => d.name === (b as Course).department)?.id; - // Get the index from the filters.departments array (e.g., ['MA', 'EE']) - const indexA = filters.departments.indexOf(deptIdA); - const indexB = filters.departments.indexOf(deptIdB); + const idxA = idA ? filters.departments.indexOf(idA) : -1; + const idxB = idB ? filters.departments.indexOf(idB) : -1; - // Handle cases where department might not be found (shouldn't happen) - if (indexA === -1 && indexB === -1) return 0; - if (indexA === -1) return 1; - if (indexB === -1) return -1; - - // This sorts by the click-order index (e.g., 0 for 'MA', 1 for 'EE') - return indexA - indexB; + if (idxA === -1 && idxB === -1) return 0; + if (idxA === -1) return 1; + if (idxB === -1) return -1; + return idxA - idxB; }); - return sorted; } - // *** END OF FIX *** - - return filtered; // Return the unsorted (but filtered) list - }, [itemsWithAvg, filters, type]); // Depend on filters and the processed items + return result; + }, [itemsWithAvg, filters, type]); - // Group courses by department (preserve master department order) + // ── Grouping (only for courses) ───────────────────────── const groupedCourses = useMemo(() => { if (type !== "course") return null; - // prepare map with master order keys - const map = new Map(); + + const map = new Map(); + + // Initialize known departments for (const dp of departmentProperties) { map.set(dp.id, { id: dp.id, name: dp.name, items: [] }); } - // Put each filtered course into its department bucket (fallback to 'OTHER') + // Distribute courses (filteredItems as Course[]).forEach((course) => { const dp = departmentProperties.find((d) => d.name === course.department); - const id = dp?.id ?? "OTHER"; - if (!map.has(id)) { - map.set(id, { id, name: course.department ?? "Other", items: [] }); + const bucketId = dp?.id ?? "OTHER"; + if (!map.has(bucketId)) { + map.set(bucketId, { id: bucketId, name: course.department ?? "Other", items: [] }); } - map.get(id)!.items.push(course); + map.get(bucketId)!.items.push(course); }); - // Build ordered array of groups (master order first, then any others) + // Build final ordered list const ordered: { id: string; name: string; items: Course[] }[] = []; for (const dp of departmentProperties) { - const group = map.get(dp.id); - if (group && group.items.length > 0) ordered.push(group); + const g = map.get(dp.id); + if (g?.items.length) ordered.push(g); } - // add any groups not in master list (e.g. OTHER) - for (const [, group] of map) { - if (!departmentProperties.find((d) => d.id === group.id) && group.items.length > 0) { + // Add any unexpected departments at the end + for (const [id, group] of map) { + if (!departmentProperties.some((d) => d.id === id) && group.items.length > 0) { ordered.push(group); } } + return ordered; }, [filteredItems, type]); - - // 6. Render + + // ── Render ─────────────────────────────────────────────── if (isLoading) { - // Show skeleton loaders return (
@@ -255,15 +228,15 @@ export default function ItemList({ type, filters }: ItemListProps) {
{[...Array(8)].map((_, i) => ( -
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
))}
@@ -276,27 +249,27 @@ export default function ItemList({ type, filters }: ItemListProps) { const getSortLabel = () => { switch (sortBy) { - case "best-rated": return "Best Rated"; + case "best-rated": return "Best Rated"; case "most-reviewed": return "Most Reviewed"; - case "easiest": return "Easiest"; - case "hardest": return "Hardest"; - default: return "Best Rated"; + case "easiest": return "Easiest"; + case "hardest": return "Hardest"; + default: return "Best Rated"; } }; return (
+ {/* Header + Sort */}

Showing {filteredItems.length}{" "} {filteredItems.length === 1 ? type : `${type}s`}

- - {/* Modern Dropdown for Sorting */} + - - - setSortBy("best-rated")} className="font-bold text-sm cursor-pointer hover:bg-primary/10 transition-colors duration-200 rounded-md gap-2" > Best Rated - setSortBy("most-reviewed")} className="font-bold text-sm cursor-pointer hover:bg-primary/10 transition-colors duration-200 rounded-md gap-2" > Most Reviewed - {type === 'course' && ( + {type === "course" && ( <> - setSortBy("easiest")} className="font-bold text-sm cursor-pointer hover:bg-primary/10 transition-colors duration-200 rounded-md gap-2" > Easiest - setSortBy("hardest")} className="font-bold text-sm cursor-pointer hover:bg-primary/10 transition-colors duration-200 rounded-md gap-2" > @@ -345,12 +318,14 @@ export default function ItemList({ type, filters }: ItemListProps) {
+ {/* No results */} {filteredItems.length === 0 ? (

No {type}s found

Try adjusting your filters

) : type === "course" && groupedCourses && groupedCourses.length > 0 ? ( + // ── Grouped courses view ──
{groupedCourses.map((group) => (
@@ -363,20 +338,17 @@ export default function ItemList({ type, filters }: ItemListProps) {
{group.items.map((course) => ( - + ))}
))}
) : ( + // ── Flat list (professors + ungrouped courses) ──
- {filteredItems.map((item: Course | Professor) => ( - + {filteredItems.map((item) => ( + ))}
)}