import React, { useState, useEffect, useRef } from 'react'; import { Plus, Settings, Users, BarChart, Trash2, Edit2, X, MessageSquare, Send, Image as ImageIcon, Minus, PlusCircle, Video, Loader2, FileText, FileSpreadsheet, FileArchive, Link as LinkIcon, Paperclip, Lightbulb, ListVideo, Check, Lock, Unlock, Globe, Download, Copy, ChevronUp, ChevronDown, ChevronLeft, ChevronRight, Move, RotateCcw, } from 'lucide-react'; import { Course, Comment, Attachment, Lesson, AttachmentType, LessonMedia } from '../types'; import { useToast } from '../context/ToastContext'; import { getManagedCourses, saveCourse, deleteCourse, duplicateCourse, getComments, replyToComment, moderateComment } from '../services/db'; import { getAttachmentIcon, getAttachmentBadge } from '../components/MaterialCard'; import { CourseCoverImage } from '../components/CourseCoverImage'; import { BunnyVideo, courseApi, instructorApi, InstructorAnalytics } from '../services/api'; import { useSearchParams } from 'react-router-dom'; const formatVideoDuration = (seconds: number | null | undefined) => { if (!seconds || seconds < 1) return 'Processando'; const totalSeconds = Math.round(seconds); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const remainingSeconds = totalSeconds % 60; if (hours) return `${hours}h ${String(minutes).padStart(2, '0')}min`; if (minutes) return `${minutes}min`; return `${remainingSeconds}s`; }; const totalVideoDuration = (lessons: Lesson[]) => { const seconds = lessons.reduce((total, lesson) => total + (lesson.media?.[0]?.durationSeconds || 0), 0); return seconds ? formatVideoDuration(seconds) : 'Em processamento'; }; // 1. Create/Edit Course Modal const CourseModal: React.FC<{ isOpen: boolean; onClose: () => void; onSave: (course: Course) => Promise; initialData?: Course; }> = ({ isOpen, onClose, onSave, initialData }) => { const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); const [category, setCategory] = useState(''); const [status, setStatus] = useState('draft'); const [thumbnailPreview, setThumbnailPreview] = useState(''); const [coverImageUrl, setCoverImageUrl] = useState(''); const [coverImageZoom, setCoverImageZoom] = useState(1); const [coverImagePositionX, setCoverImagePositionX] = useState(50); const [coverImagePositionY, setCoverImagePositionY] = useState(50); const [isAdjustingCoverPosition, setIsAdjustingCoverPosition] = useState(false); const [coverPreviewError, setCoverPreviewError] = useState(false); const coverFileRef = useRef(null); const localCoverPreviewRef = useRef(null); const coverDragStartRef = useRef<{ pointerX: number; pointerY: number; positionX: number; positionY: number } | null>(null); const [categories, setCategories] = useState([]); const [coverConfigured, setCoverConfigured] = useState(null); const [coverUploadMessage, setCoverUploadMessage] = useState(''); const [isCoverUploading, setIsCoverUploading] = useState(false); const attachmentFileRef = useRef(null); const [assetConfigured, setAssetConfigured] = useState(null); const [isAssetUploading, setIsAssetUploading] = useState(false); const [assetUploadMessage, setAssetUploadMessage] = useState(''); const [isDraggingAsset, setIsDraggingAsset] = useState(false); const [editorStep, setEditorStep] = useState<1 | 2 | 3>(1); // Lesson Management const [lessons, setLessons] = useState([]); // New Lesson State const [newLessonTitle, setNewLessonTitle] = useState(''); const [newLessonMedia, setNewLessonMedia] = useState(null); const [newLessonIsFree, setNewLessonIsFree] = useState(false); const [bunnyConfigured, setBunnyConfigured] = useState(null); const [bunnyUploadState, setBunnyUploadState] = useState<'idle' | 'uploading' | 'uploaded' | 'error'>('idle'); const [bunnyUploadMessage, setBunnyUploadMessage] = useState(''); const [deletingVideoId, setDeletingVideoId] = useState(null); // Tips & Global Attachments const [tips, setTips] = useState([]); const [newTip, setNewTip] = useState(''); const [attachments, setAttachments] = useState([]); const [newLinkName, setNewLinkName] = useState(''); const [newLinkUrl, setNewLinkUrl] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { if (isOpen) { setEditorStep(1); if (initialData) { setTitle(initialData.title); setDescription(initialData.description); setCategory(initialData.category); setStatus(initialData.status || 'published'); setThumbnailPreview(initialData.thumbnail); setCoverImageUrl(initialData.thumbnail); setCoverImageZoom(initialData.coverImageZoom || 1); setCoverImagePositionX(initialData.coverImagePositionX ?? 50); setCoverImagePositionY(initialData.coverImagePositionY ?? 50); setTips(initialData.tips || []); setAttachments(initialData.attachments || []); setLessons(initialData.lessons || []); } else { setTitle(''); setDescription(''); setCategory(''); setStatus('draft'); setThumbnailPreview(''); setCoverImageUrl(''); setCoverImageZoom(1); setCoverImagePositionX(50); setCoverImagePositionY(50); setTips([]); setAttachments([]); setLessons([]); } if (localCoverPreviewRef.current) { URL.revokeObjectURL(localCoverPreviewRef.current); localCoverPreviewRef.current = null; } setCoverPreviewError(false); setIsAdjustingCoverPosition(false); setNewLessonMedia(null); setBunnyUploadState('idle'); setBunnyUploadMessage(''); setCoverUploadMessage(''); setAssetUploadMessage(''); setNewLinkName(''); setNewLinkUrl(''); instructorApi.bunnyConfiguration() .then((response) => setBunnyConfigured(response.data.configured)) .catch(() => setBunnyConfigured(false)); instructorApi.coverConfiguration() .then((response) => setCoverConfigured(response.data.configured)) .catch(() => setCoverConfigured(false)); instructorApi.assetConfiguration() .then((response) => setAssetConfigured(response.data.configured)) .catch(() => setAssetConfigured(false)); courseApi.categories() .then((response) => { setCategories(response.data); if (!initialData) setCategory((current) => current || response.data[0] || ''); }) .catch(() => setCategories([])); } }, [isOpen, initialData]); useEffect(() => () => { if (localCoverPreviewRef.current) URL.revokeObjectURL(localCoverPreviewRef.current); }, []); const toBunnyMedia = (video: BunnyVideo): LessonMedia => ({ provider: 'bunny', externalId: video.id, status: video.status, durationSeconds: video.durationSeconds, }); const handleBunnyUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; event.target.value = ''; if (!file) return; if (!newLessonTitle.trim()) { alert('Informe o título da aula antes de enviar o vídeo.'); return; } if (!bunnyConfigured) { alert('O envio de vídeos ainda não está disponível. Tente novamente mais tarde ou entre em contato com o administrador.'); return; } setBunnyUploadState('uploading'); setBunnyUploadMessage(`Enviando ${file.name}...`); try { const created = await instructorApi.createBunnyVideo(newLessonTitle.trim()); const uploaded = await instructorApi.uploadBunnyVideo(created.data.id, file); const media = toBunnyMedia(uploaded.data); setNewLessonMedia(media); setBunnyUploadState('uploaded'); setBunnyUploadMessage(uploaded.data.status === 'ready' ? 'Vídeo pronto para ser adicionado à aula.' : 'Upload concluído. O vídeo está sendo processado; você já pode salvar a aula.'); } catch (error) { setBunnyUploadState('error'); setBunnyUploadMessage('Não foi possível enviar o vídeo. Tente novamente.'); } }; const handleCoverUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; event.target.value = ''; if (!file) return; if (!coverConfigured) { setCoverUploadMessage('O armazenamento de imagens ainda não foi configurado pelo administrador.'); return; } setIsCoverUploading(true); setCoverUploadMessage(`Enviando ${file.name}...`); try { const response = await instructorApi.uploadCover(file); if (localCoverPreviewRef.current) URL.revokeObjectURL(localCoverPreviewRef.current); const localPreviewUrl = URL.createObjectURL(file); localCoverPreviewRef.current = localPreviewUrl; setThumbnailPreview(localPreviewUrl); setCoverImageUrl(response.data.coverImageUrl); setCoverImageZoom(1); setCoverImagePositionX(50); setCoverImagePositionY(50); setCoverPreviewError(false); setCoverUploadMessage('Imagem de capa enviada com sucesso.'); } catch (error) { setCoverUploadMessage(error instanceof Error ? error.message : 'Não foi possível enviar a imagem de capa.'); } finally { setIsCoverUploading(false); } }; const startCoverPositioning = (event: React.PointerEvent) => { event.preventDefault(); event.currentTarget.setPointerCapture(event.pointerId); coverDragStartRef.current = { pointerX: event.clientX, pointerY: event.clientY, positionX: coverImagePositionX, positionY: coverImagePositionY, }; }; const moveCoverPosition = (event: React.PointerEvent) => { const start = coverDragStartRef.current; if (!start) return; const bounds = event.currentTarget.getBoundingClientRect(); const nextX = Math.min(100, Math.max(0, start.positionX - ((event.clientX - start.pointerX) / bounds.width) * 100)); const nextY = Math.min(100, Math.max(0, start.positionY - ((event.clientY - start.pointerY) / bounds.height) * 100)); setCoverImagePositionX(Number(nextX.toFixed(1))); setCoverImagePositionY(Number(nextY.toFixed(1))); }; const endCoverPositioning = (event: React.PointerEvent) => { coverDragStartRef.current = null; if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId); }; const refreshLessonMedia = async (lessonId: string) => { const lesson = lessons.find((item) => item.id === lessonId); const media = lesson?.media?.[0]; if (!lesson || !media || media.provider !== 'bunny') return; try { const response = await instructorApi.bunnyVideoStatus(media.externalId); const refreshed = toBunnyMedia(response.data); setLessons((current) => current.map((item) => item.id === lessonId ? { ...item, duration: formatVideoDuration(refreshed.durationSeconds), media: [refreshed], } : item)); } catch (error) { alert(error instanceof Error ? error.message : 'Não foi possível atualizar o status do vídeo.'); } }; // Bunny encodes asynchronously. While the course editor stays open, keep // pending lessons up to date and persist the latest state on the API. useEffect(() => { if (!isOpen) return; const pending = lessons .map((lesson) => ({ lesson, media: lesson.media?.[0] })) .filter((item): item is { lesson: Lesson; media: LessonMedia } => item.media?.provider === 'bunny' && item.media.status === 'processing'); if (!pending.length) return; const timer = window.setTimeout(() => { Promise.all(pending.map(async ({ lesson, media }) => { try { const response = await instructorApi.bunnyVideoStatus(media.externalId); return { lessonId: lesson.id, media: toBunnyMedia(response.data) }; } catch { return null; } })).then((updates) => { const available = updates.filter((update): update is { lessonId: string; media: LessonMedia } => Boolean(update)); if (!available.length) return; setLessons((current) => current.map((lesson) => { const update = available.find((item) => item.lessonId === lesson.id); if (!update) return lesson; return { ...lesson, duration: formatVideoDuration(update.media.durationSeconds), media: [update.media], }; })); }); }, 10_000); return () => window.clearTimeout(timer); }, [isOpen, lessons]); const handleAddLesson = () => { if (!newLessonTitle.trim()) { alert("Digite um título para a aula."); return; } if (!newLessonMedia) { alert('Envie o vídeo da aula antes de adicioná-la.'); return; } const newLesson: Lesson = { id: `lesson-${Date.now()}`, title: newLessonTitle, duration: formatVideoDuration(newLessonMedia.durationSeconds), videoUrl: newLessonMedia.playbackUrl || '', mediaProvider: newLessonMedia.provider, media: [newLessonMedia], isFree: newLessonIsFree, description: '' }; setLessons([...lessons, newLesson]); setNewLessonTitle(''); setNewLessonMedia(null); setNewLessonIsFree(false); setBunnyUploadState('idle'); setBunnyUploadMessage(''); }; const removeLesson = async (lesson: Lesson) => { const video = lesson.media?.find((media) => media.provider === 'bunny'); const message = video ? 'Excluir esta aula e o vídeo enviado? O vídeo será removido permanentemente e esta ação não poderá ser desfeita.' : 'Excluir esta aula? Esta ação não poderá ser desfeita após salvar o curso.'; if (!window.confirm(message)) return; if (video) { setDeletingVideoId(video.externalId); try { await instructorApi.deleteBunnyVideo(video.externalId); } catch (error) { alert(error instanceof Error ? error.message : 'Não foi possível excluir o vídeo. Tente novamente.'); return; } finally { setDeletingVideoId(null); } } setLessons((current) => current.filter((item) => item.id !== lesson.id)); }; const toggleLessonAccess = (id: string) => { setLessons(lessons.map(l => l.id === id ? { ...l, isFree: !l.isFree } : l)); }; const moveLesson = (id: string, direction: -1 | 1) => { setLessons((current) => { const index = current.findIndex((lesson) => lesson.id === id); const nextIndex = index + direction; if (index < 0 || nextIndex < 0 || nextIndex >= current.length) return current; const next = [...current]; [next[index], next[nextIndex]] = [next[nextIndex], next[index]]; return next; }); }; const handleAddTip = () => { if (newTip.trim()) { setTips([...tips, newTip.trim()]); setNewTip(''); } }; const removeTip = (idx: number) => { setTips(tips.filter((_, i) => i !== idx)); }; const handleAttachmentUploads = async (files: FileList | File[]) => { const selectedFiles = Array.from(files); if (!selectedFiles.length) return; if (!assetConfigured) { setAssetUploadMessage('O armazenamento de materiais ainda não foi configurado pelo administrador.'); return; } setIsAssetUploading(true); setAssetUploadMessage(selectedFiles.length === 1 ? `Enviando ${selectedFiles[0].name}...` : `Enviando ${selectedFiles.length} arquivos...`); const uploadedAttachments: Attachment[] = []; const failedFiles: string[] = []; for (const file of selectedFiles) { try { const response = await instructorApi.uploadAsset(file); const type: AttachmentType = response.data.kind === 'spreadsheet' ? 'spreadsheet' : response.data.kind === 'archive' ? 'zip' : file.name.toLowerCase().endsWith('.pdf') ? 'pdf' : 'doc'; uploadedAttachments.push({ id: `asset-${response.data.key}`, name: file.name, type, url: response.data.assetUrl, sizeBytes: response.data.sizeBytes, description: '', }); } catch { failedFiles.push(file.name); } } if (uploadedAttachments.length) setAttachments((current) => [...current, ...uploadedAttachments]); setAssetUploadMessage(failedFiles.length ? `${uploadedAttachments.length} enviado(s). Não foi possível enviar: ${failedFiles.join(', ')}.` : `${uploadedAttachments.length} material(is) enviado(s) com sucesso.`); setIsAssetUploading(false); }; const handleAttachmentUpload = async (event: React.ChangeEvent) => { const files = event.target.files; event.target.value = ''; if (!files) return; await handleAttachmentUploads(files); }; const handleAddLink = () => { const url = newLinkUrl.trim(); if (!url) { alert('Informe o link do material.'); return; } try { const parsedUrl = new URL(url); if (!['https:', 'http:'].includes(parsedUrl.protocol)) throw new Error(); setAttachments((current) => [...current, { id: `link-${Date.now()}`, name: newLinkName.trim() || parsedUrl.hostname, type: 'link', url, description: '', }]); setNewLinkName(''); setNewLinkUrl(''); } catch { alert('Informe um link HTTP ou HTTPS válido.'); } }; const removeAttachment = (id: string) => { setAttachments(attachments.filter(a => a.id !== id)); }; const goToEditorStep = (nextStep: 1 | 2 | 3) => { if (nextStep > 1 && (!title.trim() || !category)) { alert('Informe o título e a categoria do curso antes de continuar.'); return; } if (nextStep > 2 && lessons.length === 0) { alert('Adicione pelo menos uma aula antes de continuar.'); return; } setEditorStep(nextStep); }; const handleSubmit = async () => { if (lessons.length === 0) { alert("Adicione pelo menos uma aula à trilha."); return; } setIsSubmitting(true); const durationStr = totalVideoDuration(lessons); try { await onSave({ id: initialData?.id || `course-${Date.now()}`, title, description, category: category || categories[0], status, thumbnail: coverImageUrl || '/course-placeholder.svg', coverImageZoom, coverImagePositionX, coverImagePositionY, duration: durationStr, instructor: initialData?.instructor || '', views: initialData?.views || 0, progress: initialData?.progress || 0, lessons: lessons, tips: tips, attachments: attachments }); onClose(); } catch (error) { alert(error instanceof Error ? error.message : 'Não foi possível salvar este curso.'); } finally { setIsSubmitting(false); } }; if (!isOpen) return null; return (

{initialData ? 'Editar curso' : 'Novo curso'}

{editorStep === 1 ? 'Informações do curso' : editorStep === 2 ? 'Estruture as aulas' : 'Materiais complementares'}

{editorStep === 1 ? 'Defina como o curso será apresentado.' : editorStep === 2 ? 'Adicione e organize a trilha de aprendizado.' : 'Envie os arquivos de apoio para seus alunos.'}

{[ { step: 1, label: 'Curso' }, { step: 2, label: 'Aulas' }, { step: 3, label: 'Materiais' }, ].map(({ step, label }) => ( ))}
{/* Thumbnail */}
{ if (!isCoverUploading && coverConfigured) coverFileRef.current?.click(); }} onKeyDown={(event) => { if ((event.key === 'Enter' || event.key === ' ') && !isCoverUploading && coverConfigured) coverFileRef.current?.click(); }} role="button" tabIndex={0} aria-label="Enviar imagem de capa" className={`w-full aspect-video rounded-2xl border-2 border-dashed border-white/10 bg-black/40 flex flex-col items-center justify-center transition-colors relative overflow-hidden group ${coverConfigured && !isCoverUploading ? 'cursor-pointer hover:border-orange-500/50' : 'cursor-not-allowed opacity-70'}`} > {thumbnailPreview ? ( <> {coverPreviewError ? (

Não foi possível carregar a imagem

Tente enviá-la novamente. Se o problema continuar, verifique a configuração de entrega de imagens.

) : ( setCoverPreviewError(true)} className="h-full w-full object-cover opacity-80 transition-transform duration-200" style={{ transform: `scale(${coverImageZoom})`, objectPosition: `${coverImagePositionX}% ${coverImagePositionY}%`, }} /> )} {isAdjustingCoverPosition && !coverPreviewError && (
event.stopPropagation()} onPointerDown={startCoverPositioning} onPointerMove={moveCoverPosition} onPointerUp={endCoverPositioning} onPointerCancel={endCoverPositioning} >
Arraste a imagem para ajustar o enquadramento
)}
Clique na imagem para alterar
{Math.round(coverImageZoom * 100)}%
) : (

{isCoverUploading ? 'Enviando imagem...' : 'Clique para enviar uma imagem de capa'}

)}

{coverUploadMessage || (coverConfigured ? 'JPG, PNG ou WebP. Tamanho máximo: 10 MB.' : 'O envio de imagens estará disponível assim que o armazenamento de mídia for configurado.')}

setTitle(e.target.value)} placeholder="Ex.: Estratégias de tráfego pago para e-commerce" className="w-full bg-black/50 text-white text-base font-bold border border-white/10 rounded-xl px-4 py-3 focus:outline-none focus:border-orange-500/50" required />