course video

This commit is contained in:
5t4l1n
2025-07-29 11:50:32 +05:30
parent f72bcc69aa
commit ac23a90125
2 changed files with 267 additions and 133 deletions
@@ -1,69 +1,63 @@
"use client" "use client"
import { useEffect, useState } from "react"
import { useRouter, useParams } from "next/navigation"
import { toast } from "react-hot-toast"
import { useAuth } from "@/context/auth-context"
import api from "@/lib/api"
import { CourseSidebar } from "@/components/course-sidebar" import { CourseSidebar } from "@/components/course-sidebar"
import { LessonViewer } from "@/components/lesson-viewer" import { LessonViewer } from "@/components/lesson-viewer"
import { Loader2 } from "lucide-react" import { Loader2 } from "lucide-react"
import { useAuth } from "@/context/auth-context"
import { useRouter } from "next/navigation"
import { toast } from "react-hot-toast"
import { useState, useEffect, use } from "react" // ✅ Added 'use' import
import type { Course } from "@/lib/types" import type { Course } from "@/lib/types"
import api from "@/lib/api"
interface CourseDetailPageProps { export default function LessonDetailPage() {
params: Promise<{ // ✅ Changed to Promise const params = useParams()
courseId: string
lessonId: string
}>
}
export default function CourseDetailPage({ params }: CourseDetailPageProps) {
const { courseId, lessonId } = use(params) // ✅ Unwrap params using React.use()
const { user, firebaseUser, isLoadingAuth } = useAuth()
const router = useRouter() const router = useRouter()
const courseId = params?.courseId ?? ''
const lessonId = params?.lessonId ?? ''
const { user, firebaseUser, isLoading: isAuthLoading } = useAuth()
const [course, setCourse] = useState<Course | null>(null) const [course, setCourse] = useState<Course | null>(null)
const [isLoadingCourse, setIsLoadingCourse] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
useEffect(() => { useEffect(() => {
if (!isLoadingAuth && !user && !firebaseUser) { if (!isAuthLoading && !user && !firebaseUser) {
toast.error("Please login to view courses.") toast.error("Please login to view lessons.")
router.push("/") router.replace("/")
return return
} }
const fetchCourse = async () => { if ((user || firebaseUser) && courseId) {
setIsLoadingCourse(true) const fetchCourse = async () => {
setError(null) setLoading(true)
try { setError(null)
const response = await api.get<Course>(`/api/courses/${courseId}`) try {
setCourse(response.data) const response = await api.get<Course>(`/api/courses/${courseId}?t=${Date.now()}`)
} catch (err: any) { setCourse(response.data)
console.error("Failed to fetch course details:", err) } catch (err: any) {
setError(err.response?.data?.message || "Failed to load course details.") setError(err.message || "Failed to load course.")
toast.error(err.response?.data?.message || "Failed to load course details.") toast.error(err.message || "Failed to load course.")
} finally { } finally {
setIsLoadingCourse(false) setLoading(false)
}
} }
}
if (user || firebaseUser) {
fetchCourse() fetchCourse()
} }
}, [user, firebaseUser, isLoadingAuth, router, courseId]) }, [user, firebaseUser, isAuthLoading, router, courseId])
if (isLoadingAuth || isLoadingCourse) { if (isAuthLoading || loading) {
return ( return (
<div className="flex justify-center items-center min-h-[calc(100vh-64px)]"> <div className="flex justify-center items-center min-h-screen bg-white">
<Loader2 className="h-8 w-8 animate-spin text-primary-purple" /> <Loader2 className="h-10 w-10 animate-spin text-indigo-600" />
<span className="ml-2 text-lg">Loading course...</span> <span className="ml-3 text-indigo-700 text-lg">Loading lesson...</span>
</div> </div>
) )
} }
if (error) { if (error) {
return ( return (
<div className="flex justify-center items-center min-h-[calc(100vh-64px)] text-red-500"> <div className="flex justify-center items-center min-h-screen bg-white text-red-600">
<p>{error}</p> <p>{error}</p>
</div> </div>
) )
@@ -71,22 +65,18 @@ export default function CourseDetailPage({ params }: CourseDetailPageProps) {
if (!course) { if (!course) {
return ( return (
<div className="flex justify-center items-center min-h-[calc(100vh-64px)] text-gray-600 dark:text-gray-300"> <div className="flex justify-center items-center min-h-screen bg-white text-gray-700">
<p className="text-xl">Course not found.</p> <p>Course not found.</p>
</div> </div>
) )
} }
return ( return (
<div className="flex flex-col md:flex-row min-h-[calc(100vh-64px)]"> <div className="flex flex-col md:flex-row min-h-screen bg-gray-50">
<CourseSidebar <CourseSidebar courseId={course.id} modules={course.modules} activeLessonId={lessonId} />
courseId={course.id} <main className="flex-grow p-8 max-w-4xl mx-auto w-full">
modules={course.modules}
activeLessonId={lessonId}
/>
<div className="flex-1 p-4 md:p-8 overflow-y-auto">
<LessonViewer courseId={course.id} lessonId={lessonId} /> <LessonViewer courseId={course.id} lessonId={lessonId} />
</div> </main>
</div> </div>
) )
} }
+227 -83
View File
@@ -1,112 +1,256 @@
"use client" "use client"
import { CourseSidebar } from "@/components/course-sidebar" import { useEffect, useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { useRouter, useParams } from "next/navigation"
import { Loader2 } from "lucide-react" import { Loader2 } from "lucide-react"
import { useAuth } from "@/context/auth-context"
import { useRouter } from "next/navigation"
import { toast } from "react-hot-toast" import { toast } from "react-hot-toast"
import { useState, useEffect, use } from "react" // ✅ Added 'use' import
import type { Course } from "@/lib/types"
import api from "@/lib/api" import api from "@/lib/api"
import { useAuth } from "@/context/auth-context"
interface CourseOverviewPageProps { type Lesson = {
params: Promise<{ // ✅ Changed to Promise id: string
courseId: string title: string
}> description?: string
video_url?: string
}
type Module = {
id: string
title: string
lessons: Lesson[]
}
type Course = {
id: string
title: string
description: string
modules: Module[]
embed_url?: string
video_url?: string
} }
export default function CourseOverviewPage({ params }: CourseOverviewPageProps) { export default function CoursePage() {
const { courseId } = use(params) // ✅ Unwrap params using React.use() const { user, firebaseUser, isLoading: authLoading } = useAuth()
const { user, firebaseUser, isLoadingAuth } = useAuth() const params = useParams()
const router = useRouter() const router = useRouter()
const courseId = params?.courseId as string
const [course, setCourse] = useState<Course | null>(null) const [course, setCourse] = useState<Course | null>(null)
const [isLoadingCourse, setIsLoadingCourse] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
// Sidebar state: current
const [selectedModuleIdx, setSelectedModuleIdx] = useState(0)
const [selectedLessonIdx, setSelectedLessonIdx] = useState(0)
const [completed, setCompleted] = useState(false)
useEffect(() => { useEffect(() => {
if (!isLoadingAuth && !user && !firebaseUser) { if (!authLoading && !user && !firebaseUser) {
toast.error("Please login to view courses.") toast.error("Please login to view courses.")
router.push("/") router.replace("/")
return return
} }
if ((user || firebaseUser) && courseId) {
const fetchCourse = async () => { ;(async () => {
setIsLoadingCourse(true) setLoading(true)
setError(null) setError(null)
try { try {
const response = await api.get<Course>(`/api/courses/${courseId}`) const resp = await api.get<Course>(`/api/courses/${courseId}?t=${Date.now()}`)
setCourse(response.data) setCourse(resp.data)
} catch (err: any) { setSelectedModuleIdx(0)
console.error("Failed to fetch course details:", err) setSelectedLessonIdx(0)
setError(err.response?.data?.message || "Failed to load course details.") setCompleted(false)
toast.error(err.response?.data?.message || "Failed to load course details.") } catch {
} finally { setError("Failed to load course data.")
setIsLoadingCourse(false) } finally {
} setLoading(false)
}
})()
} }
}, [authLoading, user, firebaseUser, courseId, router])
if (user || firebaseUser) { // Helper: embed URL
fetchCourse() function getEmbedUrl(url?: string): string | undefined {
if (!url) return undefined
const regExp = /(?:youtu\.be\/|youtube\.com\/(?:watch\?v=|embed\/))([^#&?]{11})/
const match = url.match(regExp)
if (match && match[1]) {
return `https://www.youtube.com/embed/${match[1]}?rel=0&modestbranding=1`
} }
}, [user, firebaseUser, isLoadingAuth, router, courseId]) // fallback (could already be an embed url or another provider)
return url
useEffect(() => {
if (course && course.modules.length > 0 && course.modules[0].lessons.length > 0) {
// Redirect to the first lesson of the course
router.replace(`/courses/${courseId}/lesson/${course.modules[0].lessons[0].id}`)
}
}, [course, courseId, router])
if (isLoadingAuth || isLoadingCourse) {
return (
<div className="flex justify-center items-center min-h-[calc(100vh-64px)]">
<Loader2 className="h-8 w-8 animate-spin text-primary-purple" />
<span className="ml-2 text-lg">Loading course...</span>
</div>
)
} }
if (error) { const modules = course?.modules || []
return ( // Pick first non-empty for fallback if nothing selected
<div className="flex justify-center items-center min-h-[calc(100vh-64px)] text-red-500"> const selModIdx = modules.length > 0 ? selectedModuleIdx : 0
<p>{error}</p> const lessons = modules.length > 0 ? modules[selModIdx]?.lessons : []
</div> const selLesIdx = lessons.length > 0 ? selectedLessonIdx : 0
) const currentLesson = lessons.length > 0 ? lessons[selLesIdx] : undefined
// for navigation
const isEnd =
modules.length > 0 &&
selModIdx === modules.length - 1 &&
lessons.length > 0 &&
selLesIdx === lessons.length - 1
function prev() {
if (selLesIdx > 0) setSelectedLessonIdx(selLesIdx - 1)
else if (selModIdx > 0) {
const prevLessons = modules[selModIdx - 1].lessons
setSelectedModuleIdx(selModIdx - 1)
setSelectedLessonIdx(Math.max(prevLessons.length - 1, 0))
}
}
function next() {
if (lessons.length && selLesIdx < lessons.length - 1) setSelectedLessonIdx(selLesIdx + 1)
else if (selModIdx < modules.length - 1) {
setSelectedModuleIdx(selModIdx + 1)
setSelectedLessonIdx(0)
}
} }
if (!course) { function markComplete() {
return ( setCompleted(true)
<div className="flex justify-center items-center min-h-[calc(100vh-64px)] text-gray-600 dark:text-gray-300"> toast.success("Course Completed!")
<p className="text-xl">Course not found.</p>
</div>
)
} }
if (authLoading || loading) return (
<div className="flex items-center justify-center min-h-screen">
<Loader2 className="h-8 w-8 animate-spin text-indigo-700" /><span className="ml-2">Loading course...</span>
</div>
)
if (error) return (
<div className="flex items-center justify-center min-h-screen text-red-500">{error}</div>
)
if (!course) return (
<div className="flex items-center justify-center min-h-screen text-gray-700">Course not found.</div>
)
return ( return (
<div className="flex flex-col md:flex-row min-h-[calc(100vh-64px)]"> <div className="flex flex-col md:flex-row gap-6 max-w-7xl mx-auto px-4 py-8">
<CourseSidebar {/* Sidebar: Always show all modules and lessons */}
courseId={course.id} <aside className="w-full md:w-64 bg-white rounded-xl shadow-md p-4 md:sticky md:top-20 h-fit max-h-[80vh] overflow-y-auto mb-4 md:mb-0">
modules={course.modules} <h2 className="text-lg font-bold mb-4 text-indigo-700">{course.title}</h2>
activeLessonId="" <p className="text-xs text-gray-500 mb-5">{course.description}</p>
/> {modules.length === 0 ? (
<div className="flex-1 p-4 md:p-8 overflow-y-auto"> <div className="text-gray-500 italic py-6">No modules yet for this course.</div>
<Card className="bg-white shadow-md rounded-lg p-6 dark:bg-gray-800 dark:text-gray-100"> ) : (
<CardHeader> <ul>
<CardTitle className="text-2xl font-bold text-primary-purple"> {modules.map((mod, mIdx) => (
{course.title} Overview <li key={mod.id} className="mb-4">
</CardTitle> <div
</CardHeader> className={`font-semibold mb-2 cursor-pointer ${
<CardContent className="space-y-4"> mIdx === selModIdx ? "text-purple-600" : "text-gray-700"
<p className="text-lg text-gray-700 dark:text-gray-200"> }`}
{course.description} onClick={() => { setSelectedModuleIdx(mIdx); setSelectedLessonIdx(0); }}
</p> >
<p className="text-gray-600 dark:text-gray-300"> {mod.title}
Select a lesson from the sidebar to begin. </div>
</p> <ul className="pl-4 border-l-2 border-gray-100">
</CardContent> {mod.lessons.map((lesson, lIdx) => (
</Card> <li
</div> key={lesson.id}
className={
`py-1 px-2 rounded mb-1 cursor-pointer text-sm
${mIdx===selModIdx && lIdx===selLesIdx
? "bg-indigo-600 text-white"
: "hover:bg-indigo-100 text-gray-700"}`
}
onClick={() => { setSelectedModuleIdx(mIdx); setSelectedLessonIdx(lIdx); }}
>
{lesson.title}
</li>
))}
{mod.lessons.length === 0 && (
<li className="text-xs text-gray-400 pl-1 py-1">No lessons</li>
)}
</ul>
</li>
))}
</ul>
)}
</aside>
{/* Main: show lesson or course video/desc/mark as read */}
<main className="flex-1 bg-white rounded-xl shadow-md p-6 min-h-80 max-w-2xl mx-auto">
{modules.length > 0 && lessons.length > 0 && currentLesson ? (
<>
<h2 className="text-2xl font-bold mb-2">{currentLesson.title}</h2>
{currentLesson.video_url && (
<div className="aspect-video rounded overflow-hidden my-4 shadow-lg">
<iframe
src={getEmbedUrl(currentLesson.video_url)}
title={currentLesson.title}
allowFullScreen
width="100%"
height="100%"
className="w-full h-full"
/>
</div>
)}
{currentLesson.description && (
<div className="text-gray-700 mb-6">{currentLesson.description}</div>
)}
{/* Navigation / mark as read */}
<div className="flex justify-between gap-2">
<button
className="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300 disabled:opacity-60"
onClick={prev}
disabled={selModIdx===0 && selLesIdx===0}
>
Previous
</button>
{!isEnd ? (
<button className="px-4 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-700" onClick={next}>
Next
</button>
) : (
<button
className={`px-4 py-2 rounded font-semibold ${
completed
? "bg-green-600 text-white"
: "bg-purple-600 text-white hover:bg-purple-700"
}`}
onClick={markComplete}
disabled={completed}
>
{completed ? "Course Completed ✓" : "Mark as Read"}
</button>
)}
</div>
{completed && (
<div className="mt-6 bg-green-50 border border-green-300 p-4 rounded text-green-700 text-center font-bold shadow">
🎉 Course Completed! Certificate coming soon.
</div>
)}
</>
) :
// Course has no modules or no lessons
(
<div>
<h2 className="text-2xl font-bold mb-3">{course.title}</h2>
<p className="mb-6 text-gray-700">{course.description}</p>
{(course.embed_url || course.video_url) ? (
<div className="aspect-video rounded-lg overflow-hidden my-5 shadow-lg">
<iframe
src={getEmbedUrl(course.embed_url || course.video_url)}
title={`Video for ${course.title}`}
allowFullScreen
width="100%"
height="100%"
className="w-full h-full"
/>
</div>
) : (
<div className="text-gray-400 italic mb-4">No video available for this course yet.</div>
)}
<div className="mt-8 text-gray-500 text-center">
No modules or lessons yet.
</div>
</div>
)}
</main>
</div> </div>
) )
} }