courseworx/frontend/src/pages/Courses.js

259 lines
No EOL
8.7 KiB
JavaScript

import React, { useState } from 'react';
import { useQuery } from 'react-query';
import { Link } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { coursesAPI } from '../services/api';
import {
MagnifyingGlassIcon,
PlusIcon,
StarIcon,
ClockIcon,
UserIcon,
AcademicCapIcon,
} from '@heroicons/react/24/outline';
import LoadingSpinner from '../components/LoadingSpinner';
const Courses = () => {
const { isTrainer, isSuperAdmin } = useAuth();
const [search, setSearch] = useState('');
const [category, setCategory] = useState('');
const [level, setLevel] = useState('');
const [sortBy, setSortBy] = useState('createdAt');
const [sortOrder, setSortOrder] = useState('DESC');
const { data: coursesData, isLoading } = useQuery(
['courses', { search, category, level, sortBy, sortOrder }],
() => coursesAPI.getAll({ search, category, level, sortBy, sortOrder }),
{ keepPreviousData: true }
);
const { data: categoriesData } = useQuery(
['categories'],
() => coursesAPI.getCategories(),
{ enabled: !isTrainer && !isSuperAdmin }
);
const formatPrice = (price) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(price);
};
const formatDuration = (minutes) => {
if (!minutes) return 'N/A';
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
};
const getLevelColor = (level) => {
const colors = {
beginner: 'badge-success',
intermediate: 'badge-warning',
advanced: 'badge-danger',
};
return colors[level] || 'badge-secondary';
};
if (isLoading) {
return <LoadingSpinner size="lg" className="mt-8" />;
}
return (
<div>
<div className="mb-8">
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-gray-900">Courses</h1>
<p className="text-gray-600">
{isTrainer || isSuperAdmin
? 'Manage your courses and create new ones'
: 'Discover and enroll in courses'
}
</p>
</div>
{(isTrainer || isSuperAdmin) && (
<Link
to="/courses/create"
className="btn-primary flex items-center"
>
<PlusIcon className="h-5 w-5 mr-2" />
Create Course
</Link>
)}
</div>
</div>
{/* Filters */}
<div className="card mb-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Search
</label>
<div className="relative">
<MagnifyingGlassIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="text"
placeholder="Search courses..."
className="input-field pl-10"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Category
</label>
<select
className="input-field"
value={category}
onChange={(e) => setCategory(e.target.value)}
>
<option value="">All Categories</option>
{categoriesData?.categories?.map((cat) => (
<option key={cat} value={cat}>
{cat}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Level
</label>
<select
className="input-field"
value={level}
onChange={(e) => setLevel(e.target.value)}
>
<option value="">All Levels</option>
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Sort By
</label>
<select
className="input-field"
value={`${sortBy}-${sortOrder}`}
onChange={(e) => {
const [field, order] = e.target.value.split('-');
setSortBy(field);
setSortOrder(order);
}}
>
<option value="createdAt-DESC">Newest First</option>
<option value="createdAt-ASC">Oldest First</option>
<option value="title-ASC">Title A-Z</option>
<option value="title-DESC">Title Z-A</option>
<option value="price-ASC">Price Low to High</option>
<option value="price-DESC">Price High to Low</option>
</select>
</div>
</div>
</div>
{/* Course Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{coursesData?.courses?.map((course) => (
<div key={course.id} className="card-hover group">
<div className="aspect-w-16 aspect-h-9 mb-4">
{course.thumbnail ? (
<img
src={course.thumbnail}
alt={course.title}
className="w-full h-48 object-cover rounded-lg"
/>
) : (
<div className="w-full h-48 bg-gray-200 rounded-lg flex items-center justify-center">
<AcademicCapIcon className="h-12 w-12 text-gray-400" />
</div>
)}
</div>
<div className="space-y-3">
<div className="flex items-start justify-between">
<h3 className="text-lg font-semibold text-gray-900 group-hover:text-primary-600 transition-colors">
{course.title}
</h3>
<span className={`badge ${getLevelColor(course.level)}`}>
{course.level}
</span>
</div>
<p className="text-gray-600 text-sm line-clamp-2">
{course.shortDescription || course.description}
</p>
<div className="flex items-center justify-between text-sm text-gray-500">
<div className="flex items-center">
<UserIcon className="h-4 w-4 mr-1" />
<span>{course.trainer?.firstName} {course.trainer?.lastName}</span>
</div>
<div className="flex items-center">
<ClockIcon className="h-4 w-4 mr-1" />
<span>{formatDuration(course.duration)}</span>
</div>
</div>
{course.rating && (
<div className="flex items-center">
<StarIcon className="h-4 w-4 text-yellow-400 mr-1" />
<span className="text-sm text-gray-600">
{course.rating} ({course.totalRatings} ratings)
</span>
</div>
)}
<div className="flex items-center justify-between pt-3 border-t border-gray-200">
<div className="text-lg font-bold text-gray-900">
{formatPrice(course.price)}
</div>
<Link
to={`/courses/${course.id}`}
className="btn-primary text-sm"
>
{isTrainer || isSuperAdmin ? 'Edit' : 'View Details'}
</Link>
</div>
</div>
</div>
))}
</div>
{/* Empty State */}
{coursesData?.courses?.length === 0 && (
<div className="text-center py-12">
<AcademicCapIcon className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">No courses found</h3>
<p className="mt-1 text-sm text-gray-500">
{search || category || level
? 'Try adjusting your filters'
: 'Get started by creating your first course'
}
</p>
</div>
)}
{/* Pagination */}
{coursesData?.pagination && coursesData.pagination.totalPages > 1 && (
<div className="mt-8 flex justify-center">
<nav className="flex items-center space-x-2">
{/* Add pagination controls here */}
</nav>
</div>
)}
</div>
);
};
export default Courses;