Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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<string | null>(null);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [uploadMsg, setUploadMsg] = useState<string | null>(null);
Expand Down Expand Up @@ -145,15 +166,31 @@ const CourseExams = ({ courseId, onBack, darkMode }: Props) => {
if (error) return <div className="text-center p-4 text-red-600">Failed to load exams.</div>;

return (
<div className="grid grid-cols-fill-minmax-300 gap-8 relative z-10 sm:grid-cols-1 sm:gap-5">
<button
<div>
<div className="flex items-center gap-3 mb-4">
<button
className={`px-6 py-3 rounded-xl font-semibold shadow mb-4 w-fit ${darkMode ? "bg-gray-700 text-white" : "bg-gradient-to-r from-gray-700 to-gray-800 text-white"}`}
onClick={onBack}
>
← Back to Courses
</button>

{exams && exams.map((exam) => {
<input
placeholder="Search exams or batch..."
value={search}
onChange={e => 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'}`}
/>

<select className="ml-auto px-3 py-2 rounded-lg border" value={statusFilter} onChange={e => setStatusFilter(e.target.value as any)}>
<option value="all">All</option>
<option value="upcoming">Upcoming</option>
<option value="ongoing">Ongoing</option>
<option value="ended">Ended</option>
</select>
</div>

{filteredExams && filteredExams.map((exam) => {
const isStarted = new Date(exam.startTime) <= now;
const isEnded = new Date(exam.endTime) < now;
const canSubmit = isStarted && !isEnded;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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%)';
Expand Down Expand Up @@ -61,8 +78,18 @@ const CourseList = ({ onSelectCourse, darkMode }: Props) => {
if (error) return <div className={`text-center p-4 ${darkMode ? "text-red-400" : "text-red-600"}`}>Error loading courses.</div>;

return (
<div className="grid grid-cols-fill-minmax-300 gap-8 relative z-10 sm:grid-cols-1 sm:gap-5">
{courses && courses.map((course) => (
<div>
<div className="mb-4">
<input
placeholder="Search courses by name or code..."
value={search}
onChange={e => 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'}`}
/>
</div>

<div className="grid grid-cols-fill-minmax-300 gap-8 relative z-10 sm:grid-cols-1 sm:gap-5">
{filtered && filtered.map((course) => (
<div key={course._id} className={commonCardClasses}>
<div className={commonCardBeforeClasses} style={{ background: cardBeforeGradient }}></div>
<h3 className="mb-4 text-xl font-bold tracking-tight">{course.name}</h3>
Expand All @@ -78,6 +105,7 @@ const CourseList = ({ onSelectCourse, darkMode }: Props) => {
</div>
))}
</div>
</div>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ const palette = {

const TeacherCourses = () => {
const [courses, setCourses] = useState<Course[] | null>(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("");
Expand Down Expand Up @@ -176,6 +188,9 @@ const TeacherCourses = () => {
Courses and Batches
</h2>
<div className="w-full max-w-5xl">
<div className="mb-4">
<input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search courses or codes..." className="w-full px-3 py-2 rounded border" />
</div>
{courses === null ? (
<p className="text-center text-lg font-medium" style={{ color: palette['text-muted'] }}>Loading courses...</p>
) : courses.length === 0 ? (
Expand All @@ -196,7 +211,7 @@ const TeacherCourses = () => {
</tr>
</thead>
<tbody>
{courses.flatMap((course) =>
{filteredCourses.flatMap((course) =>
course.batches.map((batch) => (
<tr key={course._id + batch._id}
style={{
Expand Down