Flatlogic Bot a29f901104 test
2026-01-21 16:24:37 +00:00

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>;
};