diff --git a/Peer_Evaluation_V3_NPTEL/frontend/src/components/student/CourseExams.tsx b/Peer_Evaluation_V3_NPTEL/frontend/src/components/student/CourseExams.tsx index 76f7bc1e..98251c4d 100644 --- a/Peer_Evaluation_V3_NPTEL/frontend/src/components/student/CourseExams.tsx +++ b/Peer_Evaluation_V3_NPTEL/frontend/src/components/student/CourseExams.tsx @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; -import { useState, useRef, useEffect } from 'react'; +import { useState, useRef, useEffect, useMemo } from 'react'; const PORT = import.meta.env.VITE_BACKEND_PORT || 5000; @@ -58,6 +58,27 @@ const CourseExams = ({ courseId, onBack, darkMode }: Props) => { queryFn: () => fetchExams(courseId), }); + const [search, setSearch] = useState(''); + const [debouncedSearch, setDebouncedSearch] = useState(''); + const [statusFilter, setStatusFilter] = useState<'all'|'upcoming'|'ongoing'|'ended'>('all'); + + useEffect(() => { + const t = setTimeout(() => setDebouncedSearch(search.trim().toLowerCase()), 250); + return () => clearTimeout(t); + }, [search]); + + const filteredExams = useMemo(() => { + if (!exams) return []; + return exams.filter(exam => { + const matchesSearch = !debouncedSearch || (exam.title && exam.title.toLowerCase().includes(debouncedSearch)) || (exam.batch?.name && exam.batch.name.toLowerCase().includes(debouncedSearch)); + const now = new Date(); + const isStarted = new Date(exam.startTime) <= now; + const isEnded = new Date(exam.endTime) < now; + const matchesStatus = statusFilter === 'all' || (statusFilter === 'upcoming' && !isStarted) || (statusFilter === 'ongoing' && isStarted && !isEnded) || (statusFilter === 'ended' && isEnded); + return matchesSearch && matchesStatus; + }); + }, [exams, debouncedSearch, statusFilter]); + const [activeExamId, setActiveExamId] = useState(null); const [selectedFile, setSelectedFile] = useState(null); const [uploadMsg, setUploadMsg] = useState(null); @@ -145,15 +166,31 @@ const CourseExams = ({ courseId, onBack, darkMode }: Props) => { if (error) return
Failed to load exams.
; return ( -
- - {exams && exams.map((exam) => { + setSearch(e.target.value)} + className={`ml-4 px-3 py-2 rounded-lg border ${darkMode ? 'bg-gray-800 text-white border-gray-600' : 'bg-white'}`} + /> + + +
+ + {filteredExams && filteredExams.map((exam) => { const isStarted = new Date(exam.startTime) <= now; const isEnded = new Date(exam.endTime) < now; const canSubmit = isStarted && !isEnded; diff --git a/Peer_Evaluation_V3_NPTEL/frontend/src/components/student/CourseList.tsx b/Peer_Evaluation_V3_NPTEL/frontend/src/components/student/CourseList.tsx index 396d8cdb..8ff4edcd 100644 --- a/Peer_Evaluation_V3_NPTEL/frontend/src/components/student/CourseList.tsx +++ b/Peer_Evaluation_V3_NPTEL/frontend/src/components/student/CourseList.tsx @@ -1,5 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; +import { useState, useEffect, useMemo } from 'react'; const PORT = import.meta.env.VITE_BACKEND_PORT || 5000; @@ -27,6 +28,22 @@ const CourseList = ({ onSelectCourse, darkMode }: Props) => { queryFn: fetchCourses, }); + const [search, setSearch] = useState(''); + const [debouncedSearch, setDebouncedSearch] = useState(''); + + useEffect(() => { + const t = setTimeout(() => setDebouncedSearch(search.trim().toLowerCase()), 250); + return () => clearTimeout(t); + }, [search]); + + const filtered = useMemo(() => { + if (!courses) return []; + if (!debouncedSearch) return courses; + return courses.filter(c => + c.name.toLowerCase().includes(debouncedSearch) || c.code.toLowerCase().includes(debouncedSearch) + ); + }, [courses, debouncedSearch]); + const cardShadow = `0 4px 6px rgba(0, 0, 0, 0.05), 0 10px 25px rgba(0, 0, 0, 0.08)`; const cardHoverShadow = `0 10px 20px rgba(0, 0, 0, 0.1), 0 20px 40px rgba(0, 0, 0, 0.12)`; const cardBeforeGradient = 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)'; @@ -61,8 +78,18 @@ const CourseList = ({ onSelectCourse, darkMode }: Props) => { if (error) return
Error loading courses.
; return ( -
- {courses && courses.map((course) => ( +
+
+ setSearch(e.target.value)} + className={`w-full px-4 py-2 rounded-lg border ${darkMode ? 'bg-gray-800 text-white border-gray-600' : 'bg-white'}`} + /> +
+ +
+ {filtered && filtered.map((course) => (

{course.name}

@@ -78,6 +105,7 @@ const CourseList = ({ onSelectCourse, darkMode }: Props) => {
))}
+
); }; diff --git a/Peer_Evaluation_V3_NPTEL/frontend/src/components/teacher/TeacherCourses.tsx b/Peer_Evaluation_V3_NPTEL/frontend/src/components/teacher/TeacherCourses.tsx index cda26b2c..d1adf082 100644 --- a/Peer_Evaluation_V3_NPTEL/frontend/src/components/teacher/TeacherCourses.tsx +++ b/Peer_Evaluation_V3_NPTEL/frontend/src/components/teacher/TeacherCourses.tsx @@ -30,6 +30,18 @@ const palette = { const TeacherCourses = () => { const [courses, setCourses] = useState(null); + const [search, setSearch] = useState(''); + const [debouncedSearch, setDebouncedSearch] = useState(''); + + useEffect(() => { + const t = setTimeout(() => setDebouncedSearch(search.trim().toLowerCase()), 250); + return () => clearTimeout(t); + }, [search]); + + const filteredCourses = (courses || []).filter(c => { + if (!debouncedSearch) return true; + return c.name.toLowerCase().includes(debouncedSearch) || c.code.toLowerCase().includes(debouncedSearch); + }); const [showEnrollModal, setShowEnrollModal] = useState(false); const [enrollCourse, setEnrollCourse] = useState(""); const [enrollBatch, setEnrollBatch] = useState(""); @@ -176,6 +188,9 @@ const TeacherCourses = () => { Courses and Batches
+
+ setSearch(e.target.value)} placeholder="Search courses or codes..." className="w-full px-3 py-2 rounded border" /> +
{courses === null ? (

Loading courses...

) : courses.length === 0 ? ( @@ -196,7 +211,7 @@ const TeacherCourses = () => { - {courses.flatMap((course) => + {filteredCourses.flatMap((course) => course.batches.map((batch) => (