feat: complete platform operations roadmap
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user