diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/src/app/dashboard/courses/[id]/learn/page.tsx b/src/app/dashboard/courses/[id]/learn/page.tsx index 541ff31..e382b42 100644 --- a/src/app/dashboard/courses/[id]/learn/page.tsx +++ b/src/app/dashboard/courses/[id]/learn/page.tsx @@ -1,69 +1,30 @@ "use client"; -import { useEffect, useState, useRef } from "react"; -import { useParams } from "next/navigation"; -import Link from "next/link"; +import { useEffect, useState, useRef, useCallback, useMemo } from 'react'; +import { useParams } from 'next/navigation'; import { - CheckCircle2, - Circle, - ChevronDown, - ChevronRight, - Download, - Loader2, - ArrowLeft, - MessageSquare, - FileText, - Award, -} from "lucide-react"; -import { coursesApi, enrollmentsApi, lessonsApi } from "@/lib/api/services"; -import { formatDuration, cn } from "@/lib/utils"; -import type { Course, Enrollment, Lesson } from "@/types"; -import NotesPanel from "@/components/learn/NotesPanel"; -import CourseCompletionModal from "@/components/learn/CourseCompletionModal"; -import QuizComponent from "@/components/learn/QuizComponent"; -import { Breadcrumb } from "@/components/ui"; -import type { QuizQuestion } from "@/types"; - -type SidebarTab = "lessons" | "notes" | "qa"; - -const DEFAULT_QUIZ_QUESTIONS: QuizQuestion[] = [ - { - id: "default-1", - type: "multiple_choice", - question: "What does the Stellar network primarily do?", - options: [ - { id: "a", text: "Mine new coins", isCorrect: false }, - { - id: "b", - text: "Facilitate fast, low-cost cross-border payments", - isCorrect: true, - }, - { id: "c", text: "Host smart contracts only", isCorrect: false }, - ], - explanation: - "Stellar is a payment-focused blockchain network built for fast, low-cost transfers.", - }, - { - id: "default-2", - type: "multi_select", - question: "Which of the following are Stellar-native assets?", - options: [ - { id: "a", text: "XLM (Lumens)", isCorrect: true }, - { id: "b", text: "Issued credit assets", isCorrect: true }, - { id: "c", text: "Bitcoin", isCorrect: false }, - { id: "d", text: "Ether", isCorrect: false }, - ], - explanation: - "XLM and issued credit assets are native to Stellar; Bitcoin and Ether exist on other networks.", - }, - { - id: "default-3", - type: "true_false", - question: "Stellar transactions are finalized in seconds, not minutes.", - explanation: - "The Stellar network settles transactions in roughly 3-5 seconds.", - }, -]; + CheckCircle2, Circle, ChevronDown, ChevronRight, + Download, Loader2, ArrowLeft, Award, SkipForward, +} from 'lucide-react'; +import Link from 'next/link'; +import { coursesApi, enrollmentsApi, lessonsApi } from '@/lib/api/services'; +import { formatDuration, cn } from '@/lib/utils'; +import CourseCompletionModal from '@/components/learn/CourseCompletionModal'; +import { VideoPlayer } from '@/components/learn/VideoPlayer'; +import { AutoplayCountdown } from '@/components/learn/AutoplayCountdown'; +import type { Course, Enrollment, Lesson } from '@/types'; + +// ── Constants ────────────────────────────────────────────────────────────── +const AUTOPLAY_STORAGE_KEY = 'hamplard:autoplay'; + +// ── Helpers ──────────────────────────────────────────────────────────────── + +/** Returns the flat ordered list of all lessons across every module */ +function flatLessons(course: Course): Lesson[] { + return course.modules?.flatMap((m) => m.lessons) ?? []; +} + +// ── Component ────────────────────────────────────────────────────────────── export default function LearnPage() { const { id } = useParams<{ id: string }>(); @@ -77,47 +38,91 @@ export default function LearnPage() { const [sidebarTab, setSidebarTab] = useState("lessons"); const [showCompletionModal, setShowCompletionModal] = useState(false); - const videoRef = useRef(null); + // ── Autoplay state ─────────────────────────────────────────────────────── + const [autoplay, setAutoplay] = useState(() => { + if (typeof window === 'undefined') return true; + const stored = window.localStorage.getItem(AUTOPLAY_STORAGE_KEY); + return stored === null ? true : stored === 'true'; + }); + const [showCountdown, setShowCountdown] = useState(false); + const progressRef = useRef(null); const previousCompletedCountRef = useRef(null); const hasInitializedCompletionState = useRef(false); + // ── Derived: next lesson ───────────────────────────────────────────────── + const nextLesson = useMemo(() => { + if (!course || !activeLesson) return null; + const all = flatLessons(course); + const idx = all.findIndex((l) => l.id === activeLesson.id); + return idx >= 0 && idx < all.length - 1 ? all[idx + 1] : null; + }, [course, activeLesson]); + + // ── Persist autoplay preference ────────────────────────────────────────── + const handleAutoplayChange = useCallback((enabled: boolean) => { + setAutoplay(enabled); + if (typeof window !== 'undefined') { + window.localStorage.setItem(AUTOPLAY_STORAGE_KEY, String(enabled)); + } + // If the user turns autoplay off while the countdown is visible, cancel it + if (!enabled) setShowCountdown(false); + }, []); + + // ── Video ended handler ────────────────────────────────────────────────── + const handleVideoEnded = useCallback(() => { + if (autoplay && nextLesson) { + setShowCountdown(true); + } + // If autoplay is off or there's no next lesson the video just stops + }, [autoplay, nextLesson]); + + // ── Navigate to next lecture ───────────────────────────────────────────── + const goToNext = useCallback(() => { + if (!nextLesson) return; + setShowCountdown(false); + setActiveLesson(nextLesson); + }, [nextLesson]); + + const cancelCountdown = useCallback(() => { + setShowCountdown(false); + }, []); + + // ── Initial data load ──────────────────────────────────────────────────── useEffect(() => { - Promise.all([coursesApi.get(id), enrollmentsApi.get(id)]) - .then(([c, e]) => { - setCourse(c); - setEnrollment(e); - // Open first module by default - if (c.modules?.[0]) setExpanded({ [c.modules[0].id]: true }); - // Start from first incomplete lesson - const allLessons = c.modules?.flatMap((m) => m.lessons) ?? []; - const completedIds = new Set( - e.lessonProgress?.filter((p) => p.completed).map((p) => p.lessonId), - ); - const first = - allLessons.find((l) => !completedIds.has(l.id)) ?? allLessons[0]; - if (first) setActiveLesson(first); - }) - .catch(console.error) - .finally(() => setLoading(false)); - - return () => { - if (progressRef.current) clearInterval(progressRef.current); - }; + Promise.all([ + coursesApi.get(id), + enrollmentsApi.get(id), + ]).then(([c, e]) => { + setCourse(c); + setEnrollment(e); + // Open first module by default + if (c.modules?.[0]) setExpanded({ [c.modules[0].id]: true }); + // Start from first incomplete lesson + const allLessons = flatLessons(c); + const completedIds = new Set( + e.lessonProgress?.filter((p: { completed: boolean }) => p.completed).map((p: { lessonId: string }) => p.lessonId), + ); + const first = allLessons.find((l) => !completedIds.has(l.id)) ?? allLessons[0]; + if (first) setActiveLesson(first); + }) + .catch(console.error) + .finally(() => setLoading(false)); + + return () => { if (progressRef.current) clearInterval(progressRef.current); }; }, [id]); + // ── Course completion detection ────────────────────────────────────────── useEffect(() => { if (!course || !enrollment) return; - const totalLessons = course.modules?.flatMap((m) => m.lessons).length ?? 0; - const completedCount = - enrollment.lessonProgress?.filter((p) => p.completed).length ?? 0; + const totalLessons = flatLessons(course).length; + const completedCount = enrollment.lessonProgress?.filter((p: { completed: boolean }) => p.completed).length ?? 0; const isComplete = totalLessons > 0 && completedCount >= totalLessons; - const prevIncomplete = + const previouslyIncomplete = previousCompletedCountRef.current === null || previousCompletedCountRef.current < totalLessons; const newlyCompleted = - hasInitializedCompletionState.current && prevIncomplete && isComplete; + hasInitializedCompletionStateRef.current && previouslyIncomplete && isComplete; previousCompletedCountRef.current = completedCount; hasInitializedCompletionState.current = true; @@ -129,8 +134,8 @@ export default function LearnPage() { const storageKey = `course-completion:${course.id}`; const hasSeenModal = - typeof window !== "undefined" && - window.localStorage.getItem(storageKey) === "true"; + typeof window !== 'undefined' && + window.localStorage.getItem(storageKey) === 'true'; if (newlyCompleted && !hasSeenModal) { setShowCompletionModal(true); @@ -142,9 +147,15 @@ export default function LearnPage() { } }, [course, enrollment]); + // ── Dismiss countdown when lesson changes (e.g. sidebar click) ────────── + useEffect(() => { + setShowCountdown(false); + }, [activeLesson]); + + // ── Helpers ────────────────────────────────────────────────────────────── const isLessonCompleted = (lessonId: string) => enrollment?.lessonProgress?.some( - (p) => p.lessonId === lessonId && p.completed, + (p: { lessonId: string; completed: boolean }) => p.lessonId === lessonId && p.completed, ) ?? false; const handleMarkComplete = async () => { @@ -159,25 +170,16 @@ export default function LearnPage() { } }; - const totalLessons = course?.modules?.flatMap((m) => m.lessons).length ?? 0; - const completedCount = - enrollment?.lessonProgress?.filter((p) => p.completed).length ?? 0; - const progress = - totalLessons > 0 ? Math.round((completedCount / totalLessons) * 100) : 0; + const totalLessons = course ? flatLessons(course).length : 0; + const completedCount = enrollment?.lessonProgress?.filter((p: { completed: boolean }) => p.completed).length ?? 0; + const progress = totalLessons > 0 ? Math.round((completedCount / totalLessons) * 100) : 0; - const allLessons = course?.modules?.flatMap((m) => m.lessons) ?? []; - const currentIndex = allLessons.findIndex((l) => l.id === activeLesson?.id); - const nextLesson = - currentIndex >= 0 && currentIndex < allLessons.length - 1 - ? allLessons[currentIndex + 1] - : null; - - if (loading) - return ( -
- -
- ); + // ── Render ──────────────────────────────────────────────────────────────── + if (loading) return ( +
+ +
+ ); if (!course) return ( @@ -195,52 +197,38 @@ export default function LearnPage() { /> )} - {/* ÔöÇÔöÇ Left sidebar ÔöÇÔöÇ */} -