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"
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 { LessonViewer } from "@/components/lesson-viewer"
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 api from "@/lib/api"
interface CourseDetailPageProps {
params: Promise<{ // ✅ Changed to Promise
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()
export default function LessonDetailPage() {
const params = useParams()
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 [isLoadingCourse, setIsLoadingCourse] = useState(true)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (!isLoadingAuth && !user && !firebaseUser) {
toast.error("Please login to view courses.")
router.push("/")
if (!isAuthLoading && !user && !firebaseUser) {
toast.error("Please login to view lessons.")
router.replace("/")
return
}
const fetchCourse = async () => {
setIsLoadingCourse(true)
setError(null)
try {
const response = await api.get<Course>(`/api/courses/${courseId}`)
setCourse(response.data)
} catch (err: any) {
console.error("Failed to fetch course details:", err)
setError(err.response?.data?.message || "Failed to load course details.")
toast.error(err.response?.data?.message || "Failed to load course details.")
} finally {
setIsLoadingCourse(false)
if ((user || firebaseUser) && courseId) {
const fetchCourse = async () => {
setLoading(true)
setError(null)
try {
const response = await api.get<Course>(`/api/courses/${courseId}?t=${Date.now()}`)
setCourse(response.data)
} catch (err: any) {
setError(err.message || "Failed to load course.")
toast.error(err.message || "Failed to load course.")
} finally {
setLoading(false)
}
}
}
if (user || firebaseUser) {
fetchCourse()
}
}, [user, firebaseUser, isLoadingAuth, router, courseId])
}, [user, firebaseUser, isAuthLoading, router, courseId])
if (isLoadingAuth || isLoadingCourse) {
if (isAuthLoading || loading) {
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 className="flex justify-center items-center min-h-screen bg-white">
<Loader2 className="h-10 w-10 animate-spin text-indigo-600" />
<span className="ml-3 text-indigo-700 text-lg">Loading lesson...</span>
</div>
)
}
if (error) {
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>
</div>
)
@@ -71,22 +65,18 @@ export default function CourseDetailPage({ params }: CourseDetailPageProps) {
if (!course) {
return (
<div className="flex justify-center items-center min-h-[calc(100vh-64px)] text-gray-600 dark:text-gray-300">
<p className="text-xl">Course not found.</p>
<div className="flex justify-center items-center min-h-screen bg-white text-gray-700">
<p>Course not found.</p>
</div>
)
}
return (
<div className="flex flex-col md:flex-row min-h-[calc(100vh-64px)]">
<CourseSidebar
courseId={course.id}
modules={course.modules}
activeLessonId={lessonId}
/>
<div className="flex-1 p-4 md:p-8 overflow-y-auto">
<div className="flex flex-col md:flex-row min-h-screen bg-gray-50">
<CourseSidebar courseId={course.id} modules={course.modules} activeLessonId={lessonId} />
<main className="flex-grow p-8 max-w-4xl mx-auto w-full">
<LessonViewer courseId={course.id} lessonId={lessonId} />
</div>
</main>
</div>
)
}
+227 -83
View File
@@ -1,112 +1,256 @@
"use client"
import { CourseSidebar } from "@/components/course-sidebar"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { useEffect, useState } from "react"
import { useRouter, useParams } from "next/navigation"
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 api from "@/lib/api"
import { useAuth } from "@/context/auth-context"
interface CourseOverviewPageProps {
params: Promise<{ // ✅ Changed to Promise
courseId: string
}>
type Lesson = {
id: 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) {
const { courseId } = use(params) // ✅ Unwrap params using React.use()
const { user, firebaseUser, isLoadingAuth } = useAuth()
export default function CoursePage() {
const { user, firebaseUser, isLoading: authLoading } = useAuth()
const params = useParams()
const router = useRouter()
const courseId = params?.courseId as string
const [course, setCourse] = useState<Course | null>(null)
const [isLoadingCourse, setIsLoadingCourse] = useState(true)
const [loading, setLoading] = useState(true)
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(() => {
if (!isLoadingAuth && !user && !firebaseUser) {
if (!authLoading && !user && !firebaseUser) {
toast.error("Please login to view courses.")
router.push("/")
router.replace("/")
return
}
const fetchCourse = async () => {
setIsLoadingCourse(true)
setError(null)
try {
const response = await api.get<Course>(`/api/courses/${courseId}`)
setCourse(response.data)
} catch (err: any) {
console.error("Failed to fetch course details:", err)
setError(err.response?.data?.message || "Failed to load course details.")
toast.error(err.response?.data?.message || "Failed to load course details.")
} finally {
setIsLoadingCourse(false)
}
if ((user || firebaseUser) && courseId) {
;(async () => {
setLoading(true)
setError(null)
try {
const resp = await api.get<Course>(`/api/courses/${courseId}?t=${Date.now()}`)
setCourse(resp.data)
setSelectedModuleIdx(0)
setSelectedLessonIdx(0)
setCompleted(false)
} catch {
setError("Failed to load course data.")
} finally {
setLoading(false)
}
})()
}
}, [authLoading, user, firebaseUser, courseId, router])
if (user || firebaseUser) {
fetchCourse()
// Helper: embed URL
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])
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>
)
// fallback (could already be an embed url or another provider)
return url
}
if (error) {
return (
<div className="flex justify-center items-center min-h-[calc(100vh-64px)] text-red-500">
<p>{error}</p>
</div>
)
const modules = course?.modules || []
// Pick first non-empty for fallback if nothing selected
const selModIdx = modules.length > 0 ? selectedModuleIdx : 0
const lessons = modules.length > 0 ? modules[selModIdx]?.lessons : []
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) {
return (
<div className="flex justify-center items-center min-h-[calc(100vh-64px)] text-gray-600 dark:text-gray-300">
<p className="text-xl">Course not found.</p>
</div>
)
function markComplete() {
setCompleted(true)
toast.success("Course Completed!")
}
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 (
<div className="flex flex-col md:flex-row min-h-[calc(100vh-64px)]">
<CourseSidebar
courseId={course.id}
modules={course.modules}
activeLessonId=""
/>
<div className="flex-1 p-4 md:p-8 overflow-y-auto">
<Card className="bg-white shadow-md rounded-lg p-6 dark:bg-gray-800 dark:text-gray-100">
<CardHeader>
<CardTitle className="text-2xl font-bold text-primary-purple">
{course.title} Overview
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-lg text-gray-700 dark:text-gray-200">
{course.description}
</p>
<p className="text-gray-600 dark:text-gray-300">
Select a lesson from the sidebar to begin.
</p>
</CardContent>
</Card>
</div>
<div className="flex flex-col md:flex-row gap-6 max-w-7xl mx-auto px-4 py-8">
{/* Sidebar: Always show all modules and lessons */}
<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">
<h2 className="text-lg font-bold mb-4 text-indigo-700">{course.title}</h2>
<p className="text-xs text-gray-500 mb-5">{course.description}</p>
{modules.length === 0 ? (
<div className="text-gray-500 italic py-6">No modules yet for this course.</div>
) : (
<ul>
{modules.map((mod, mIdx) => (
<li key={mod.id} className="mb-4">
<div
className={`font-semibold mb-2 cursor-pointer ${
mIdx === selModIdx ? "text-purple-600" : "text-gray-700"
}`}
onClick={() => { setSelectedModuleIdx(mIdx); setSelectedLessonIdx(0); }}
>
{mod.title}
</div>
<ul className="pl-4 border-l-2 border-gray-100">
{mod.lessons.map((lesson, lIdx) => (
<li
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>
)
}