Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Intergrate check course for Admin #168

Merged
merged 8 commits into from
Mar 13, 2025
Merged
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
14 changes: 14 additions & 0 deletions src/app/admins/check-courses/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client';

import CourseRequestTable from '@/components/admins/check-courses/CourseRequestTable';

export default function CheckCoursesPage() {
return (
<div className="px-8 py-10 ml-10">
<h1 className="text-2xl font-bold mb-6">Course Creation Requests</h1>
<div className="bg-white shadow rounded-lg">
<CourseRequestTable />
</div>
</div>
);
}
68 changes: 68 additions & 0 deletions src/components/admins/check-courses/CourseActionsDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { BsThreeDots } from 'react-icons/bs';
import { FaCheck, FaEye, FaTimes } from 'react-icons/fa';

import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';

interface CourseActionsDropdownProps {
readonly courseId: number;
readonly status: string;
readonly onStatusUpdate: (courseId: number, newStatus: string) => void;
readonly onViewDetails: (courseId: number) => void;
readonly position: 'top' | 'bottom';
}

export default function CourseActionsDropdown({
courseId,
status,
onStatusUpdate,
onViewDetails,
position,
}: CourseActionsDropdownProps) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="w-7 h-7 flex items-center justify-center rounded-full hover:bg-gray-100 transition-all duration-200 group">
<BsThreeDots className="w-4 h-4 text-gray-400 group-hover:text-gray-600" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side={position} className="w-36">
{status === 'pending' && (
<>
<DropdownMenuItem
onClick={() => onStatusUpdate(courseId, 'approved')}
className="flex items-center gap-2 group cursor-pointer"
>
<span className="w-6 h-6 rounded-full bg-green-50 flex items-center justify-center group-hover:bg-green-100 transition-all duration-200">
<FaCheck className="w-3 h-3 text-green-600" />
</span>
<span className="group-hover:text-green-600">Approve</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onStatusUpdate(courseId, 'rejected')}
className="flex items-center gap-2 group cursor-pointer"
>
<span className="w-6 h-6 rounded-full bg-red-50 flex items-center justify-center group-hover:bg-red-100 transition-all duration-200">
<FaTimes className="w-3 h-3 text-red-600" />
</span>
<span className="group-hover:text-red-600">Reject</span>
</DropdownMenuItem>
</>
)}
<DropdownMenuItem
onClick={() => onViewDetails(courseId)}
className="flex items-center gap-2 group cursor-pointer"
>
<span className="w-6 h-6 rounded-full bg-blue-50 flex items-center justify-center group-hover:bg-blue-100 transition-all duration-200">
<FaEye className="w-3 h-3 text-blue-600" />
</span>
<span className="group-hover:text-blue-600">Details</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
228 changes: 228 additions & 0 deletions src/components/admins/check-courses/CourseRequestTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import axios from 'axios';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { BsFilter } from 'react-icons/bs';

import PaginationCustom from '@/components/commons/PaginationCustom';
import { Course as ApiCourse } from '@/schemas/course.schema';
import { getCourses } from '@/services/api/course';
import { getUserClerk } from '@/services/api/user';

import CourseActionsDropdown from './CourseActionsDropdown';
import StatusBadge from './StatusBadge';

interface Teacher {
id: number;
firstName?: string;
lastName?: string;
name?: string;
}

interface Course extends ApiCourse {
teacher?: Teacher;
}

export default function CourseRequestTable() {
const router = useRouter();
const [currentPage, setCurrentPage] = useState(1);
const [courses, setCourses] = useState<Course[]>([]);
const [loading, setLoading] = useState(true);
const [statusFilter, setStatusFilter] = useState<string>('all');
const itemsPerPage = 5;

useEffect(() => {
const fetchData = async () => {
try {
const coursesData = await getCourses();

const coursesWithTeacherDetails = await Promise.all(
coursesData.map(async (course) => {
try {
if (course.teacherId) {
const teacherDetails = await getUserClerk(course.teacherId);
return {
...course,
teacher: {
id: course.teacherId,
firstName: teacherDetails.firstName,
lastName: teacherDetails.lastName,
name: `${teacherDetails.firstName} ${teacherDetails.lastName}`,
},
} as Course;
}
return course as Course;
} catch (error) {
console.error(
`Failed to fetch teacher details for course ${course.id}:`,
error,
);
return course as Course;
}
}),
);

setCourses(coursesWithTeacherDetails);
} catch (error) {
console.error('Failed to fetch courses:', error);
} finally {
setLoading(false);
}
};

fetchData();
}, []);

const filteredCourses = courses.filter((course) =>
statusFilter === 'all' ? true : course.status === statusFilter,
);

const totalPages = Math.ceil(filteredCourses.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const currentItems = filteredCourses.slice(
startIndex,
startIndex + itemsPerPage,
);

const handleStatusUpdate = async (courseId: number, newStatus: string) => {
try {
const courseToUpdate = courses.find((c) => c.id === courseId);
if (!courseToUpdate) return;

await axios.put(
`${process.env.NEXT_PUBLIC_API_URL}/courses/${courseId}`,
{ status: newStatus },
{
headers: {
'Content-Type': 'application/json',
},
},
);

setCourses((prevCourses) =>
prevCourses.map((course) =>
course.id === courseId ? { ...course, status: newStatus } : course,
),
);
} catch (error) {
console.error('Failed to update course status:', error);
}
};

const handleViewDetails = (courseId: number) => {
router.push(`/courses/${courseId}`);
};

const getDropdownPosition = (index: number) => {
if (index >= currentItems.length - 2) {
return 'bottom';
}
return 'top';
};

if (loading) {
return (
<div className="flex justify-center items-center py-8">Loading...</div>
);
}

return (
<div>
<div className="px-6 py-4 bg-white border-b border-gray-200">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<BsFilter className="w-5 h-5 text-gray-500" />
<span className="text-sm text-gray-600">Filter by status:</span>
</div>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="px-3 py-1.5 text-sm border border-gray-200 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">All Status</option>
<option value="pending">Pending</option>
<option value="approved">Approved</option>
<option value="rejected">Rejected</option>
</select>
</div>
</div>

<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase">
Teacher
</th>
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase">
Course Name
</th>
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase">
Description
</th>
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase">
Status
</th>
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase">
Created At
</th>
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white">
{currentItems.map((course, index) => (
<tr key={course.id} className="border-b border-gray-100">
<td className="px-6 py-4 text-sm text-gray-900">
{course.teacher?.firstName || course.teacher?.lastName
? [course.teacher.firstName, course.teacher.lastName]
.filter(Boolean)
.join(' ')
: (course.teacher?.name ?? 'Unknown Teacher')}
</td>
<td className="px-6 py-4 text-sm text-gray-900">
{course.title}
</td>
<td className="px-6 py-4 text-sm text-gray-500 max-w-[300px] truncate">
{course.description}
</td>
<td className="px-6 py-4">
<StatusBadge status={course.status} />
</td>
<td className="px-6 py-4 text-sm text-gray-500">
{course.createdAt
? new Date(course.createdAt).toLocaleDateString()
: '-'}
</td>
<td className="px-6 py-4">
<CourseActionsDropdown
courseId={course.id}
status={course.status}
onStatusUpdate={handleStatusUpdate}
onViewDetails={handleViewDetails}
position={getDropdownPosition(index)}
/>
</td>
</tr>
))}
</tbody>
</table>
</div>

<div className="flex items-center justify-between px-6 py-4 bg-white border-t border-gray-200">
<div className="text-sm text-gray-500">
Showing {startIndex + 1} to{' '}
{Math.min(startIndex + itemsPerPage, filteredCourses.length)} of{' '}
{filteredCourses.length} entries
</div>
<PaginationCustom
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
activeClassName="bg-blue-50 text-blue-600"
hoverClassName="hover:bg-gray-100"
/>
</div>
</div>
);
}
32 changes: 32 additions & 0 deletions src/components/admins/check-courses/StatusBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
interface StatusBadgeProps {
readonly status: string;
}

export default function StatusBadge({ status }: StatusBadgeProps) {
switch (status) {
case 'pending':
return (
<span className="px-3 py-1 text-xs bg-yellow-50 text-yellow-600 rounded">
Pending
</span>
);
case 'approved':
return (
<span className="px-3 py-1 text-xs bg-green-50 text-green-600 rounded">
Approved
</span>
);
case 'rejected':
return (
<span className="px-3 py-1 text-xs bg-red-50 text-red-600 rounded">
Rejected
</span>
);
default:
return (
<span className="px-3 py-1 text-xs bg-gray-50 text-gray-600 rounded">
{status}
</span>
);
}
}
28 changes: 28 additions & 0 deletions src/components/teachers/courses/FilterSelects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,25 @@ const prices = [
{ value: '1', label: '1 - 20$' },
];

const statuses = [
{ value: 'all', label: 'All Status' },
{ value: 'pending', label: 'Pending' },
{ value: 'approved', label: 'Approved' },
{ value: 'rejected', label: 'Rejected' },
];

interface FilterSelectsProps {
onCategoryChange: (category: string) => void;
onRatingChange: (rating: string) => void;
onPriceChange?: (price: string) => void;
onStatusChange?: (status: string) => void;
}

const FilterSelects: React.FC<FilterSelectsProps> = ({
onCategoryChange,
onRatingChange,
onPriceChange,
onStatusChange,
}) => {
return (
<div className="flex gap-6">
Expand Down Expand Up @@ -106,6 +115,25 @@ const FilterSelects: React.FC<FilterSelectsProps> = ({
</Select>
</div>
)}
{onStatusChange && (
<div className="w-60 flex flex-col justify-start items-start gap-2">
<div className="text-[#6e7484] text-xs font-normal leading-none">
Status:
</div>
<Select onValueChange={onStatusChange}>
<SelectTrigger className="h-12 pl-[18px] pr-4 py-3 bg-white border border-[#e8eaef] items-center gap-[103px] inline-flex overflow-hidden justify-between">
<SelectValue placeholder="All Status" />
</SelectTrigger>
<SelectContent>
{statuses.map((status) => (
<SelectItem key={status.value} value={status.value}>
{status.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
);
};
Expand Down
Loading