microtecflix/src/components/CourseView.tsx

411 lines
16 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { ArrowLeft, BookOpen, ChevronRight, FileText, CheckCircle2, Circle, MessageSquare, AlertCircle, Award } from 'lucide-react';
import VideoPlayer from './VideoPlayer';
import CourseActivity from './CourseActivity';
interface LessonProgress {
watchedPercentage: number;
completed: boolean;
}
interface Lesson {
id: string;
moduleId: string;
title: string;
videoUrl: string;
videoType: 'youtube' | 'direct';
order: number;
content: string;
progress: LessonProgress;
note: string;
}
interface Module {
id: string;
courseId: string;
title: string;
order: number;
lessons: Lesson[];
}
interface Course {
id: string;
title: string;
description: string;
thumbnail: string;
category: string;
}
interface CourseViewProps {
courseId: string;
token: string | null;
onBack: () => void;
}
export default function CourseView({ courseId, token, onBack }: CourseViewProps) {
const [course, setCourse] = useState<Course | null>(null);
const [modules, setModules] = useState<Module[]>([]);
const [activeLesson, setActiveLesson] = useState<Lesson | null>(null);
const [noteContent, setNoteContent] = useState('');
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
const [activeActivityModuleId, setActiveActivityModuleId] = useState<string | null>(null);
useEffect(() => {
fetchCourseDetails();
}, [courseId]);
const fetchCourseDetails = async () => {
setIsLoading(true);
setError(null);
try {
const res = await fetch(`/api/courses/${courseId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!res.ok) {
throw new Error('Falha ao carregar detalhes do curso');
}
const data = await res.json();
setCourse(data.course);
setModules(data.modules);
// Default to the first lesson of the first module
if (data.modules && data.modules.length > 0) {
const firstMod = data.modules[0];
if (firstMod.lessons && firstMod.lessons.length > 0) {
const firstLsn = firstMod.lessons[0];
setActiveLesson(firstLsn);
setNoteContent(firstLsn.note || '');
}
}
} catch (err: any) {
setError(err.message || 'Erro inesperado.');
} finally {
setIsLoading(false);
}
};
const handleSelectLesson = (lesson: Lesson) => {
setActiveLesson(lesson);
setNoteContent(lesson.note || '');
setSaveStatus('idle');
};
const handleProgressUpdate = async (watchedPercentage: number) => {
if (!activeLesson) return;
try {
const res = await fetch(`/api/lessons/${activeLesson.id}/progress`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ watchedPercentage })
});
if (res.ok) {
const data = await res.json();
// Update local state for lesson progress
setModules(prev => prev.map(mod => {
return {
...mod,
lessons: mod.lessons.map(lsn => {
if (lsn.id === activeLesson.id) {
return {
...lsn,
progress: {
watchedPercentage: Math.max(lsn.progress.watchedPercentage, watchedPercentage),
completed: data.completed || lsn.progress.completed
}
};
}
return lsn;
})
};
}));
// Also update the active lesson progress locally
setActiveLesson(prev => {
if (!prev) return null;
return {
...prev,
progress: {
watchedPercentage: Math.max(prev.progress.watchedPercentage, watchedPercentage),
completed: data.completed || prev.progress.completed
}
};
});
}
} catch (err) {
console.error('Error saving progress:', err);
}
};
const handleSaveNote = async () => {
if (!activeLesson) return;
setSaveStatus('saving');
try {
const res = await fetch(`/api/lessons/${activeLesson.id}/notes`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ content: noteContent })
});
if (res.ok) {
setSaveStatus('saved');
// Update local modules array so state matches
setModules(prev => prev.map(mod => {
return {
...mod,
lessons: mod.lessons.map(lsn => {
if (lsn.id === activeLesson.id) {
return { ...lsn, note: noteContent };
}
return lsn;
})
};
}));
setTimeout(() => setSaveStatus('idle'), 3000);
} else {
setSaveStatus('error');
}
} catch (err) {
setSaveStatus('error');
}
};
if (isLoading) {
return (
<div className="flex flex-col items-center justify-center min-h-[60vh] text-zinc-400 space-y-4">
<div className="w-12 h-12 border-4 border-[#E50914] border-t-transparent rounded-full animate-spin" />
<p className="text-sm font-semibold tracking-wide">Preparando seu ambiente de estudos...</p>
</div>
);
}
if (error || !course) {
return (
<div className="flex flex-col items-center justify-center min-h-[50vh] text-center max-w-md mx-auto p-4 space-y-4">
<AlertCircle className="w-12 h-12 text-[#E50914]" />
<h2 className="text-xl font-bold text-white">Oops! Algo deu errado</h2>
<p className="text-sm text-zinc-400">{error || 'Não foi possível carregar as informações deste curso.'}</p>
<button
onClick={onBack}
className="bg-zinc-800 hover:bg-zinc-700 text-white font-bold px-6 py-2 rounded-lg transition-colors flex items-center space-x-2"
>
<ArrowLeft className="w-4 h-4" />
<span>Voltar para cursos</span>
</button>
</div>
);
}
if (activeActivityModuleId) {
return (
<CourseActivity
token={token}
moduleId={activeActivityModuleId}
onBack={() => setActiveActivityModuleId(null)}
onCompleted={(passed) => {
if (passed) {
// Re-fetch course details to update any unlocked certificates if applicable
fetchCourseDetails();
}
}}
/>
);
}
return (
<div className="space-y-6">
{/* Back Header */}
<div className="flex items-center justify-between pb-4 border-b border-white/10">
<button
onClick={onBack}
className="group flex items-center space-x-2 text-zinc-400 hover:text-white transition-colors font-semibold text-sm cursor-pointer"
>
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
<span>Voltar ao Catálogo</span>
</button>
<span className="text-zinc-500 text-xs font-bold uppercase tracking-widest">{course.category}</span>
</div>
{/* Main Grid: Player on left, lessons sidebar on right */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Left Hand: Video & Notes */}
<div className="lg:col-span-2 space-y-6">
{activeLesson ? (
<div className="space-y-4">
<h2 className="text-xl md:text-2xl font-sans font-extrabold text-white tracking-tight">
{activeLesson.title}
</h2>
<VideoPlayer
videoUrl={activeLesson.videoUrl}
videoType={activeLesson.videoType}
lessonId={activeLesson.id}
onProgressUpdate={handleProgressUpdate}
initialProgress={activeLesson.progress?.watchedPercentage || 0}
/>
{/* Lesson Description Content */}
{activeLesson.content && (
<div className="glass-card p-5 rounded-xl space-y-2">
<h3 className="text-xs font-bold text-zinc-400 uppercase tracking-widest flex items-center space-x-1.5">
<FileText className="w-3.5 h-3.5 text-[#E50914]" />
<span>Recursos & Descrição da Aula</span>
</h3>
<p className="text-zinc-300 text-sm leading-relaxed whitespace-pre-line">
{activeLesson.content}
</p>
</div>
)}
{/* Notes & Doubts panel */}
<div className="glass-card p-5 rounded-xl space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-xs font-bold text-zinc-400 uppercase tracking-widest flex items-center space-x-1.5">
<MessageSquare className="w-3.5 h-3.5 text-[#E50914]" />
<span>Suas Anotações e Dúvidas</span>
</h3>
{saveStatus === 'saving' && <span className="text-[10px] text-zinc-500">Salvando...</span>}
{saveStatus === 'saved' && <span className="text-[10px] text-emerald-500 font-bold">Salvo com sucesso!</span>}
{saveStatus === 'error' && <span className="text-[10px] text-red-500 font-bold">Erro ao salvar.</span>}
</div>
<textarea
value={noteContent}
onChange={(e) => setNoteContent(e.target.value)}
placeholder="Escreva aqui suas anotações pessoais, dúvidas ou resumos sobre esta aula... Seus apontamentos são salvos automaticamente."
className="w-full h-32 glass-input rounded-lg p-3 text-sm text-zinc-100 placeholder-zinc-500 focus:outline-none resize-none"
/>
<div className="flex justify-end">
<button
onClick={handleSaveNote}
disabled={saveStatus === 'saving'}
className="glass-btn-secondary text-white font-bold text-xs px-4 py-2 rounded-lg cursor-pointer"
>
Salvar Anotações
</button>
</div>
</div>
</div>
) : (
<div className="h-[40vh] glass-card rounded-xl flex flex-col items-center justify-center text-zinc-500 text-center p-6">
<BookOpen className="w-12 h-12 text-zinc-700 mb-2" />
<p className="text-sm font-semibold text-zinc-400">Nenhuma aula cadastrada neste curso ainda.</p>
<p className="text-xs text-zinc-600 mt-1">O administrador adicionará conteúdo em breve!</p>
</div>
)}
</div>
{/* Right Hand: Interactive Modules Accordion */}
<div className="space-y-4">
<div className="glass-card p-4 rounded-xl space-y-1">
<h3 className="font-sans font-extrabold text-sm text-white tracking-tight">Conteúdo do Curso</h3>
<p className="text-xs text-zinc-500 font-medium">{course.title}</p>
</div>
<div className="space-y-3">
{modules.map((mod, modIdx) => (
<div
key={mod.id}
className="glass-card rounded-xl overflow-hidden"
>
{/* Module Title bar */}
<div className="glass-accordion-header px-4 py-3 flex items-center justify-between">
<div className="flex items-center space-x-2">
<span className="text-[#E50914] text-xs font-black">M{modIdx + 1}</span>
<h4 className="font-sans font-bold text-xs text-zinc-200 tracking-tight leading-snug line-clamp-1">
{mod.title}
</h4>
</div>
<span className="text-[10px] text-zinc-500 font-bold whitespace-nowrap">
{mod.lessons.length} {mod.lessons.length === 1 ? 'aula' : 'aulas'}
</span>
</div>
{/* Lessons list inside module */}
<div className="divide-y divide-white/5">
{mod.lessons.length === 0 ? (
<div className="p-3 text-xs text-zinc-600 text-center italic">Nenhuma aula neste módulo.</div>
) : (
mod.lessons.map(lesson => {
const isActive = activeLesson?.id === lesson.id;
const isLsnCompleted = lesson.progress?.completed;
return (
<button
key={lesson.id}
onClick={() => handleSelectLesson(lesson)}
className={`w-full px-4 py-3 text-left flex items-start space-x-3 transition-colors ${
isActive
? 'bg-[#E50914]/10 text-white'
: 'hover:bg-zinc-800/40 text-zinc-400 hover:text-zinc-200'
}`}
>
<div className="mt-0.5 flex-shrink-0">
{isLsnCompleted ? (
<CheckCircle2 className="w-4 h-4 text-emerald-500 fill-emerald-500/10" />
) : (
<Circle className="w-4 h-4 text-zinc-600" />
)}
</div>
<div className="space-y-0.5">
<span className={`font-medium text-xs leading-snug line-clamp-2 ${isActive ? 'text-[#E50914] font-bold' : ''}`}>
{lesson.title}
</span>
{lesson.progress?.watchedPercentage > 0 && !isLsnCompleted && (
<div className="flex items-center space-x-1.5 pt-1">
<div className="w-16 bg-zinc-800 h-1 rounded-full overflow-hidden">
<div
className="bg-amber-500 h-full"
style={{ width: `${lesson.progress.watchedPercentage}%` }}
/>
</div>
<span className="text-[9px] text-zinc-500 font-bold">{lesson.progress.watchedPercentage}%</span>
</div>
)}
</div>
</button>
);
})
)}
{/* Module Activity Button */}
{mod.lessons.length > 0 && (
<div className="p-3 border-t border-white/5 bg-black/20">
<button
onClick={() => setActiveActivityModuleId(mod.id)}
className="w-full flex items-center justify-center space-x-2 bg-zinc-800 hover:bg-[#E50914] hover:text-white text-zinc-300 transition-colors py-2.5 rounded-lg text-xs font-bold uppercase tracking-wider group"
>
<Award className="w-4 h-4 text-[#E50914] group-hover:text-white transition-colors" />
<span>Avaliação do Módulo</span>
</button>
</div>
)}
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
}