289 lines
13 KiB
TypeScript
289 lines
13 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import type { ReactElement } from 'react';
|
|
import Head from 'next/head';
|
|
import { useRouter } from 'next/router';
|
|
import axios from 'axios';
|
|
import Link from 'next/link';
|
|
import LayoutGuest from '../../layouts/Guest';
|
|
import { useAppSelector } from '../../stores/hooks';
|
|
import { mdiClockOutline, mdiAccount, mdiTagOutline, mdiTranslate, mdiArrowLeft } from '@mdi/js';
|
|
import BaseIcon from '../../components/BaseIcon';
|
|
import BaseButton from '../../components/BaseButton';
|
|
|
|
// Course Details Page
|
|
export default function CourseDetailsPage() {
|
|
const router = useRouter();
|
|
const { id } = router.query;
|
|
const projectName = useAppSelector((state) => state.style.projectName) || 'EduFlow';
|
|
|
|
const [course, setCourse] = useState<any>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<any>(null);
|
|
|
|
useEffect(() => {
|
|
// Wait for router to be ready and id to be available
|
|
if (!router.isReady) return;
|
|
|
|
if (id && typeof id === 'string') {
|
|
const fetchCourse = async () => {
|
|
try {
|
|
setLoading(true);
|
|
console.log('Fetching course with ID:', id);
|
|
const response = await axios.get(`/courses/${id}`);
|
|
setCourse(response.data);
|
|
setError(null);
|
|
} catch (err) {
|
|
console.error('Failed to fetch course:', err);
|
|
setError(err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchCourse();
|
|
} else {
|
|
// If router is ready but no ID, stop loading
|
|
console.log('Router ready but no ID found in query:', router.query);
|
|
setLoading(false);
|
|
}
|
|
}, [id, router.isReady]);
|
|
|
|
const Header = () => (
|
|
<header className="bg-white border-b border-gray-100 sticky top-0 z-50">
|
|
<div className="container mx-auto px-4 h-16 flex items-center justify-between">
|
|
<Link href="/" className="flex items-center space-x-2">
|
|
<span className="text-2xl font-bold text-indigo-600">{projectName}</span>
|
|
</Link>
|
|
<div className="flex items-center space-x-4">
|
|
<Link href="/" className="text-gray-600 hover:text-indigo-600 font-medium text-sm">
|
|
All Courses
|
|
</Link>
|
|
<BaseButton label="Login" color="info" small href="/login" />
|
|
</div>
|
|
</div>
|
|
</header>
|
|
);
|
|
|
|
const Footer = () => (
|
|
<footer className="bg-slate-900 text-slate-400 py-12 border-t border-slate-800">
|
|
<div className="container mx-auto px-4">
|
|
<div className="flex flex-col md:flex-row justify-between items-center">
|
|
<div className="mb-4 md:mb-0">
|
|
<span className="text-xl font-bold text-white">{projectName}</span>
|
|
<p className="text-sm mt-2">Empowering learners worldwide.</p>
|
|
</div>
|
|
<div className="text-sm">
|
|
© 2026 {projectName} LMS. All rights reserved.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex flex-col min-h-screen">
|
|
<Header />
|
|
<main className="flex-grow flex items-center justify-center bg-gray-50">
|
|
<div className="flex flex-col items-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500 mb-4"></div>
|
|
<p className="text-gray-500 animate-pulse">Loading course details...</p>
|
|
</div>
|
|
</main>
|
|
<Footer />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error || !course) {
|
|
return (
|
|
<div className="flex flex-col min-h-screen">
|
|
<Header />
|
|
<main className="flex-grow container mx-auto px-4 py-12 text-center bg-gray-50">
|
|
<div className="max-w-md mx-auto bg-white p-8 rounded-2xl shadow-sm border border-gray-100">
|
|
<h1 className="text-2xl font-bold text-gray-800 mb-4">Course not found</h1>
|
|
<p className="text-gray-600 mb-8">The course you are looking for might have been removed or the link is incorrect.</p>
|
|
<BaseButton label="Back to Home Catalog" color="info" onClick={() => router.push('/')} icon={mdiArrowLeft} />
|
|
</div>
|
|
</main>
|
|
<Footer />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const getThumbnail = (course: any) => {
|
|
if (course.thumbnail && course.thumbnail.length > 0) {
|
|
return `/api/file/download?privateUrl=${course.thumbnail[0].privateUrl}`;
|
|
}
|
|
return 'https://images.pexels.com/photos/1181244/pexels-photo-1181244.jpeg?auto=compress&cs=tinysrgb&w=600';
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col min-h-screen bg-gray-50">
|
|
<Head>
|
|
<title>{`${course.title} | ${projectName}`}</title>
|
|
</Head>
|
|
<Header />
|
|
|
|
<main className="flex-grow">
|
|
{/* Hero Section */}
|
|
<div className="bg-indigo-900 text-white py-16">
|
|
<div className="container mx-auto px-4">
|
|
<div className="max-w-4xl">
|
|
<div className="flex items-center space-x-2 mb-4">
|
|
{course.category && (
|
|
<span className="bg-indigo-700 text-indigo-100 px-3 py-1 rounded-full text-sm font-semibold uppercase tracking-wide">
|
|
{course.category.name}
|
|
</span>
|
|
)}
|
|
<span className="bg-green-600 text-white px-3 py-1 rounded-full text-sm font-semibold uppercase tracking-wide">
|
|
{course.level || 'Beginner'}
|
|
</span>
|
|
</div>
|
|
<h1 className="text-4xl md:text-5xl font-extrabold mb-6 leading-tight">
|
|
{course.title}
|
|
</h1>
|
|
<p className="text-xl text-indigo-100 mb-8 max-w-3xl leading-relaxed">
|
|
{course.short_description}
|
|
</p>
|
|
|
|
<div className="flex flex-wrap gap-6 text-indigo-100">
|
|
<div className="flex items-center">
|
|
<BaseIcon path={mdiAccount} size={20} className="mr-2" />
|
|
<span>By <span className="font-semibold">{course.instructor ? `${course.instructor.firstName}${course.instructor.lastName ? " " + course.instructor.lastName : ""}` : 'Instructor'}</span></span>
|
|
</div>
|
|
{course.duration && (
|
|
<div className="flex items-center">
|
|
<BaseIcon path={mdiClockOutline} size={20} className="mr-2" />
|
|
<span>{course.duration} minutes</span>
|
|
</div>
|
|
)}
|
|
{course.language && (
|
|
<div className="flex items-center">
|
|
<BaseIcon path={mdiTranslate} size={20} className="mr-2" />
|
|
<span>{course.language}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="container mx-auto px-4 py-12">
|
|
<div className="flex flex-col lg:flex-row gap-8">
|
|
{/* Main Content */}
|
|
<div className="lg:w-2/3">
|
|
<section className="bg-white rounded-2xl shadow-sm p-8 mb-8 border border-gray-100">
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-6 border-b border-gray-50 pb-4">Course Description</h2>
|
|
<div className="prose max-w-none text-gray-700 leading-relaxed whitespace-pre-line">
|
|
{course.description || 'No description available.'}
|
|
</div>
|
|
</section>
|
|
|
|
<section className="bg-white rounded-2xl shadow-sm p-8 border border-gray-100">
|
|
<div className="flex items-center justify-between mb-6 border-b border-gray-50 pb-4">
|
|
<h2 className="text-2xl font-bold text-gray-900">Curriculum</h2>
|
|
<span className="text-indigo-600 font-bold bg-indigo-50 px-3 py-1 rounded-lg text-sm">
|
|
{course.lessons_course?.length || 0} lessons
|
|
</span>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{course.lessons_course && course.lessons_course.length > 0 ? (
|
|
course.lessons_course.sort((a: any, b: any) => (a.order || 0) - (b.order || 0)).map((lesson: any, index: number) => (
|
|
<div key={lesson.id} className="flex items-start p-4 rounded-xl border border-gray-100 hover:bg-indigo-50/30 transition-all group cursor-default">
|
|
<div className="flex-shrink-0 w-10 h-10 bg-indigo-50 text-indigo-600 rounded-full flex items-center justify-center font-bold mr-4 group-hover:bg-indigo-600 group-hover:text-white transition-colors">
|
|
{index + 1}
|
|
</div>
|
|
<div className="flex-grow">
|
|
<h3 className="font-bold text-gray-900 mb-1 group-hover:text-indigo-600 transition-colors">{lesson.title}</h3>
|
|
<p className="text-sm text-gray-500 line-clamp-2">{lesson.content || lesson.short_description || 'No content description.'}</p>
|
|
</div>
|
|
{lesson.duration && (
|
|
<div className="ml-4 text-xs font-medium text-gray-400 whitespace-nowrap pt-1">
|
|
{lesson.duration} min
|
|
</div>
|
|
)}
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="text-center py-12 bg-gray-50 rounded-xl border-2 border-dashed border-gray-200">
|
|
<BaseIcon path={mdiClockOutline} size={48} className="mx-auto text-gray-300 mb-4" />
|
|
<p className="text-gray-500 font-medium italic">
|
|
No lessons available for this course yet.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
{/* Sidebar */}
|
|
<div className="lg:w-1/3">
|
|
<div className="bg-white rounded-2xl shadow-xl p-6 sticky top-24 border border-gray-100 overflow-hidden">
|
|
<div className="mb-6 relative -mx-6 -mt-6">
|
|
<img
|
|
src={getThumbnail(course)}
|
|
alt={course.title}
|
|
className="w-full h-56 object-cover"
|
|
/>
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
|
|
</div>
|
|
|
|
<div className="mb-6">
|
|
<div className="flex items-baseline mb-4">
|
|
<span className="text-4xl font-black text-gray-900 mr-2">
|
|
{course.price === 0 || !course.price || course.price === '0' ? 'Free' : `$${course.price}`}
|
|
</span>
|
|
{course.price > 0 && (
|
|
<span className="text-gray-400 line-through text-lg">$349</span>
|
|
)}
|
|
</div>
|
|
|
|
<BaseButton
|
|
label="Enroll Now"
|
|
color="info"
|
|
className="w-full py-4 text-lg font-black rounded-xl shadow-lg shadow-indigo-200 hover:shadow-indigo-300 transition-all"
|
|
href="/login"
|
|
/>
|
|
<p className="text-center text-xs text-gray-500 mt-4 font-medium uppercase tracking-tighter">
|
|
30-Day Money-Back Guarantee
|
|
</p>
|
|
</div>
|
|
|
|
<div className="border-t border-gray-100 pt-6">
|
|
<h4 className="font-black text-gray-900 mb-4 uppercase text-xs tracking-widest">Course Features</h4>
|
|
<ul className="space-y-4">
|
|
<li className="flex items-center text-sm text-gray-700 font-medium">
|
|
<div className="w-8 h-8 rounded-lg bg-indigo-50 flex items-center justify-center mr-3">
|
|
<BaseIcon path={mdiClockOutline} size={18} className="text-indigo-600" />
|
|
</div>
|
|
{course.duration ? `${course.duration} minutes total` : 'Flexible duration'}
|
|
</li>
|
|
<li className="flex items-center text-sm text-gray-700 font-medium">
|
|
<div className="w-8 h-8 rounded-lg bg-green-50 flex items-center justify-center mr-3">
|
|
<BaseIcon path={mdiAccount} size={18} className="text-green-600" />
|
|
</div>
|
|
Direct instructor access
|
|
</li>
|
|
<li className="flex items-center text-sm text-gray-700 font-medium">
|
|
<div className="w-8 h-8 rounded-lg bg-amber-50 flex items-center justify-center mr-3">
|
|
<BaseIcon path={mdiTagOutline} size={18} className="text-amber-600" />
|
|
</div>
|
|
Certificate of completion
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<Footer />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
CourseDetailsPage.getLayout = function getLayout(page: ReactElement) {
|
|
return <LayoutGuest>{page}</LayoutGuest>;
|
|
}; |