veza/apps/web/src/components/education/CourseLearningView.tsx

354 lines
13 KiB
TypeScript
Raw Normal View History

import React, { useState } from 'react';
import { Button } from '../ui/button';
import { ProgressBar } from '../ui/progress';
import { Course } from '../../types';
import {
ChevronLeft,
ChevronRight,
CheckCircle,
PlayCircle,
FileText,
HelpCircle,
Menu,
X,
} from 'lucide-react';
import { useToast } from '../../context/ToastContext';
import { QuizModal } from './modals/QuizModal';
import { CertificateModal } from './modals/CertificateModal';
interface CourseLearningViewProps {
course: Course;
onBack: () => void;
}
export const CourseLearningView: React.FC<CourseLearningViewProps> = ({
course,
onBack,
}) => {
const { addToast } = useToast();
const [activeLessonId, setActiveLessonId] = useState<string>(
course.modules?.[0]?.lessons[0]?.id || '',
);
const [completedLessons, setCompletedLessons] = useState<string[]>([]);
const [sidebarOpen, setSidebarOpen] = useState(true);
const [activeTab, setActiveTab] = useState<
'overview' | 'notes' | 'resources'
>('overview');
// Quiz State
const [showQuiz, setShowQuiz] = useState(false);
const [activeQuiz, setActiveQuiz] = useState<any>(null);
// Certificate State
const [showCertificate, setShowCertificate] = useState(false);
// Flattened lessons for navigation
const allLessons = course.modules?.flatMap((m) => m.lessons) || [];
const currentLessonIndex = allLessons.findIndex(
(l) => l.id === activeLessonId,
);
const currentLesson = allLessons[currentLessonIndex];
const handleNext = () => {
if (currentLessonIndex < allLessons.length - 1) {
const nextLesson = allLessons[currentLessonIndex + 1];
setActiveLessonId(nextLesson.id);
markComplete(currentLesson.id);
} else {
// Course finished
markComplete(currentLesson.id);
addToast('Course Completed! 🎉', 'success');
if (course.certificateAvailable) {
setShowCertificate(true);
}
}
};
const handlePrev = () => {
if (currentLessonIndex > 0) {
setActiveLessonId(allLessons[currentLessonIndex - 1].id);
}
};
const markComplete = (id: string) => {
if (!completedLessons.includes(id)) {
setCompletedLessons([...completedLessons, id]);
}
};
const startQuiz = (quizId: string) => {
// Mock Quiz Data
setActiveQuiz({
id: quizId,
title: 'Module Assessment',
passingScore: 70,
questions: [
{
id: 'q1',
question: 'What is the frequency range of a sub-bass?',
options: ['20-60Hz', '200-500Hz', '1-2kHz'],
correctIndex: 0,
},
{
id: 'q2',
question: 'Which plugin is best for sidechaining?',
options: ['Reverb', 'Compressor', 'Delay'],
correctIndex: 1,
},
],
});
setShowQuiz(true);
};
const progress = Math.round(
(completedLessons.length / allLessons.length) * 100,
);
return (
<div className="flex flex-col h-[calc(100vh-6rem)] -m-6 md:-m-10 bg-kodo-void">
{/* Header Bar */}
<div className="h-16 border-b border-kodo-steel bg-kodo-ink px-4 flex items-center justify-between shrink-0 z-20">
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" onClick={onBack}>
<ChevronLeft className="w-4 h-4 mr-1" /> Back
</Button>
<div className="h-6 w-px bg-kodo-steel"></div>
<h2 className="font-bold text-white text-sm md:text-base truncate max-w-md">
{course.title}
</h2>
</div>
<div className="flex items-center gap-4">
<div className="hidden md:block w-32">
<ProgressBar value={progress} color="lime" />
</div>
<div className="text-xs text-gray-400 font-mono hidden md:block">
{progress}% Complete
</div>
<Button
variant="ghost"
size="icon"
onClick={() => setSidebarOpen(!sidebarOpen)}
>
{sidebarOpen ? (
<X className="w-5 h-5" />
) : (
<Menu className="w-5 h-5" />
)}
</Button>
</div>
</div>
<div className="flex flex-1 overflow-hidden">
{/* Main Content Area */}
<div className="flex-1 flex flex-col min-w-0 overflow-y-auto custom-scrollbar">
{/* Player Stage */}
<div className="bg-black aspect-video w-full flex items-center justify-center relative">
{currentLesson?.type === 'video' ? (
<div className="text-center">
<PlayCircle className="w-16 h-16 text-white opacity-50 mx-auto mb-4" />
<p className="text-gray-500">Video Player Placeholder</p>
<p className="text-xs text-gray-600 mt-2">
{currentLesson.title}
</p>
</div>
) : currentLesson?.type === 'quiz' ? (
<div className="text-center">
<HelpCircle className="w-16 h-16 text-kodo-gold mx-auto mb-4" />
<h3 className="text-white text-xl font-bold mb-4">
Quiz: {currentLesson.title}
</h3>
<Button
variant="primary"
onClick={() =>
currentLesson.quizId && startQuiz(currentLesson.quizId)
}
>
Start Quiz
</Button>
</div>
) : (
<div className="p-8 max-w-2xl mx-auto text-left w-full h-full overflow-y-auto bg-kodo-graphite">
<h2 className="text-2xl font-bold text-white mb-4">
{currentLesson?.title}
</h2>
<p className="text-gray-300 leading-relaxed">
{currentLesson?.content ||
'This is a text-based lesson. Content would be rendered here in Markdown.'}
</p>
</div>
)}
</div>
{/* Tabs & Meta */}
<div className="p-6 md:p-8 max-w-5xl mx-auto w-full">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-white">
{currentLesson?.title}
</h1>
<div className="flex gap-2">
<Button
variant="secondary"
onClick={handlePrev}
disabled={currentLessonIndex === 0}
icon={<ChevronLeft className="w-4 h-4" />}
>
Prev
</Button>
<Button variant="primary" onClick={handleNext}>
{currentLessonIndex === allLessons.length - 1
? 'Finish Course'
: 'Next'}{' '}
<ChevronRight className="w-4 h-4 ml-1" />
</Button>
</div>
</div>
<div className="border-b border-kodo-steel flex gap-6 mb-6">
{['overview', 'notes', 'resources'].map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab as any)}
className={`pb-3 text-sm font-bold uppercase tracking-wider border-b-2 transition-colors ${activeTab === tab ? 'border-kodo-cyan text-white' : 'border-transparent text-gray-500 hover:text-gray-300'}`}
>
{tab}
</button>
))}
</div>
{activeTab === 'overview' && (
<div className="text-gray-300 space-y-4">
<p>
In this lesson, we cover the fundamentals of the topic. Make
sure to take notes.
</p>
<div className="p-4 bg-kodo-ink rounded border border-kodo-steel/50">
<h4 className="font-bold text-white mb-2">Key Takeaways</h4>
<ul className="list-disc pl-5 text-sm space-y-1">
<li>Understanding the core concept</li>
<li>Applying technique A to situation B</li>
<li>Common pitfalls to avoid</li>
</ul>
</div>
</div>
)}
{activeTab === 'notes' && (
<div>
<textarea
className="w-full h-40 bg-kodo-ink border border-kodo-steel rounded p-4 text-white resize-none focus:border-kodo-cyan outline-none"
placeholder="Type your personal notes here..."
/>
<Button variant="secondary" size="sm" className="mt-2">
Save Note
</Button>
</div>
)}
{activeTab === 'resources' && (
<div className="space-y-2">
<div className="flex items-center justify-between p-3 bg-kodo-ink rounded border border-kodo-steel">
<div className="flex items-center gap-3">
<FileText className="w-5 h-5 text-kodo-cyan" />
<span className="text-sm text-white">
Lesson Slides.pdf
</span>
</div>
<Button variant="ghost" size="sm">
Download
</Button>
</div>
<div className="flex items-center justify-between p-3 bg-kodo-ink rounded border border-kodo-steel">
<div className="flex items-center gap-3">
<FileText className="w-5 h-5 text-kodo-magenta" />
<span className="text-sm text-white">
Project Files.zip
</span>
</div>
<Button variant="ghost" size="sm">
Download
</Button>
</div>
</div>
)}
</div>
</div>
{/* Sidebar (Curriculum) */}
{sidebarOpen && (
<div className="w-80 bg-kodo-graphite border-l border-kodo-steel flex flex-col flex-shrink-0 animate-slideInRight">
<div className="p-4 border-b border-kodo-steel font-bold text-white text-sm bg-kodo-ink">
Course Content
</div>
<div className="flex-1 overflow-y-auto custom-scrollbar">
{course.modules?.map((module, i) => (
<div key={module.id} className="border-b border-kodo-steel/30">
<div className="p-4 bg-kodo-ink/50 text-xs font-bold text-gray-400 uppercase tracking-wider sticky top-0 backdrop-blur-sm z-10">
Section {i + 1}: {module.title}
</div>
<div>
{module.lessons.map((lesson) => {
const isActive = lesson.id === activeLessonId;
const isCompleted = completedLessons.includes(lesson.id);
return (
<div
key={lesson.id}
onClick={() => setActiveLessonId(lesson.id)}
className={`flex items-start gap-3 p-3 cursor-pointer border-l-2 transition-all hover:bg-white/5 ${isActive ? 'bg-kodo-cyan/10 border-kodo-cyan' : 'border-transparent'}`}
>
<div className="mt-0.5">
{isCompleted ? (
<CheckCircle className="w-4 h-4 text-kodo-lime" />
) : lesson.type === 'video' ? (
<PlayCircle
className={`w-4 h-4 ${isActive ? 'text-kodo-cyan' : 'text-gray-500'}`}
/>
) : (
<HelpCircle className="w-4 h-4 text-gray-500" />
)}
</div>
<div className="flex-1">
<div
className={`text-sm font-medium leading-snug ${isActive ? 'text-white' : 'text-gray-300'}`}
>
{lesson.title}
</div>
<div className="text-[10px] text-gray-500 mt-1 flex items-center gap-2">
<span>{lesson.duration}</span>
{lesson.type === 'quiz' && (
<span className="text-kodo-gold">Quiz</span>
)}
</div>
</div>
</div>
);
})}
</div>
</div>
))}
</div>
</div>
)}
</div>
{/* Modals */}
{showQuiz && activeQuiz && (
<QuizModal
quiz={activeQuiz}
onClose={() => setShowQuiz(false)}
onComplete={(score) => {
addToast(`Quiz Completed. Score: ${score}%`, 'info');
markComplete(currentLesson.id);
}}
/>
)}
{showCertificate && (
<CertificateModal
studentName="Cyber Producer"
courseName={course.title}
completionDate={new Date().toLocaleDateString()}
onClose={() => setShowCertificate(false)}
/>
)}
</div>
);
};