Files
compor-academy/components/CoursePlayerModal.tsx
Cauê Faleiros 9b6fea4b81
All checks were successful
CI / Validate frontend and API (push) Successful in 1m58s
CI / Build and publish Docker images (push) Successful in 22s
feat: complete platform operations roadmap
2026-09-04 10:07:07 -03:00

683 lines
32 KiB
TypeScript

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';
declare global {
interface Window {
playerjs?: {
Player: new (element: HTMLIFrameElement) => {
on: (event: string, listener: (data?: { seconds?: number; duration?: number }) => void) => void;
};
};
}
}
let bunnyPlayerJsPromise: Promise<void> | null = null;
const loadBunnyPlayerJs = () => {
if (window.playerjs) return Promise.resolve();
if (bunnyPlayerJsPromise) return bunnyPlayerJsPromise;
bunnyPlayerJsPromise = new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://assets.mediadelivery.net/playerjs/player-0.1.0.min.js';
script.async = true;
script.onload = () => resolve();
script.onerror = () => reject(new Error('Bunny player events could not be loaded'));
document.head.appendChild(script);
});
return bunnyPlayerJsPromise;
};
interface CoursePlayerModalProps {
course: Course | null;
onClose: () => void;
}
export const CoursePlayerModal: React.FC<CoursePlayerModalProps> = ({ course, onClose }) => {
const { user } = useAuth();
const [comments, setComments] = useState<Comment[]>([]);
const [newComment, setNewComment] = useState('');
const [currentLesson, setCurrentLesson] = useState<Lesson | null>(null);
const [completedLessonIds, setCompletedLessonIds] = useState<string[]>([]);
const [watchedSecondsByLesson, setWatchedSecondsByLesson] = useState<Record<string, number>>({});
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
const [activeTab, setActiveTab] = useState<'playlist' | 'materials' | 'discussion'>('playlist');
const [copiedLink, setCopiedLink] = useState(false);
const [playback, setPlayback] = useState<LessonPlayback | null>(null);
const [playbackError, setPlaybackError] = useState('');
const videoRef = useRef<HTMLVideoElement>(null);
const bunnyFrameRef = useRef<HTMLIFrameElement>(null);
const lastProgressSave = useRef<Record<string, number>>({});
// 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));
}
}
}
};
// Bunny's iframe does not expose an HTMLVideoElement. Player.js receives
// Bunny's trusted playback events and sends the same progress updates used
// for external video providers.
useEffect(() => {
if (!playback || playback.kind !== 'embed' || !currentLesson || !user) return;
let disposed = false;
const lessonId = currentLesson.id;
void loadBunnyPlayerJs().then(() => {
if (disposed || !bunnyFrameRef.current || !window.playerjs) return;
const player = new window.playerjs.Player(bunnyFrameRef.current);
player.on('timeupdate', (data) => {
const watchedSeconds = Math.floor(data?.seconds || 0);
if (disposed || watchedSeconds <= 0 || watchedSeconds - (lastProgressSave.current[lessonId] || 0) < 30) return;
lastProgressSave.current[lessonId] = watchedSeconds;
setWatchedSecondsByLesson((current) => ({ ...current, [lessonId]: watchedSeconds }));
saveLessonProgress(lessonId, watchedSeconds).catch(() => undefined);
});
player.on('ended', (data) => {
if (disposed) return;
markLessonCompleted(lessonId, Math.floor(data?.duration || data?.seconds || 0));
});
}).catch(() => undefined);
return () => { disposed = true; };
}, [playback, currentLesson, user, completedLessonIds]);
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 (
<div className="fixed inset-0 z-[100] bg-[#09090b] text-white overflow-y-auto animate-fade-in-up">
{/* Top Bar Floating Controls */}
<div className="fixed top-0 left-0 right-0 p-4 md:p-6 z-30 flex items-center justify-between bg-gradient-to-b from-black/90 via-black/40 to-transparent pointer-events-none">
<div className="flex items-center gap-3 pointer-events-auto bg-black/60 backdrop-blur-md px-4 py-2 rounded-[980px] border border-white/10">
<span className="w-2 h-2 rounded-[980px] bg-orange-500 animate-pulse"></span>
<span className="text-caption font-semibold tracking-caption text-gray-200 truncate max-w-[200px] md:max-w-md">
{course.title}
</span>
</div>
<div className="flex items-center gap-3 pointer-events-auto">
<button
onClick={handleShare}
className="flex items-center gap-2 bg-white/10 hover:bg-white/20 backdrop-blur-md text-white text-body-sm font-semibold tracking-body-sm px-4 py-2.5 rounded-[980px] transition-all active:scale-95 shadow-xl"
title="Copiar Link"
>
<Share2 className="w-4 h-4 text-orange-400" />
<span className="hidden sm:inline">{copiedLink ? 'Link Copiado!' : 'Compartilhar'}</span>
</button>
<button
onClick={onClose}
className="bg-white/10 hover:bg-white/20 backdrop-blur-md p-2.5 rounded-[980px] transition-all active:scale-95 text-white shadow-xl"
title="Fechar"
>
<X className="w-5 h-5" />
</button>
</div>
</div>
<div className="max-w-[1440px] mx-auto min-h-screen pt-16 md:pt-20 pb-20">
<div className="flex flex-col lg:flex-row gap-8 px-4 md:px-8">
{/* Main Left Column: Video & Lesson Details */}
<div className="flex-1 lg:max-w-[65%] xl:max-w-[68%]">
{/* Video Player Section */}
<div className="w-full aspect-video bg-black rounded-[8px] overflow-hidden relative shadow-xl border border-white/10 group">
{canWatch ? (
playback ? (
playback.kind === 'embed' && playback.embedUrl ? (
<iframe
key={currentLesson?.id}
ref={bunnyFrameRef}
src={playback.embedUrl}
title={currentLesson?.title || 'Vídeo da aula'}
className="w-full h-full border-0 rounded-[8px]"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; picture-in-picture; fullscreen"
allowFullScreen
/>
) : playback.source ? (
<video
key={currentLesson?.id}
ref={videoRef}
src={playback.source}
className="w-full h-full object-contain rounded-[8px]"
controls
autoPlay
onTimeUpdate={handleTimeUpdate}
onEnded={handleVideoEnded}
onLoadedMetadata={restoreWatchPosition}
>
Seu navegador não suporta a tag de vídeo.
</video>
) : (
<div className="absolute inset-0 flex items-center justify-center text-sm text-gray-400">Vídeo indisponível.</div>
)
) : (
<div className="absolute inset-0 flex flex-col items-center justify-center text-center p-6 text-sm text-gray-400">
{playbackError ? <span className="text-amber-300">{playbackError}</span> : <span>Carregando vídeo seguro...</span>}
</div>
)
) : (
// Locked State
<div className="absolute inset-0 flex flex-col items-center justify-center bg-zinc-950/90 backdrop-blur-md z-10 text-center p-6 rounded-[8px]">
<div className="bg-orange-500/10 border border-orange-500/20 p-4 rounded-[8px] mb-4 text-orange-400">
<Lock className="w-8 h-8" />
</div>
<h2 className="font-display text-subheading md:text-heading-sm font-bold tracking-heading-sm leading-heading-sm mb-2">Aula Exclusiva (Pro)</h2>
<p className="text-body-sm font-normal text-gray-400 mb-6 max-w-md leading-body-sm tracking-body-sm">
Esta aula e seus materiais de download complementares estão disponíveis para membros. Faça login para continuar assistindo.
</p>
<button
onClick={() => setShowLoginPrompt(true)}
className="bg-orange-500 hover:bg-orange-600 text-white text-body-sm font-semibold tracking-body-sm px-8 py-3 rounded-[980px] transition-all shadow-xl shadow-orange-500/25 active:scale-95"
>
Fazer Login para Acessar
</button>
</div>
)}
</div>
{/* Current Lesson Header Info */}
<div className="mt-6 mb-8">
<div className="flex flex-wrap items-center justify-between gap-3 mb-3">
<div className="flex items-center gap-2.5 text-caption font-semibold uppercase tracking-caption text-orange-400">
<span>{course.category}</span>
<span className="w-1 h-1 rounded-[980px] bg-zinc-700"></span>
<span className="text-gray-400">{currentLesson?.duration || '15m'}</span>
</div>
<div className="flex items-center gap-2">
{/* Mark as completed toggle button */}
{currentLesson && (
<button
onClick={() => toggleLessonCompleted(currentLesson.id)}
className={`inline-flex items-center gap-1.5 text-xs font-semibold px-3.5 py-1.5 rounded-[980px] transition-all active:scale-95 ${
completedLessonIds.includes(currentLesson.id)
? 'bg-emerald-500/20 text-emerald-400 hover:bg-emerald-500/30 border border-emerald-500/30 font-bold'
: 'bg-white/10 hover:bg-emerald-500/20 hover:text-emerald-300 text-gray-300'
}`}
>
<CheckCircle2 className="w-3.5 h-3.5" />
<span>{completedLessonIds.includes(currentLesson.id) ? 'Aula Concluída' : 'Concluir Aula'}</span>
</button>
)}
{/* Direct Button to Materials Tab if Available */}
{allCourseAttachments.length > 0 && (
<button
onClick={() => setActiveTab('materials')}
className="inline-flex items-center gap-2 text-caption font-semibold tracking-caption bg-orange-500/10 hover:bg-orange-500/20 text-orange-400 border border-orange-500/20 px-3.5 py-1.5 rounded-[980px] transition-all"
>
<Download className="w-3.5 h-3.5" />
<span>{allCourseAttachments.length} {allCourseAttachments.length === 1 ? 'Material' : 'Materiais'}</span>
</button>
)}
</div>
</div>
<h1 className="font-display text-subheading sm:text-heading-sm md:text-heading font-bold tracking-heading leading-heading text-white mb-3">
{currentLesson?.title || course.title}
</h1>
<p className="text-body-sm sm:text-body text-gray-300 leading-body tracking-body font-normal mb-6">
{currentLesson?.description || course.description}
</p>
{/* Lesson Specific Quick Downloads (if present) */}
{currentLessonAttachments.length > 0 && (
<div className="p-6 rounded-[8px] bg-zinc-900/80 border border-orange-500/20 mb-8 space-y-4 shadow-xl">
<div className="flex items-center justify-between">
<h4 className="text-caption font-semibold uppercase tracking-caption text-orange-400 flex items-center gap-2">
<Download className="w-4 h-4" />
Downloads Desta Aula
</h4>
<span className="text-caption font-mono text-gray-400">
{currentLessonAttachments.length} {currentLessonAttachments.length === 1 ? 'arquivo' : 'arquivos'}
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{currentLessonAttachments.map(att => (
<MaterialCard key={att.id} attachment={att} compact />
))}
</div>
</div>
)}
{/* Instructor Key Takeaways / Tips */}
{course.tips && course.tips.length > 0 && (
<div className="bg-gradient-to-br from-zinc-900 to-black border border-white/10 rounded-[8px] p-6 mb-8 shadow-xl">
<h3 className="font-display text-subheading font-bold text-white mb-4 flex items-center gap-2.5 tracking-subheading leading-subheading">
<Lightbulb className="w-5 h-5 text-amber-400" />
Recomendações Práticas do Instrutor
</h3>
<ul className="space-y-3">
{course.tips.map((tip, i) => (
<li key={i} className="flex gap-3 text-gray-300 text-body-sm font-normal leading-body-sm tracking-body-sm">
<span className="block w-2 h-2 rounded-[980px] bg-orange-500 mt-2 shrink-0"></span>
<span>{tip}</span>
</li>
))}
</ul>
</div>
)}
</div>
</div>
{/* Right Sidebar: Tabs for Playlist, Materials, and Discussion */}
<div className="lg:w-[35%] xl:w-[32%] shrink-0 flex flex-col space-y-6">
{/* Tab Navigation */}
<div className="flex items-center justify-between bg-zinc-950/90 p-1.5 rounded-[980px] border border-white/10 shadow-xl gap-1">
<button
onClick={() => setActiveTab('playlist')}
className={`flex-1 flex items-center justify-center gap-1.5 py-2 px-3 sm:px-3.5 rounded-[980px] text-xs font-semibold whitespace-nowrap transition-all ${
activeTab === 'playlist'
? 'bg-orange-500 text-white shadow-md shadow-orange-500/25'
: 'text-gray-400 hover:text-white hover:bg-white/5'
}`}
>
<ListVideo className="w-3.5 h-3.5 shrink-0" />
<span>Aulas ({course.lessons.length})</span>
</button>
<button
onClick={() => setActiveTab('materials')}
className={`flex-1 flex items-center justify-center gap-1.5 py-2 px-3 sm:px-3.5 rounded-[980px] text-xs font-semibold whitespace-nowrap transition-all ${
activeTab === 'materials'
? 'bg-orange-500 text-white shadow-md shadow-orange-500/25'
: 'text-gray-400 hover:text-white hover:bg-white/5'
}`}
>
<Download className="w-3.5 h-3.5 shrink-0" />
<span>Downloads ({allCourseAttachments.length})</span>
</button>
<button
onClick={() => setActiveTab('discussion')}
className={`flex-1 flex items-center justify-center gap-1.5 py-2 px-3 sm:px-3.5 rounded-[980px] text-xs font-semibold whitespace-nowrap transition-all ${
activeTab === 'discussion'
? 'bg-orange-500 text-white shadow-md shadow-orange-500/25'
: 'text-gray-400 hover:text-white hover:bg-white/5'
}`}
>
<MessageSquare className="w-3.5 h-3.5 shrink-0" />
<span>Dúvidas ({comments.length})</span>
</button>
</div>
{/* TAB CONTENT: Playlist */}
{activeTab === 'playlist' && (
<div className="bg-zinc-950/80 rounded-2xl p-5 space-y-3 shadow-xl">
<div className="flex items-center justify-between pb-3 border-b border-white/5">
<span className="text-xs font-bold uppercase tracking-wider text-gray-400">Trilha de Aulas</span>
<span className="text-xs text-orange-400 font-mono font-bold">{course.duration}</span>
</div>
<div className="space-y-2 max-h-[600px] overflow-y-auto pr-1">
{course.lessons.map((lesson, index) => {
const isActive = currentLesson?.id === lesson.id;
const isCompleted = completedLessonIds.includes(lesson.id);
const isLocked = !user;
const hasAttachments = lesson.attachments && lesson.attachments.length > 0;
return (
<div
key={lesson.id}
onClick={() => changeLesson(lesson)}
className={`
group flex items-start gap-3.5 p-3.5 rounded-xl cursor-pointer transition-all border
${isActive
? 'bg-orange-500/15 border-orange-500/40 text-orange-400 shadow-md shadow-orange-500/10'
: isCompleted
? 'bg-emerald-950/40 hover:bg-emerald-900/50 border-emerald-500/30 text-emerald-300 shadow-sm'
: 'bg-zinc-900/80 hover:bg-zinc-800/90 border-transparent text-gray-200'
}
${isLocked ? 'opacity-70' : 'opacity-100'}
`}
>
{/* Status Icon */}
<div className="mt-0.5 shrink-0">
{isActive ? (
<div className="p-2 rounded-lg bg-orange-500 text-white shadow-md">
<Play className="w-3.5 h-3.5 fill-white" />
</div>
) : isCompleted ? (
<div className="p-2 rounded-lg bg-emerald-500 text-white shadow-md">
<Check className="w-3.5 h-3.5 stroke-[3]" />
</div>
) : isLocked ? (
<div className="p-2 rounded-lg bg-white/5 text-gray-400">
<Lock className="w-3.5 h-3.5" />
</div>
) : (
<div className="p-2 rounded-lg bg-white/5 text-gray-400 font-mono text-xs font-bold w-8 h-8 flex items-center justify-center">
{index + 1}
</div>
)}
</div>
<div className="flex-1 min-w-0">
<h4 className={`text-xs sm:text-sm font-semibold truncate ${
isActive
? 'text-orange-400 font-bold'
: isCompleted
? 'text-emerald-300 group-hover:text-emerald-200 font-semibold'
: 'text-gray-200 group-hover:text-white'
}`}>
{lesson.title}
</h4>
<div className="flex items-center gap-2 text-xs text-gray-400 mt-1 font-normal">
<span>{lesson.duration}</span>
{isCompleted && (
<span className="bg-emerald-500/20 text-emerald-400 px-2 py-0.5 rounded-md text-[10px] font-bold uppercase flex items-center gap-1">
<CheckCircle2 className="w-2.5 h-2.5" /> Concluída
</span>
)}
{lesson.isFree ? (
<span className="bg-emerald-500/15 text-emerald-400 px-2 py-0.5 rounded-md text-[10px] font-bold uppercase">Grátis</span>
) : (
<span className="bg-purple-500/15 text-purple-400 px-2 py-0.5 rounded-md text-[10px] font-bold uppercase">Exclusiva</span>
)}
{hasAttachments && (
<span className="flex items-center gap-1 text-[10px] text-orange-400 bg-orange-500/15 px-2 py-0.5 rounded-md font-bold">
<Paperclip className="w-2.5 h-2.5" />
{lesson.attachments!.length} anexo(s)
</span>
)}
</div>
</div>
</div>
);
})}
</div>
</div>
)}
{/* TAB CONTENT: Materials & Downloads */}
{activeTab === 'materials' && (
<div className="bg-zinc-950/80 rounded-2xl p-5 space-y-4 shadow-xl">
<div className="flex items-center justify-between pb-3 border-b border-white/5">
<div>
<h3 className="text-sm font-bold text-white">Materiais para Download</h3>
<p className="text-xs text-gray-400 mt-0.5">Arquivos anexados a este curso</p>
</div>
<span className="text-xs bg-orange-500/15 text-orange-400 font-bold px-2.5 py-1 rounded-full">
{allCourseAttachments.length} Total
</span>
</div>
{allCourseAttachments.length === 0 ? (
<div className="py-12 text-center text-gray-500">
<Download className="w-8 h-8 mx-auto mb-2 opacity-30 text-orange-400" />
<p className="text-xs text-gray-400">Nenhum material cadastrado para este curso ainda.</p>
</div>
) : (
<div className="space-y-3 max-h-[600px] overflow-y-auto pr-1">
{allCourseAttachments.map((item, idx) => (
<div key={`${item.attachment.id}-${idx}`} className="space-y-1.5">
<span className="text-xs text-gray-400 font-semibold px-1 flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-orange-500"></span>
{item.source}
</span>
<MaterialCard attachment={item.attachment} compact />
</div>
))}
</div>
)}
</div>
)}
{/* TAB CONTENT: Discussion */}
{activeTab === 'discussion' && (
<div className="bg-zinc-950/80 rounded-2xl p-5 flex flex-col space-y-4 shadow-xl">
<h3 className="text-sm font-bold text-white pb-2 border-b border-white/5">Dúvidas & Comentários</h3>
{/* Comment Form */}
<form onSubmit={handleAddComment} className="flex gap-2">
<input
type="text"
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
placeholder={user ? "Faça uma pergunta sobre a aula..." : "Faça login para comentar"}
disabled={!user}
className="flex-1 bg-zinc-900/90 rounded-xl px-3.5 py-2.5 text-xs text-white placeholder:text-gray-500 focus:outline-none focus:ring-1 focus:ring-orange-500"
/>
<button
type="submit"
disabled={!newComment.trim() || !user}
className="p-2.5 rounded-xl bg-orange-500 hover:bg-orange-600 disabled:opacity-30 text-white transition-colors"
>
<Send className="w-4 h-4" />
</button>
</form>
{/* Comments List */}
<div className="space-y-3 max-h-[450px] overflow-y-auto pr-1">
{comments.length === 0 ? (
<p className="text-xs text-gray-500 text-center py-8">Nenhum comentário ainda. Seja o primeiro a perguntar!</p>
) : (
comments.map(c => (
<div key={c.id} className="p-3 rounded-xl bg-zinc-900/90 space-y-1">
<div className="flex items-center justify-between text-xs font-semibold">
<span className="text-gray-200">{c.user}</span>
<span className="text-gray-500 font-mono text-[11px]">{c.date}</span>
</div>
<p className="text-xs text-gray-300 font-normal leading-relaxed">{c.text}</p>
{c.reply && (
<div className="mt-2 pl-3 border-l-2 border-orange-500 text-xs bg-white/[0.02] p-2 rounded-r-lg">
<span className="font-bold text-orange-400 uppercase text-[10px]">Instrutor:</span>
<p className="text-gray-300 text-xs mt-0.5">{c.reply}</p>
</div>
)}
</div>
))
)}
</div>
</div>
)}
</div>
</div>
</div>
<LoginModal isOpen={showLoginPrompt} onClose={() => setShowLoginPrompt(false)} />
</div>
);
};