feat: complete platform operations roadmap
All checks were successful
CI / Validate frontend and API (push) Successful in 1m58s
CI / Build and publish Docker images (push) Successful in 22s

This commit is contained in:
Cauê Faleiros
2026-09-04 10:07:07 -03:00
parent 2c137529bc
commit 9b6fea4b81
19 changed files with 589 additions and 66 deletions

View File

@@ -24,13 +24,13 @@ import {
Unlock,
Globe,
Download
,Copy, ChevronUp, ChevronDown
} from 'lucide-react';
import { Course, Comment, Attachment, Lesson, AttachmentType, LessonMedia } from '../types';
import { useToast } from '../context/ToastContext';
import { getManagedCourses, saveCourse, deleteCourse, getComments, replyToComment, moderateComment } from '../services/db';
import { CATEGORIES } from '../constants';
import { getManagedCourses, saveCourse, deleteCourse, duplicateCourse, getComments, replyToComment, moderateComment } from '../services/db';
import { getAttachmentIcon, getAttachmentBadge } from '../components/MaterialCard';
import { BunnyVideo, instructorApi, InstructorAnalytics } from '../services/api';
import { BunnyVideo, courseApi, instructorApi, InstructorAnalytics } from '../services/api';
// 1. Create/Edit Course Modal
const CourseModal: React.FC<{
@@ -44,8 +44,7 @@ const CourseModal: React.FC<{
const [category, setCategory] = useState('');
const [status, setStatus] = useState<Course['status']>('draft');
const [thumbnailPreview, setThumbnailPreview] = useState<string>('');
const [instructor, setInstructor] = useState('Mário Morgado');
const [instructorRole, setInstructorRole] = useState('Especialista em Performance');
const [categories, setCategories] = useState<string[]>([]);
// Lesson Management
const [lessons, setLessons] = useState<Lesson[]>([]);
@@ -83,19 +82,15 @@ const CourseModal: React.FC<{
setCategory(initialData.category);
setStatus(initialData.status || 'published');
setThumbnailPreview(initialData.thumbnail);
setInstructor(initialData.instructor || 'Mário Morgado');
setInstructorRole(initialData.instructorRole || 'Especialista em Performance');
setTips(initialData.tips || []);
setAttachments(initialData.attachments || []);
setLessons(initialData.lessons || []);
} else {
setTitle('');
setDescription('');
setCategory(CATEGORIES[0]);
setCategory('');
setStatus('draft');
setThumbnailPreview('');
setInstructor('Mário Morgado');
setInstructorRole('Especialista em Performance');
setTips([]);
setAttachments([]);
setLessons([]);
@@ -106,6 +101,12 @@ const CourseModal: React.FC<{
instructorApi.bunnyConfiguration()
.then((response) => setBunnyConfigured(response.data.configured))
.catch(() => setBunnyConfigured(false));
courseApi.categories()
.then((response) => {
setCategories(response.data);
if (!initialData) setCategory((current) => current || response.data[0] || '');
})
.catch(() => setCategories([]));
}
}, [isOpen, initialData]);
@@ -242,6 +243,17 @@ const CourseModal: React.FC<{
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()]);
@@ -295,12 +307,11 @@ const CourseModal: React.FC<{
id: initialData?.id || `course-${Date.now()}`,
title,
description,
category: category || CATEGORIES[0],
category: category || categories[0],
status,
thumbnail: thumbnailPreview || 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&w=800&q=80',
thumbnail: thumbnailPreview || '/course-placeholder.svg',
duration: durationStr,
instructor,
instructorRole,
instructor: initialData?.instructor || '',
views: initialData?.views || 0,
progress: initialData?.progress || 0,
lessons: lessons,
@@ -379,7 +390,7 @@ const CourseModal: React.FC<{
onChange={(e) => setCategory(e.target.value)}
className="w-full bg-black/50 text-white border border-white/10 rounded-xl px-3 py-2.5 text-xs focus:outline-none focus:border-orange-500/50"
>
{CATEGORIES.map(cat => (
{categories.map(cat => (
<option key={cat} value={cat} className="bg-zinc-900">{cat}</option>
))}
</select>
@@ -393,16 +404,6 @@ const CourseModal: React.FC<{
</select>
</div>
<div>
<label className="text-xs text-gray-400 uppercase font-bold tracking-wider mb-1 block">Instrutor</label>
<input
type="text"
value={instructor}
onChange={(e) => setInstructor(e.target.value)}
placeholder="Nome do Instrutor"
className="w-full bg-black/50 text-white border border-white/10 rounded-xl px-3 py-2.5 text-xs focus:outline-none focus:border-orange-500/50"
/>
</div>
</div>
<div>
@@ -551,6 +552,8 @@ const CourseModal: React.FC<{
</div>
<div className="flex items-center gap-1">
<button type="button" disabled={index === 0} onClick={() => moveLesson(lesson.id, -1)} className="p-1 text-gray-400 hover:text-white disabled:opacity-30" title="Mover aula para cima"><ChevronUp className="w-4 h-4" /></button>
<button type="button" disabled={index === lessons.length - 1} onClick={() => moveLesson(lesson.id, 1)} className="p-1 text-gray-400 hover:text-white disabled:opacity-30" title="Mover aula para baixo"><ChevronDown className="w-4 h-4" /></button>
{lesson.media?.[0]?.provider === 'bunny' && lesson.media[0].status !== 'ready' && (
<button
type="button"
@@ -728,6 +731,16 @@ export const ManageCourses: React.FC = () => {
}
};
const handleDuplicate = async (id: string) => {
try {
await duplicateCourse(id);
showToast('Cópia criada como rascunho.', 'success');
loadData();
} catch (error) {
showToast(error instanceof Error ? error.message : 'Não foi possível duplicar este curso.', 'error');
}
};
const handleSave = async (course: Course) => {
await saveCourse(course);
showToast('Curso e materiais salvos com sucesso!', 'success');
@@ -906,6 +919,8 @@ export const ManageCourses: React.FC = () => {
Editar Conteúdo
</button>
<button onClick={() => void handleDuplicate(course.id)} className="p-2.5 rounded-[980px] bg-white/5 hover:bg-white/10 text-gray-400 hover:text-white transition-colors" title="Duplicar curso"><Copy className="w-4 h-4" /></button>
<button
onClick={() => handleDelete(course.id)}
className="p-2.5 rounded-[980px] bg-white/5 hover:bg-red-500/20 text-gray-400 hover:text-red-400 transition-colors"