// components/CourseDetailPage.jsx /** * Course Detail Page Component * * Fetches and displays detailed information for a single course, * including overview, curriculum, instructor details, reviews, * and related courses. Uses tabs for content organization. */ // React, Hooks, PropTypes are globally available function CourseDetailPage({ courseId }) { // --- State --- const [course, setCourse] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState("overview"); // Default tab // --- API Endpoint --- const COURSE_DETAIL_ENDPOINT = "/api/get_course_detail.php"; // --- Effects --- // Fetch course details when courseId changes useEffect(() => { const fetchCourse = async () => { // Reset state for new fetch setLoading(true); setError(null); setCourse(null); setActiveTab("overview"); // Reset tab on new course load // Ensure courseId is a valid number before proceeding const numericCourseId = parseInt(courseId, 10); if (isNaN(numericCourseId) || numericCourseId <= 0) { setError("Invalid Course ID provided."); setLoading(false); console.error("CourseDetailPage: Invalid courseId prop received:", courseId); return; } console.log(`CourseDetailPage: Fetching details for course ID: ${numericCourseId}`); try { const response = await fetch(`${COURSE_DETAIL_ENDPOINT}?id=${numericCourseId}`); // Check for network or server errors (non-2xx status) if (!response.ok) { let errorMsg = `HTTP error! Status: ${response.status}`; try { // Try to parse JSON error from backend const errData = await response.json(); errorMsg = errData.error || errorMsg; } catch (e) { /* Ignore parsing error if body isn't JSON */ } throw new Error(errorMsg); } const data = await response.json(); // Check for application-level errors (e.g., { success: false, error: '...' }) if (!data.success) { // Handle specific "Not Found" error from API if (response.status === 404 || data.error?.toLowerCase().includes('not found')) { throw new Error(`Course with ID ${numericCourseId} not found.`); } throw new Error(data.error || "Failed to fetch course details"); } console.log("CourseDetailPage: Course details fetched:", data.data); setCourse(data.data); // Set the fetched course data } catch (err) { console.error("CourseDetailPage: Error fetching course details:", err); setError(err.message); // Set error state to display message } finally { setLoading(false); // Ensure loading state is turned off } }; fetchCourse(); // Scroll to top when courseId changes for better UX window.scrollTo(0, 0); }, [courseId]); // Re-run effect if courseId changes // --- Helper Functions --- // Safely splits text by newline, handling null/undefined input and trimming lines const splitLines = (text) => { return text ? text.split('\n').map(line => line.trim()).filter(line => line !== '') : []; }; // Renders star ratings using Font Awesome icons const renderStars = (rating) => { const numRating = parseFloat(rating) || 0; const fullStars = Math.floor(numRating); // Use Math.round to decide half star more conventionally const halfStar = Math.round(numRating - fullStars) === 1 ? 1 : 0; // Correct calculation for empty stars const emptyStars = 5 - fullStars - halfStar; return ( <> {/* Render full stars */} {fullStars > 0 && Array(fullStars).fill(null).map((_, i) => )} {/* Render half star */} {halfStar > 0 && } {/* Render empty stars */} {emptyStars > 0 && Array(emptyStars).fill(null).map((_, i) => )} ); }; // Handles image loading errors for various images on the page const handleImageError = (e, placeholderType = 'course') => { e.target.onerror = null; // Prevent infinite loop let placeholderUrl = 'https://via.placeholder.com/400x225?text=No+Image'; // Default if (placeholderType === 'instructor') { placeholderUrl = 'https://via.placeholder.com/100?text=Inst'; } else if (placeholderType === 'user') { placeholderUrl = 'https://via.placeholder.com/50?text=User'; } else if (placeholderType === 'related') { placeholderUrl = 'https://via.placeholder.com/300x150?text=Related'; } e.target.src = placeholderUrl; }; // --- Render Logic --- // Loading State if (loading) { return
Loading course details...
; } // Error State if (error) { return (

Error Loading Course

{error}

{/* Provide a way back */} Back to Courses
); } // Course Not Found (Should be caught by error state now, but keep as fallback) if (!course) { return (

Course Not Found

The requested course could not be found.

Back to Courses
); } // --- Tab Content Rendering --- const renderTabContent = () => { switch (activeTab) { case "overview": return (

What you'll learn

{course.learning_objectives && splitLines(course.learning_objectives).length > 0 ? ( ) :

Learning objectives not specified for this course.

}

Requirements

{course.requirements && splitLines(course.requirements).length > 0 ? ( ) :

No specific prerequisites or requirements listed.

}

Description

{/* Render description safely. If it contains HTML, use a sanitizer or render plain text */}

{course.detailed_description || course.description || "No detailed description available."}

); case "curriculum": return (

Course Curriculum

{course.modules && course.modules.length > 0 ? (
{course.modules.map((module) => (
{module.title || `Module ${module.order_index}`}
{module.lessons && module.lessons.length > 0 ? (
    {module.lessons.map((lesson) => (
  • {/* Choose icon based on content type */} {lesson.title || "Untitled Lesson"} {/* Display preview tag if applicable */} {lesson.is_preview ? Preview : null} {lesson.duration_minutes > 0 && ( // Only show duration if > 0 {lesson.duration_minutes} min )}
  • ))}
) :

No lessons listed in this module.

}
))}
) :

Curriculum details are not available for this course.

}
); case "instructor": return (

Instructor

{course.instructor_name ? (
{course.instructor_name} handleImageError(e, 'instructor')} />

{course.instructor_name}

{course.instructor_title &&

{course.instructor_title}

}
{renderStars(course.instructor_rating)} ({course.instructor_total_ratings || 0} ratings)
{course.instructor_bio ? (

{course.instructor_bio}

) : (

No biography available for this instructor.

)}
) :

Instructor information is not available for this course.

}
); case "reviews": return (

Student Reviews ({course.reviews?.length || 0})

{course.reviews && course.reviews.length > 0 ? (
{course.reviews.map((review) => (
{review.full_name handleImageError(e, 'user')} />

{review.full_name || review.username || "Anonymous"}

{renderStars(review.rating)} {new Date(review.created_at).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
{review.content ? (

{review.content}

) : (

No comment provided.

)}
))}
) :

Be the first to review this course!

} {/* TODO: Add a "Write a Review" button/form here if user is enrolled and hasn't reviewed */}
); default: return null; // Should not happen } }; // --- Main Course Detail Render --- return (
{/* --- Course Header Section --- */}

{course.title}

{course.description}

{renderStars(course.rating)} ({course.total_ratings || 0} ratings) {course.enrollment_count || 0} students {course.level || 'N/A'} Created by: {course.instructor_name || 'Unknown'} {/* TODO: Link to instructor profile page if exists */}
{/* TODO: Add logic based on enrollment status */}
{course.title} handleImageError(e, 'course')} />
{/* --- Course Content Section (Tabs) --- */}
{/* Tab Buttons */} {['overview', 'curriculum', 'instructor', 'reviews'].map(tabName => ( ))}
{/* Tab Panels */}
{/* Render the active tab's content */} {renderTabContent()}
{/* --- Related Courses Section --- */} {course.related_courses && course.related_courses.length > 0 && (

Related Courses

{/* Use the CourseList component if suitable, or render cards directly */}
{course.related_courses.map((relatedCourse) => ( // Use CourseCard component if available and suitable window.CourseCard ? ( ) : ( // Fallback direct render (should not be needed if CourseCard loads)
{relatedCourse.title} handleImageError(e, 'related')} />

{relatedCourse.title}

{relatedCourse.category}

{/* Add rating/instructor if available in related data */}
) ))}
)}
); } // --- Prop Type Validation --- CourseDetailPage.propTypes = { // Expects a courseId prop, which should be a string (from hash) or number courseId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, }; // Make CourseDetailPage globally available window.CourseDetailPage = CourseDetailPage;