This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Plus,
|
||||
Settings,
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
Send,
|
||||
Image as ImageIcon,
|
||||
Video,
|
||||
Sparkles,
|
||||
Loader2,
|
||||
FileText,
|
||||
FileSpreadsheet,
|
||||
@@ -23,14 +22,12 @@ import {
|
||||
Check,
|
||||
Lock,
|
||||
Unlock,
|
||||
UploadCloud,
|
||||
Globe,
|
||||
Download
|
||||
} from 'lucide-react';
|
||||
import { Course, Comment, Attachment, Lesson, AttachmentType } from '../types';
|
||||
import { useToast } from '../context/ToastContext';
|
||||
import { getCourses, saveCourse, deleteCourse, getComments, saveComment } from '../services/db';
|
||||
import { generateStudentReply } from '../services/ai';
|
||||
import { getManagedCourses, saveCourse, deleteCourse, getComments, saveComment } from '../services/db';
|
||||
import { CATEGORIES } from '../constants';
|
||||
import { getAttachmentIcon, getAttachmentBadge } from '../components/MaterialCard';
|
||||
|
||||
@@ -38,7 +35,7 @@ import { getAttachmentIcon, getAttachmentBadge } from '../components/MaterialCar
|
||||
const CourseModal: React.FC<{
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (course: Course) => void;
|
||||
onSave: (course: Course) => Promise<void>;
|
||||
initialData?: Course;
|
||||
}> = ({ isOpen, onClose, onSave, initialData }) => {
|
||||
const [title, setTitle] = useState('');
|
||||
@@ -55,10 +52,8 @@ const CourseModal: React.FC<{
|
||||
const [newLessonTitle, setNewLessonTitle] = useState('');
|
||||
const [newLessonDuration, setNewLessonDuration] = useState('');
|
||||
const [newLessonVideoUrl, setNewLessonVideoUrl] = useState('');
|
||||
const [newLessonProvider, setNewLessonProvider] = useState('external');
|
||||
const [newLessonIsFree, setNewLessonIsFree] = useState(false);
|
||||
const [videoSourceType, setVideoSourceType] = useState<'upload' | 'link'>('upload');
|
||||
const [selectedFileName, setSelectedFileName] = useState('');
|
||||
const lessonFileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Tips & Global Attachments
|
||||
const [tips, setTips] = useState<string[]>([]);
|
||||
@@ -73,7 +68,6 @@ const CourseModal: React.FC<{
|
||||
const [newAttDesc, setNewAttDesc] = useState('');
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
@@ -101,31 +95,13 @@ const CourseModal: React.FC<{
|
||||
}
|
||||
}, [isOpen, initialData]);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
setThumbnailPreview(objectUrl);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLessonVideoFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
setNewLessonVideoUrl(objectUrl);
|
||||
setSelectedFileName(file.name);
|
||||
if (!newLessonDuration) setNewLessonDuration('15m');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddLesson = () => {
|
||||
if (!newLessonTitle.trim()) {
|
||||
alert("Digite um título para a aula.");
|
||||
return;
|
||||
}
|
||||
if (!newLessonVideoUrl.trim()) {
|
||||
alert("Selecione um vídeo ou insira um link.");
|
||||
alert("Enter a hosted video URL.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -134,6 +110,7 @@ const CourseModal: React.FC<{
|
||||
title: newLessonTitle,
|
||||
duration: newLessonDuration || '15m',
|
||||
videoUrl: newLessonVideoUrl,
|
||||
mediaProvider: newLessonProvider || 'external',
|
||||
isFree: newLessonIsFree,
|
||||
description: ''
|
||||
};
|
||||
@@ -143,9 +120,8 @@ const CourseModal: React.FC<{
|
||||
setNewLessonTitle('');
|
||||
setNewLessonDuration('');
|
||||
setNewLessonVideoUrl('');
|
||||
setSelectedFileName('');
|
||||
setNewLessonProvider('external');
|
||||
setNewLessonIsFree(false);
|
||||
if (lessonFileInputRef.current) lessonFileInputRef.current.value = '';
|
||||
};
|
||||
|
||||
const removeLesson = (id: string) => {
|
||||
@@ -193,7 +169,7 @@ const CourseModal: React.FC<{
|
||||
setAttachments(attachments.filter(a => a.id !== id));
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (lessons.length === 0) {
|
||||
alert("Adicione pelo menos uma aula à trilha.");
|
||||
@@ -204,24 +180,28 @@ const CourseModal: React.FC<{
|
||||
|
||||
const durationStr = `${lessons.length} Aulas`;
|
||||
|
||||
onSave({
|
||||
id: initialData?.id || `course-${Date.now()}`,
|
||||
title,
|
||||
description,
|
||||
category: category || CATEGORIES[0],
|
||||
thumbnail: thumbnailPreview || 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&w=800&q=80',
|
||||
duration: durationStr,
|
||||
instructor,
|
||||
instructorRole,
|
||||
views: initialData?.views || 0,
|
||||
progress: initialData?.progress || 0,
|
||||
lessons: lessons,
|
||||
tips: tips,
|
||||
attachments: attachments
|
||||
});
|
||||
|
||||
setIsSubmitting(false);
|
||||
onClose();
|
||||
try {
|
||||
await onSave({
|
||||
id: initialData?.id || `course-${Date.now()}`,
|
||||
title,
|
||||
description,
|
||||
category: category || CATEGORIES[0],
|
||||
thumbnail: thumbnailPreview || 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&w=800&q=80',
|
||||
duration: durationStr,
|
||||
instructor,
|
||||
instructorRole,
|
||||
views: initialData?.views || 0,
|
||||
progress: initialData?.progress || 0,
|
||||
lessons: lessons,
|
||||
tips: tips,
|
||||
attachments: attachments
|
||||
});
|
||||
onClose();
|
||||
} catch {
|
||||
alert('We could not save this course. Confirm that you are still signed in and that all asset URLs are valid.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
@@ -243,7 +223,6 @@ const CourseModal: React.FC<{
|
||||
<label className="text-xs text-gray-400 uppercase font-bold tracking-wider mb-2 block">Imagem de Capa (16:9)</label>
|
||||
<div
|
||||
className="w-full aspect-video rounded-2xl border-2 border-dashed border-white/10 bg-black/40 flex flex-col items-center justify-center cursor-pointer hover:border-orange-500/50 transition-colors relative overflow-hidden group"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
{thumbnailPreview ? (
|
||||
<>
|
||||
@@ -255,17 +234,17 @@ const CourseModal: React.FC<{
|
||||
) : (
|
||||
<div className="text-center p-6">
|
||||
<ImageIcon className="w-10 h-10 text-orange-400/60 mx-auto mb-2" />
|
||||
<p className="text-gray-300 text-xs font-medium">Clique para selecionar imagem</p>
|
||||
<p className="text-gray-300 text-xs font-medium">Add a hosted cover image URL below</p>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="url"
|
||||
value={thumbnailPreview}
|
||||
onChange={(event) => setThumbnailPreview(event.target.value)}
|
||||
placeholder="Hosted cover image URL"
|
||||
className="w-full mt-2 bg-black/50 text-white text-xs border border-white/10 rounded-xl px-4 py-3 focus:outline-none focus:border-orange-500/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
@@ -492,52 +471,21 @@ const CourseModal: React.FC<{
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Video Source Tabs */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="flex bg-white/5 p-0.5 rounded-lg border border-white/5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setVideoSourceType('upload')}
|
||||
className={`px-3 py-1 rounded-md text-xs font-bold transition-all ${videoSourceType === 'upload' ? 'bg-orange-500 text-white' : 'text-gray-400'}`}
|
||||
>
|
||||
Vídeo MP4
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setVideoSourceType('link')}
|
||||
className={`px-3 py-1 rounded-md text-xs font-bold transition-all ${videoSourceType === 'link' ? 'bg-orange-500 text-white' : 'text-gray-400'}`}
|
||||
>
|
||||
URL Externa
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
{videoSourceType === 'upload' ? (
|
||||
<div
|
||||
onClick={() => lessonFileInputRef.current?.click()}
|
||||
className="border border-dashed border-white/20 hover:border-orange-500/50 rounded-xl py-2 px-3 text-center cursor-pointer transition-all bg-white/[0.02]"
|
||||
>
|
||||
<input
|
||||
ref={lessonFileInputRef}
|
||||
type="file"
|
||||
accept="video/*"
|
||||
className="hidden"
|
||||
onChange={handleLessonVideoFileSelect}
|
||||
/>
|
||||
<span className="text-xs text-gray-300 truncate block">
|
||||
{selectedFileName ? `Arquivo: ${selectedFileName}` : 'Selecionar arquivo de vídeo'}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={newLessonVideoUrl}
|
||||
onChange={(e) => setNewLessonVideoUrl(e.target.value)}
|
||||
placeholder="https://commondatastorage.googleapis.com/...mp4"
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl px-3 py-2 text-xs text-white focus:outline-none"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={newLessonProvider}
|
||||
onChange={(event) => setNewLessonProvider(event.target.value)}
|
||||
placeholder="Provider (external, Panda, Vimeo...)"
|
||||
className="w-40 bg-white/5 border border-white/10 rounded-xl px-3 py-2 text-xs text-white focus:outline-none"
|
||||
/>
|
||||
<input
|
||||
type="url"
|
||||
value={newLessonVideoUrl}
|
||||
onChange={(event) => setNewLessonVideoUrl(event.target.value)}
|
||||
placeholder="Hosted video URL"
|
||||
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-3 py-2 text-xs text-white focus:outline-none"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@@ -577,13 +525,12 @@ export const ManageCourses: React.FC = () => {
|
||||
const [allComments, setAllComments] = useState<{comment: Comment, courseTitle: string}[]>([]);
|
||||
const [replyingTo, setReplyingTo] = useState<string | null>(null);
|
||||
const [replyText, setReplyText] = useState('');
|
||||
const [isGeneratingReply, setIsGeneratingReply] = useState(false);
|
||||
|
||||
const { showToast } = useToast();
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
const data = await getCourses();
|
||||
const data = await getManagedCourses();
|
||||
setCourses(data);
|
||||
|
||||
// Load all comments
|
||||
@@ -644,18 +591,6 @@ export const ManageCourses: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerateAIReply = async (commentText: string, courseTitle: string) => {
|
||||
setIsGeneratingReply(true);
|
||||
try {
|
||||
const aiReply = await generateStudentReply(commentText, courseTitle);
|
||||
setReplyText(aiReply);
|
||||
} catch (e) {
|
||||
showToast('Erro ao gerar resposta com IA.', 'error');
|
||||
} finally {
|
||||
setIsGeneratingReply(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Metrics
|
||||
const totalViews = courses.reduce((acc, c) => acc + (c.views || 0), 0);
|
||||
const totalLessons = courses.reduce((acc, c) => acc + c.lessons.length, 0);
|
||||
@@ -850,18 +785,7 @@ export const ManageCourses: React.FC = () => {
|
||||
className="w-full bg-black/60 border border-white/10 rounded-[8px] p-3 text-body-sm text-white focus:outline-none focus:border-orange-500/50 resize-none"
|
||||
rows={3}
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleGenerateAIReply(comment.text, courseTitle)}
|
||||
disabled={isGeneratingReply}
|
||||
className="inline-flex items-center gap-1.5 text-body-sm text-orange-400 hover:text-orange-300 font-semibold"
|
||||
>
|
||||
<Sparkles className="w-3.5 h-3.5" />
|
||||
{isGeneratingReply ? 'Gerando com IA...' : 'Sugerir Resposta com IA'}
|
||||
</button>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => { setReplyingTo(null); setReplyText(''); }}
|
||||
className="px-3.5 py-2 rounded-[980px] text-body-sm text-gray-400 hover:text-white font-semibold"
|
||||
@@ -876,7 +800,6 @@ export const ManageCourses: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setReplyingTo(comment.id)}
|
||||
|
||||
Reference in New Issue
Block a user