first commit
Some checks failed
CI / Validate frontend and API (push) Failing after 7m27s

This commit is contained in:
Cauê Faleiros
2026-08-31 12:38:34 -03:00
parent 684f929f28
commit 701788ea08
40 changed files with 5396 additions and 380 deletions

View File

@@ -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)}