import React, { useState, useEffect, useRef } from 'react'; import { X, Send, User as UserIcon, Lightbulb, Paperclip, Lock, Unlock, ListVideo, Play, Download, Sparkles, MessageSquare, CheckCircle2, Check, Share2, FileText } from 'lucide-react'; import { Course, Comment, Lesson, Attachment } from '../types'; import { useAuth } from '../context/AuthContext'; import { getComments, getLessonProgress, saveComment, saveLessonCompletion, saveLessonProgress, incrementViews } from '../services/db'; import { LoginModal } from './LoginModal'; import { MaterialCard } from './MaterialCard'; import { courseApi, LessonPlayback } from '../services/api'; interface CoursePlayerModalProps { course: Course | null; onClose: () => void; } export const CoursePlayerModal: React.FC = ({ course, onClose }) => { const { user } = useAuth(); const [comments, setComments] = useState([]); const [newComment, setNewComment] = useState(''); const [currentLesson, setCurrentLesson] = useState(null); const [completedLessonIds, setCompletedLessonIds] = useState([]); const [watchedSecondsByLesson, setWatchedSecondsByLesson] = useState>({}); const [showLoginPrompt, setShowLoginPrompt] = useState(false); const [activeTab, setActiveTab] = useState<'playlist' | 'materials' | 'discussion'>('playlist'); const [copiedLink, setCopiedLink] = useState(false); const [playback, setPlayback] = useState(null); const [playbackError, setPlaybackError] = useState(''); const videoRef = useRef(null); const lastProgressSave = useRef>({}); // Load course data and this learner's saved state from PostgreSQL. useEffect(() => { if (course) { incrementViews(course.id); setCompletedLessonIds([]); setWatchedSecondsByLesson({}); lastProgressSave.current = {}; if (user) { getComments(course.id).then(setComments).catch(() => setComments([])); getLessonProgress(course.id).then((progress) => { const completed = progress.filter((item) => item.completedAt).map((item) => item.lessonId); setCompletedLessonIds(completed); setWatchedSecondsByLesson(Object.fromEntries(progress.map((item) => [item.lessonId, item.watchedSeconds]))); lastProgressSave.current = Object.fromEntries(progress.map((item) => [item.lessonId, item.watchedSeconds])); const nextIncomplete = course.lessons.find((lesson) => !completed.includes(lesson.id)); if (nextIncomplete) setCurrentLesson(nextIncomplete); }).catch(() => setCompletedLessonIds([])); } else { setComments([]); } // Select first lesson by default if (course.lessons && course.lessons.length > 0) { setCurrentLesson(course.lessons[0]); } } }, [course, user]); // The API verifies the session before returning a time-limited Bunny embed // URL. A video provider key is never sent to the browser. useEffect(() => { if (!course || !user || !currentLesson) { setPlayback(null); setPlaybackError(''); return; } let cancelled = false; setPlayback(null); setPlaybackError(''); courseApi.playback(course.id, currentLesson.id) .then((response) => { if (!cancelled) setPlayback(response.data); }) .catch((error) => { if (!cancelled) setPlaybackError(error instanceof Error ? error.message : 'Não foi possível carregar este vídeo.'); }); return () => { cancelled = true; }; }, [course, user, currentLesson]); // Persist completed lessons and recalculate progress const updateCompletedLessons = (newCompleted: string[]) => { setCompletedLessonIds(newCompleted); if (course) { } }; const toggleLessonCompleted = (lessonId: string) => { if (!user) { setShowLoginPrompt(true); return; } if (completedLessonIds.includes(lessonId)) { const updated = completedLessonIds.filter(id => id !== lessonId); updateCompletedLessons(updated); saveLessonCompletion(lessonId, false).catch(() => updateCompletedLessons(completedLessonIds)); } else { markLessonCompleted(lessonId); } }; const markLessonCompleted = (lessonId: string, watchedSeconds?: number) => { if (!user || completedLessonIds.includes(lessonId)) return; const previous = completedLessonIds; updateCompletedLessons([...previous, lessonId]); saveLessonCompletion(lessonId, true, watchedSeconds).catch(() => updateCompletedLessons(previous)); }; // Video finished automatically -> mark lesson as completed const handleVideoEnded = () => { if (currentLesson && !completedLessonIds.includes(currentLesson.id)) { markLessonCompleted(currentLesson.id, Math.round(videoRef.current?.duration || 0)); } // Auto-advance to next lesson if available if (course?.lessons && currentLesson) { const currentIndex = course.lessons.findIndex(l => l.id === currentLesson.id); if (currentIndex >= 0 && currentIndex < course.lessons.length - 1) { const nextLesson = course.lessons[currentIndex + 1]; if (user) { setTimeout(() => { setCurrentLesson(nextLesson); }, 1000); } } } }; // Track video progress const handleTimeUpdate = () => { if (videoRef.current) { const { currentTime, duration } = videoRef.current; if (duration > 0) { if (user && currentLesson && currentTime - (lastProgressSave.current[currentLesson.id] || 0) >= 30) { const watchedSeconds = Math.floor(currentTime); lastProgressSave.current[currentLesson.id] = watchedSeconds; setWatchedSecondsByLesson((current) => ({ ...current, [currentLesson.id]: watchedSeconds })); saveLessonProgress(currentLesson.id, watchedSeconds).catch(() => undefined); } const percent = Math.round((currentTime / duration) * 100); // Automatically mark as complete when reaching 95%+ if (percent >= 95 && currentLesson && !completedLessonIds.includes(currentLesson.id)) { markLessonCompleted(currentLesson.id, Math.round(currentTime)); } } } }; const restoreWatchPosition = () => { if (!currentLesson || !videoRef.current) return; const watchedSeconds = watchedSecondsByLesson[currentLesson.id] || 0; if (watchedSeconds > 0 && watchedSeconds < videoRef.current.duration - 5) videoRef.current.currentTime = watchedSeconds; }; const handleAddComment = async (e: React.FormEvent) => { e.preventDefault(); if (!newComment.trim() || !course) return; const comment = { courseId: course.id, lessonId: currentLesson?.id, text: newComment, }; try { const savedComment = await saveComment(comment); setComments([savedComment, ...comments]); setNewComment(''); } catch { // The server remains the source of truth; leave the text intact for a retry. } }; const changeLesson = (lesson: Lesson) => { setCurrentLesson(lesson); }; const handleShare = () => { navigator.clipboard.writeText(window.location.href); setCopiedLink(true); setTimeout(() => setCopiedLink(false), 2500); }; // Collect all downloadable materials for this course (Global + All Lessons) const allCourseAttachments: Array<{ attachment: Attachment; source: string }> = []; if (course?.attachments) { course.attachments.forEach(att => { allCourseAttachments.push({ attachment: att, source: 'Material Geral do Curso' }); }); } if (course?.lessons) { course.lessons.forEach(l => { if (l.attachments) { l.attachments.forEach(att => { allCourseAttachments.push({ attachment: att, source: l.title }); }); } }); } const currentLessonAttachments = currentLesson?.attachments || []; // Access Control Logic const canWatch = currentLesson && Boolean(user); if (!course) return null; return (
{/* Top Bar Floating Controls */}
{course.title}
{/* Main Left Column: Video & Lesson Details */}
{/* Video Player Section */}
{canWatch ? ( playback ? ( playback.kind === 'embed' && playback.embedUrl ? (