From 8ff22ca6d7788651fb734b1192e8646e31fdefa9 Mon Sep 17 00:00:00 2001 From: Sidney Gomes Date: Sat, 25 Jul 2026 00:34:12 -0300 Subject: [PATCH] =?UTF-8?q?=20feat:=20adicionar=20reordena=C3=A7=C3=A3o=20?= =?UTF-8?q?por=20drag-and-drop=20para=20cursos=20m=C3=B3dulos=20e=20aulas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/AdminContentManager.tsx | 162 +++++++++++++++++++++++-- 1 file changed, 154 insertions(+), 8 deletions(-) diff --git a/src/components/AdminContentManager.tsx b/src/components/AdminContentManager.tsx index 314008a..c1a2538 100644 --- a/src/components/AdminContentManager.tsx +++ b/src/components/AdminContentManager.tsx @@ -1,7 +1,8 @@ import React, { useState, useEffect } from 'react'; import { GraduationCap, Plus, Edit, Trash2, FolderPlus, FilePlus, ChevronRight, - ArrowLeft, Film, Youtube, RefreshCw, Layers, CheckCircle, CheckCircle2 + ArrowLeft, Film, Youtube, RefreshCw, Layers, CheckCircle, CheckCircle2, + GripVertical } from 'lucide-react'; import { useModal } from '../contexts/ModalContext'; @@ -75,6 +76,128 @@ export default function AdminContentManager({ token }: AdminContentManagerProps) const [uploadingImage, setUploadingImage] = useState(false); + // Drag and Drop States + const [draggedCourseIndex, setDraggedCourseIndex] = useState(null); + const [draggedModuleIndex, setDraggedModuleIndex] = useState(null); + const [draggedLessonInfo, setDraggedLessonInfo] = useState<{ lessonId: string, moduleId: string, index: number } | null>(null); + + const handleCourseDragStart = (e: React.DragEvent, index: number) => { + setDraggedCourseIndex(index); + e.dataTransfer.effectAllowed = 'move'; + }; + + const handleCourseDragOver = (e: React.DragEvent) => { + e.preventDefault(); + }; + + const handleCourseDrop = async (e: React.DragEvent, targetIndex: number) => { + e.preventDefault(); + if (draggedCourseIndex === null || draggedCourseIndex === targetIndex) return; + + const reorderedCourses = [...courses]; + const [draggedCourse] = reorderedCourses.splice(draggedCourseIndex, 1); + reorderedCourses.splice(targetIndex, 0, draggedCourse); + + setCourses(reorderedCourses); + setDraggedCourseIndex(null); + + const itemsToSave = reorderedCourses.map((c, idx) => ({ id: c.id, order: idx + 1 })); + try { + await fetch('/api/admin/reorder', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ type: 'courses', items: itemsToSave }) + }); + } catch (err) { + console.error('Error reordering courses:', err); + } + }; + + const handleModuleDragStart = (e: React.DragEvent, index: number) => { + setDraggedModuleIndex(index); + e.dataTransfer.effectAllowed = 'move'; + }; + + const handleModuleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + }; + + const handleModuleDrop = async (e: React.DragEvent, targetIndex: number) => { + e.preventDefault(); + if (draggedModuleIndex === null || draggedModuleIndex === targetIndex) return; + + const reorderedModules = [...modules]; + const [draggedModule] = reorderedModules.splice(draggedModuleIndex, 1); + reorderedModules.splice(targetIndex, 0, draggedModule); + + const updatedModules = reorderedModules.map((m, idx) => ({ ...m, order: idx + 1 })); + setModules(updatedModules); + setDraggedModuleIndex(null); + + const itemsToSave = updatedModules.map((m) => ({ id: m.id, order: m.order })); + try { + await fetch('/api/admin/reorder', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ type: 'modules', items: itemsToSave }) + }); + } catch (err) { + console.error('Error reordering modules:', err); + } + }; + + const handleLessonDragStart = (e: React.DragEvent, lessonId: string, moduleId: string, index: number) => { + setDraggedLessonInfo({ lessonId, moduleId, index }); + e.dataTransfer.effectAllowed = 'move'; + }; + + const handleLessonDragOver = (e: React.DragEvent) => { + e.preventDefault(); + }; + + const handleLessonDrop = async (e: React.DragEvent, targetModuleId: string, targetIndex: number) => { + e.preventDefault(); + if (!draggedLessonInfo || draggedLessonInfo.moduleId !== targetModuleId || draggedLessonInfo.index === targetIndex) return; + + const targetModule = modules.find(m => m.id === targetModuleId); + if (!targetModule) return; + + const reorderedLessons = [...targetModule.lessons]; + const [draggedLesson] = reorderedLessons.splice(draggedLessonInfo.index, 1); + reorderedLessons.splice(targetIndex, 0, draggedLesson); + + const updatedLessons = reorderedLessons.map((l, idx) => ({ ...l, order: idx + 1 })); + + const updatedModules = modules.map(m => { + if (m.id === targetModuleId) { + return { ...m, lessons: updatedLessons }; + } + return m; + }); + setModules(updatedModules); + setDraggedLessonInfo(null); + + const itemsToSave = updatedLessons.map((l) => ({ id: l.id, order: l.order })); + try { + await fetch('/api/admin/reorder', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ type: 'lessons', items: itemsToSave }) + }); + } catch (err) { + console.error('Error reordering lessons:', err); + } + }; + const handleImageUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; @@ -817,11 +940,19 @@ export default function AdminContentManager({ token }: AdminContentManagerProps) ) : ( modules.map((mod, modIdx) => ( -
+
handleModuleDragStart(e, modIdx)} + onDragOver={handleModuleDragOver} + onDrop={(e) => handleModuleDrop(e, modIdx)} + > {/* Module Header Panel */}
-
+
+ M{mod.order} @@ -875,9 +1006,17 @@ export default function AdminContentManager({ token }: AdminContentManagerProps) {mod.lessons.length === 0 ? (
Nenhuma aula cadastrada neste módulo.
) : ( - mod.lessons.map(lesson => ( -
-
+ mod.lessons.map((lesson, lessonIdx) => ( +
handleLessonDragStart(e, lesson.id, mod.id, lessonIdx)} + onDragOver={handleLessonDragOver} + onDrop={(e) => handleLessonDrop(e, mod.id, lessonIdx)} + > +
+ #{lesson.order} {lesson.videoType === 'youtube' ? ( @@ -921,18 +1060,25 @@ export default function AdminContentManager({ token }: AdminContentManagerProps) ) : ( /* --- LIST ALL COURSES IN CATALOGUE --- */
- {courses.map(course => ( + {courses.map((course, courseIdx) => (
handleCourseDragStart(e, courseIdx)} + onDragOver={handleCourseDragOver} + onDrop={(e) => handleCourseDrop(e, courseIdx)} >
{/* Thumbnail */} -
+
{course.title} {course.category} + + +
{/* Info */}