feat: adicionar reordenação por drag-and-drop para cursos módulos e aulas
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 59s
Details
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 59s
Details
This commit is contained in:
parent
54d1646cba
commit
8ff22ca6d7
|
|
@ -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<number | null>(null);
|
||||
const [draggedModuleIndex, setDraggedModuleIndex] = useState<number | null>(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<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
|
@ -817,11 +940,19 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
|
|||
</div>
|
||||
) : (
|
||||
modules.map((mod, modIdx) => (
|
||||
<div key={mod.id} className="glass-card rounded-xl overflow-hidden">
|
||||
<div
|
||||
key={mod.id}
|
||||
className="glass-card rounded-xl overflow-hidden"
|
||||
draggable
|
||||
onDragStart={(e) => handleModuleDragStart(e, modIdx)}
|
||||
onDragOver={handleModuleDragOver}
|
||||
onDrop={(e) => handleModuleDrop(e, modIdx)}
|
||||
>
|
||||
|
||||
{/* Module Header Panel */}
|
||||
<div className="glass-accordion-header px-4 py-3 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center space-x-2 cursor-grab active:cursor-grabbing">
|
||||
<GripVertical className="w-4 h-4 text-zinc-500" />
|
||||
<span className="bg-zinc-800 border border-zinc-700 text-[#E50914] text-[10px] font-extrabold px-1.5 py-0.5 rounded">
|
||||
M{mod.order}
|
||||
</span>
|
||||
|
|
@ -875,9 +1006,17 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
|
|||
{mod.lessons.length === 0 ? (
|
||||
<div className="p-4 text-xs text-zinc-600 italic text-center">Nenhuma aula cadastrada neste módulo.</div>
|
||||
) : (
|
||||
mod.lessons.map(lesson => (
|
||||
<div key={lesson.id} className="px-6 py-3 flex items-center justify-between text-xs hover:bg-zinc-900/10 transition-colors">
|
||||
<div className="flex items-center space-x-3 min-w-0">
|
||||
mod.lessons.map((lesson, lessonIdx) => (
|
||||
<div
|
||||
key={lesson.id}
|
||||
className="px-6 py-3 flex items-center justify-between text-xs hover:bg-zinc-900/10 transition-colors"
|
||||
draggable
|
||||
onDragStart={(e) => handleLessonDragStart(e, lesson.id, mod.id, lessonIdx)}
|
||||
onDragOver={handleLessonDragOver}
|
||||
onDrop={(e) => handleLessonDrop(e, mod.id, lessonIdx)}
|
||||
>
|
||||
<div className="flex items-center space-x-3 min-w-0 cursor-grab active:cursor-grabbing">
|
||||
<GripVertical className="w-3.5 h-3.5 text-zinc-600 flex-shrink-0" />
|
||||
<span className="text-zinc-500 font-mono font-bold w-4 text-right">#{lesson.order}</span>
|
||||
{lesson.videoType === 'youtube' ? (
|
||||
<Youtube className="w-4 h-4 text-red-500 flex-shrink-0" />
|
||||
|
|
@ -921,18 +1060,25 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
|
|||
) : (
|
||||
/* --- LIST ALL COURSES IN CATALOGUE --- */
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{courses.map(course => (
|
||||
{courses.map((course, courseIdx) => (
|
||||
<div
|
||||
key={course.id}
|
||||
className="glass-card rounded-2xl overflow-hidden flex flex-col justify-between"
|
||||
draggable
|
||||
onDragStart={(e) => handleCourseDragStart(e, courseIdx)}
|
||||
onDragOver={handleCourseDragOver}
|
||||
onDrop={(e) => handleCourseDrop(e, courseIdx)}
|
||||
>
|
||||
<div>
|
||||
{/* Thumbnail */}
|
||||
<div className="relative w-full aspect-video">
|
||||
<div className="relative w-full aspect-video cursor-grab active:cursor-grabbing">
|
||||
<img src={course.thumbnail} alt={course.title} className="w-full h-full object-cover" referrerPolicy="no-referrer" />
|
||||
<span className="absolute top-3 left-3 bg-[#E50914]/85 text-white text-[10px] font-black px-2 py-0.5 rounded uppercase tracking-wider glass-badge">
|
||||
{course.category}
|
||||
</span>
|
||||
<span className="absolute top-3 right-3 bg-black/60 text-white p-1 rounded backdrop-blur-sm border border-white/10 opacity-70 hover:opacity-100 transition-opacity">
|
||||
<GripVertical className="w-3.5 h-3.5" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
|
|
|
|||
Loading…
Reference in New Issue