courseworx/frontend/src/pages/CourseEdit.js
Mahmoud M. Abdalla 52fe7e05c5 Release v1.0.0 - Complete Course Management System
Major Features:
- Authentication & Authorization with JWT and role-based access
- Complete User Management System with CRUD operations
- Course Management System with publishing and enrollment
- Modern React UI with Tailwind CSS and responsive design
- Internationalization (i18n) with English and Arabic support
- File Upload System for images and documents
- RESTful API with Express.js and Sequelize ORM
- PostgreSQL database with proper relationships
- Super Admin password change functionality
- CSV import for bulk user creation
- Modal-based user add/edit operations
- Search, filter, and pagination capabilities

Technical Improvements:
- Fixed homepage routing and accessibility issues
- Resolved API endpoint mismatches and data rendering
- Enhanced security with proper validation and hashing
- Optimized performance with React Query and caching
- Improved error handling and user feedback
- Clean code structure with ESLint compliance

Bug Fixes:
- Fixed non-functional Add/Edit/Delete buttons
- Resolved CSV import BOM issues
- Fixed modal rendering and accessibility
- Corrected API base URL configuration
- Enhanced backend stability and error handling

This version represents a complete, production-ready Course Management System.
2025-07-27 23:30:23 +03:00

460 lines
No EOL
15 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from 'react-query';
import { useAuth } from '../contexts/AuthContext';
import { coursesAPI } from '../services/api';
import {
AcademicCapIcon,
ClockIcon,
CurrencyDollarIcon,
BookOpenIcon,
TagIcon,
UserGroupIcon,
} from '@heroicons/react/24/outline';
import LoadingSpinner from '../components/LoadingSpinner';
import toast from 'react-hot-toast';
const CourseEdit = () => {
const { id } = useParams();
const navigate = useNavigate();
const { isTrainer, isSuperAdmin } = useAuth();
const queryClient = useQueryClient();
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState(null);
// Course image upload state
const [imageFile, setImageFile] = useState(null);
const [imagePreview, setImagePreview] = useState(null);
const [uploadingImage, setUploadingImage] = useState(false);
const handleImageChange = (e) => {
const f = e.target.files[0];
setImageFile(f);
setImagePreview(f ? URL.createObjectURL(f) : null);
};
const handleImageUpload = async () => {
if (!imageFile || !formData?.title) return;
setUploadingImage(true);
try {
await coursesAPI.uploadCourseImage(formData.title.replace(/\s+/g, '-').toLowerCase(), imageFile);
toast.success('Course image uploaded!');
setImageFile(null);
setImagePreview(null);
} catch (err) {
toast.error('Upload failed');
} finally {
setUploadingImage(false);
}
};
const { data: courseData, isLoading, error } = useQuery(
['course', id],
() => coursesAPI.getById(id),
{ enabled: !!id }
);
useEffect(() => {
if (courseData && courseData.course) {
const c = courseData.course;
setFormData({
title: c.title || '',
description: c.description || '',
shortDescription: c.shortDescription || '',
price: c.price || 0,
duration: c.duration || '',
level: c.level || 'beginner',
category: c.category || '',
tags: c.tags ? c.tags.join(', ') : '',
requirements: c.requirements || '',
learningOutcomes: c.learningOutcomes || '',
maxStudents: c.maxStudents || '',
startDate: c.startDate ? c.startDate.slice(0, 10) : '',
endDate: c.endDate ? c.endDate.slice(0, 10) : '',
isPublished: c.isPublished || false,
isFeatured: c.isFeatured || false,
});
}
}, [courseData]);
const updateCourseMutation = useMutation(
(data) => coursesAPI.update(id, data),
{
onSuccess: () => {
queryClient.invalidateQueries(['courses']);
queryClient.invalidateQueries(['course', id]);
toast.success('Course updated successfully!');
navigate(`/courses/${id}`);
},
onError: (error) => {
toast.error(error.response?.data?.error || 'Failed to update course');
setLoading(false);
},
}
);
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
const courseData = {
...formData,
price: parseFloat(formData.price),
duration: formData.duration ? parseInt(formData.duration) : null,
maxStudents: formData.maxStudents ? parseInt(formData.maxStudents) : null,
tags: formData.tags ? formData.tags.split(',').map(tag => tag.trim()) : [],
startDate: formData.startDate || null,
endDate: formData.endDate || null,
};
updateCourseMutation.mutate(courseData);
};
const handleInputChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
if (!isTrainer && !isSuperAdmin) {
return (
<div className="text-center py-12">
<h3 className="text-lg font-medium text-gray-900">Access Denied</h3>
<p className="text-gray-500">You don't have permission to edit courses.</p>
</div>
);
}
if (isLoading || !formData) {
return <LoadingSpinner size="lg" className="mt-8" />;
}
if (error) {
return (
<div className="text-center py-12">
<h3 className="text-lg font-medium text-gray-900">Error loading course</h3>
<p className="text-gray-500">Failed to load course details.</p>
<p className="text-sm text-gray-400 mt-2">Error: {error.message}</p>
</div>
);
}
return (
<div className="max-w-4xl mx-auto">
<div className="mb-8">
<div className="flex items-center">
<AcademicCapIcon className="h-8 w-8 text-primary-600 mr-3" />
<div>
<h1 className="text-2xl font-bold text-gray-900">Edit Course</h1>
<p className="text-gray-600">Update the details below and save your changes</p>
</div>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-8">
{/* Basic Information */}
<div className="card">
<h2 className="text-lg font-semibold text-gray-900 mb-6">Basic Information</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2">
Course Title *
</label>
<input
type="text"
name="title"
value={formData.title}
onChange={handleInputChange}
className="input-field"
placeholder="Enter course title"
required
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2">
Short Description
</label>
<input
type="text"
name="shortDescription"
value={formData.shortDescription}
onChange={handleInputChange}
className="input-field"
placeholder="Brief description (max 500 characters)"
maxLength={500}
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2">
Full Description *
</label>
<textarea
name="description"
value={formData.description}
onChange={handleInputChange}
className="input-field"
rows={4}
placeholder="Detailed course description"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Category
</label>
<input
type="text"
name="category"
value={formData.category}
onChange={handleInputChange}
className="input-field"
placeholder="e.g., Programming, Design, Business"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Level *
</label>
<select
name="level"
value={formData.level}
onChange={handleInputChange}
className="input-field"
required
>
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced</option>
</select>
</div>
</div>
</div>
{/* Pricing and Duration */}
<div className="card">
<h2 className="text-lg font-semibold text-gray-900 mb-6">Pricing & Duration</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Price (USD) *
</label>
<div className="relative">
<CurrencyDollarIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="number"
name="price"
value={formData.price}
onChange={handleInputChange}
className="input-field pl-10"
placeholder="0.00"
min="0"
step="0.01"
required
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Duration (minutes)
</label>
<div className="relative">
<ClockIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="number"
name="duration"
value={formData.duration}
onChange={handleInputChange}
className="input-field pl-10"
placeholder="e.g., 120"
min="0"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Max Students
</label>
<div className="relative">
<UserGroupIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="number"
name="maxStudents"
value={formData.maxStudents}
onChange={handleInputChange}
className="input-field pl-10"
placeholder="e.g., 50"
min="1"
/>
</div>
</div>
</div>
</div>
{/* Course Details */}
<div className="card">
<h2 className="text-lg font-semibold text-gray-900 mb-6">Course Details</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Tags
</label>
<div className="relative">
<TagIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="text"
name="tags"
value={formData.tags}
onChange={handleInputChange}
className="input-field pl-10"
placeholder="tag1, tag2, tag3"
/>
</div>
<p className="text-xs text-gray-500 mt-1">Separate tags with commas</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Start Date
</label>
<input
type="date"
name="startDate"
value={formData.startDate}
onChange={handleInputChange}
className="input-field"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
End Date
</label>
<input
type="date"
name="endDate"
value={formData.endDate}
onChange={handleInputChange}
className="input-field"
/>
</div>
</div>
<div className="mt-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Requirements
</label>
<textarea
name="requirements"
value={formData.requirements}
onChange={handleInputChange}
className="input-field"
rows={3}
placeholder="What students need to know before taking this course"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Learning Outcomes
</label>
<textarea
name="learningOutcomes"
value={formData.learningOutcomes}
onChange={handleInputChange}
className="input-field"
rows={3}
placeholder="What students will learn from this course"
/>
</div>
</div>
</div>
{/* Publishing Options */}
<div className="card">
<h2 className="text-lg font-semibold text-gray-900 mb-6">Publishing Options</h2>
<div className="space-y-4">
<div className="flex items-center">
<input
type="checkbox"
name="isPublished"
checked={formData.isPublished}
onChange={handleInputChange}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
<label className="ml-2 block text-sm text-gray-900">
Publish course immediately
</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
name="isFeatured"
checked={formData.isFeatured}
onChange={handleInputChange}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
<label className="ml-2 block text-sm text-gray-900">
Feature this course
</label>
</div>
</div>
</div>
{/* Course Image Upload */}
<div className="card mb-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Upload Course Image</h3>
<input type="file" accept="image/*" onChange={handleImageChange} />
{imagePreview && (
<img src={imagePreview} alt="Preview" className="mt-4 h-32 object-contain rounded" />
)}
<button
type="button"
className="btn-primary mt-4"
onClick={handleImageUpload}
disabled={!imageFile || uploadingImage}
>
{uploadingImage ? 'Uploading...' : 'Upload'}
</button>
</div>
{/* Submit Buttons */}
<div className="flex justify-end space-x-4">
<button
type="button"
onClick={() => navigate('/courses')}
className="btn-secondary"
>
Cancel
</button>
<button
type="submit"
disabled={loading}
className="btn-primary flex items-center"
>
{loading ? (
<>
<LoadingSpinner size="sm" />
<span className="ml-2">Saving...</span>
</>
) : (
<>
<BookOpenIcon className="h-5 w-5 mr-2" />
Save Changes
</>
)}
</button>
</div>
</form>
</div>
);
};
export default CourseEdit;