998 lines
42 KiB
TypeScript
998 lines
42 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
GraduationCap, Plus, Edit, Trash2, FolderPlus, FilePlus, ChevronRight,
|
|
ArrowLeft, Film, Youtube, RefreshCw, Layers, CheckCircle, CheckCircle2
|
|
} from 'lucide-react';
|
|
|
|
interface Lesson {
|
|
id: string;
|
|
moduleId: string;
|
|
title: string;
|
|
videoUrl: string;
|
|
videoType: 'youtube' | 'direct';
|
|
order: number;
|
|
content: string;
|
|
thumbnailUrl?: string;
|
|
}
|
|
|
|
interface Module {
|
|
id: string;
|
|
courseId: string;
|
|
title: string;
|
|
order: number;
|
|
lessons: Lesson[];
|
|
}
|
|
|
|
interface Course {
|
|
id: string;
|
|
title: string;
|
|
description: string;
|
|
thumbnail: string;
|
|
category: string;
|
|
isLocked?: boolean;
|
|
price?: number;
|
|
certificateMode?: string;
|
|
}
|
|
|
|
interface AdminContentManagerProps {
|
|
token: string | null;
|
|
}
|
|
|
|
export default function AdminContentManager({ token }: AdminContentManagerProps) {
|
|
const [courses, setCourses] = useState<Course[]>([]);
|
|
const [selectedCourse, setSelectedCourse] = useState<Course | null>(null);
|
|
const [modules, setModules] = useState<Module[]>([]);
|
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Forms states
|
|
const [showCourseForm, setShowCourseForm] = useState(false);
|
|
const [editingCourse, setEditingCourse] = useState<Course | null>(null);
|
|
const [courseForm, setCourseForm] = useState({
|
|
title: '', description: '', thumbnail: '', category: '', isLocked: false, price: 0, certificateMode: 'FULL_COURSE' as 'FULL_COURSE' | 'PER_MODULE'
|
|
});
|
|
|
|
const [showModuleForm, setShowModuleForm] = useState(false);
|
|
const [editingModule, setEditingModule] = useState<Module | null>(null);
|
|
const [moduleForm, setModuleForm] = useState({ title: '', order: 1 });
|
|
|
|
const [showLessonForm, setShowLessonForm] = useState(false);
|
|
const [editingLesson, setEditingLesson] = useState<Lesson | null>(null);
|
|
const [activeModuleForLesson, setActiveModuleForLesson] = useState<string | null>(null);
|
|
const [lessonForm, setLessonForm] = useState({
|
|
title: '', videoUrl: '', videoType: 'youtube' as 'youtube' | 'direct', order: 1, content: '', thumbnailUrl: ''
|
|
});
|
|
|
|
const [managingActivityForModule, setManagingActivityForModule] = useState<string | null>(null);
|
|
|
|
const [showActivityModal, setShowActivityModal] = useState(false);
|
|
const [activeModuleForActivity, setActiveModuleForActivity] = useState<string | null>(null);
|
|
const [activityQuestions, setActivityQuestions] = useState<{question: string, options: string[], correctIndex: number}[]>([]);
|
|
|
|
const [uploadingImage, setUploadingImage] = useState(false);
|
|
|
|
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
setUploadingImage(true);
|
|
try {
|
|
const res = await fetch('/api/admin/upload', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
},
|
|
body: formData
|
|
});
|
|
const data = await res.json();
|
|
if (res.ok && data.url) {
|
|
setCourseForm(prev => ({ ...prev, thumbnail: data.url }));
|
|
} else {
|
|
alert(data.error || 'Erro ao fazer upload da imagem');
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Erro de conexão ao fazer upload');
|
|
} finally {
|
|
setUploadingImage(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchCourses();
|
|
}, []);
|
|
|
|
const fetchCourses = async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const res = await fetch('/api/courses', {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setCourses(data);
|
|
}
|
|
} catch (err) {
|
|
console.error('Error fetching courses:', err);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const fetchCourseModulesAndLessons = async (courseId: string) => {
|
|
setIsLoading(true);
|
|
try {
|
|
const res = await fetch(`/api/courses/${courseId}`, {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setModules(data.modules);
|
|
}
|
|
} catch (err) {
|
|
console.error('Error loading modules:', err);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSelectCourse = (course: Course) => {
|
|
setSelectedCourse(course);
|
|
fetchCourseModulesAndLessons(course.id);
|
|
};
|
|
|
|
// --- COURSE CRUD ---
|
|
const handleSaveCourse = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
const method = editingCourse ? 'PUT' : 'POST';
|
|
const url = editingCourse ? `/api/admin/courses/${editingCourse.id}` : '/api/admin/courses';
|
|
|
|
try {
|
|
const res = await fetch(url, {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify(courseForm)
|
|
});
|
|
|
|
if (res.ok) {
|
|
await fetchCourses();
|
|
setShowCourseForm(false);
|
|
setEditingCourse(null);
|
|
setCourseForm({ title: '', description: '', thumbnail: '', category: '', isLocked: false, price: 0, certificateMode: 'FULL_COURSE' });
|
|
} else {
|
|
const errData = await res.json();
|
|
alert(`Erro ao salvar curso: ${errData.error || 'Falha na resposta do servidor'}`);
|
|
}
|
|
} catch (err) {
|
|
console.error('Error saving course:', err);
|
|
alert('Erro de rede ao salvar curso.');
|
|
}
|
|
};
|
|
|
|
const handleEditCourse = (course: Course) => {
|
|
setEditingCourse(course);
|
|
setCourseForm({
|
|
title: course.title,
|
|
description: course.description,
|
|
thumbnail: course.thumbnail,
|
|
category: course.category,
|
|
isLocked: course.isLocked || false,
|
|
price: course.price || 0,
|
|
certificateMode: (course.certificateMode || 'FULL_COURSE') as 'PER_MODULE' | 'FULL_COURSE'
|
|
});
|
|
setShowCourseForm(true);
|
|
setTimeout(() => {
|
|
document.getElementById('admin-content-top')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}, 50);
|
|
};
|
|
|
|
const handleDeleteCourse = async (courseId: string) => {
|
|
if (!window.confirm('Tem certeza que deseja excluir este curso e todo o seu conteúdo (módulos, aulas, anotações)?')) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/admin/courses/${courseId}`, {
|
|
method: 'DELETE',
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
|
|
if (res.ok) {
|
|
fetchCourses();
|
|
if (selectedCourse?.id === courseId) {
|
|
setSelectedCourse(null);
|
|
setModules([]);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Error deleting course:', err);
|
|
}
|
|
};
|
|
|
|
// --- MODULE CRUD ---
|
|
const handleSaveModule = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!selectedCourse) return;
|
|
|
|
const method = editingModule ? 'PUT' : 'POST';
|
|
const url = editingModule ? `/api/admin/modules/${editingModule.id}` : '/api/admin/modules';
|
|
const payload = editingModule ? { ...moduleForm } : { ...moduleForm, courseId: selectedCourse.id };
|
|
|
|
try {
|
|
const res = await fetch(url, {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (res.ok) {
|
|
await fetchCourseModulesAndLessons(selectedCourse.id);
|
|
setShowModuleForm(false);
|
|
setEditingModule(null);
|
|
setModuleForm({ title: '', order: 1 });
|
|
}
|
|
} catch (err) {
|
|
console.error('Error saving module:', err);
|
|
}
|
|
};
|
|
|
|
const handleEditModule = (mod: Module) => {
|
|
setEditingModule(mod);
|
|
setModuleForm({ title: mod.title, order: mod.order });
|
|
setShowModuleForm(true);
|
|
setTimeout(() => {
|
|
document.getElementById('admin-content-top')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}, 50);
|
|
};
|
|
|
|
const handleDeleteModule = async (modId: string) => {
|
|
if (!window.confirm('Excluir este módulo apagará todas as suas aulas associadas. Continuar?')) return;
|
|
if (!selectedCourse) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/admin/modules/${modId}`, {
|
|
method: 'DELETE',
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
|
|
if (res.ok) {
|
|
fetchCourseModulesAndLessons(selectedCourse.id);
|
|
}
|
|
} catch (err) {
|
|
console.error('Error deleting module:', err);
|
|
}
|
|
};
|
|
|
|
// --- LESSON CRUD ---
|
|
const handleSaveLesson = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!selectedCourse) return;
|
|
|
|
const method = editingLesson ? 'PUT' : 'POST';
|
|
const url = editingLesson ? `/api/admin/lessons/${editingLesson.id}` : '/api/admin/lessons';
|
|
const payload = editingLesson ? { ...lessonForm } : { ...lessonForm, moduleId: activeModuleForLesson };
|
|
|
|
try {
|
|
const res = await fetch(url, {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (res.ok) {
|
|
await fetchCourseModulesAndLessons(selectedCourse.id);
|
|
setShowLessonForm(false);
|
|
setEditingLesson(null);
|
|
setActiveModuleForLesson(null);
|
|
setLessonForm({ title: '', videoUrl: '', videoType: 'youtube', order: 1, content: '', thumbnailUrl: '' });
|
|
}
|
|
} catch (err) {
|
|
console.error('Error saving lesson:', err);
|
|
}
|
|
};
|
|
|
|
const handleNewLesson = (moduleId: string, lessonsCount: number) => {
|
|
setActiveModuleForLesson(moduleId);
|
|
setEditingLesson(null);
|
|
setLessonForm({
|
|
title: '',
|
|
videoUrl: '',
|
|
videoType: 'youtube',
|
|
order: lessonsCount + 1,
|
|
content: '',
|
|
thumbnailUrl: ''
|
|
});
|
|
setShowLessonForm(true);
|
|
setTimeout(() => {
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
}, 50);
|
|
};
|
|
|
|
const handleEditLesson = (lesson: Lesson) => {
|
|
setEditingLesson(lesson);
|
|
setActiveModuleForLesson(lesson.moduleId);
|
|
setLessonForm({
|
|
title: lesson.title,
|
|
videoUrl: lesson.videoUrl,
|
|
videoType: lesson.videoType,
|
|
order: lesson.order,
|
|
content: lesson.content || '',
|
|
thumbnailUrl: lesson.thumbnailUrl || ''
|
|
});
|
|
setShowLessonForm(true);
|
|
setTimeout(() => {
|
|
document.getElementById('admin-content-top')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}, 50);
|
|
};
|
|
const handleManageActivity = async (moduleId: string) => {
|
|
setActiveModuleForActivity(moduleId);
|
|
setActivityQuestions([]);
|
|
|
|
try {
|
|
const res = await fetch(`/api/admin/modules/${moduleId}/activity`, {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setActivityQuestions(data.questions || []);
|
|
}
|
|
} catch (err) {
|
|
console.error('Error fetching activity:', err);
|
|
}
|
|
|
|
setShowActivityModal(true);
|
|
};
|
|
|
|
const handleSaveActivity = async () => {
|
|
if (!activeModuleForActivity) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/admin/modules/${activeModuleForActivity}/activity`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify({ questions: activityQuestions })
|
|
});
|
|
if (res.ok) {
|
|
alert('Atividade salva com sucesso!');
|
|
setShowActivityModal(false);
|
|
} else {
|
|
alert('Erro ao salvar atividade.');
|
|
}
|
|
} catch (err) {
|
|
console.error('Error saving activity:', err);
|
|
alert('Erro ao salvar atividade.');
|
|
}
|
|
};
|
|
|
|
|
|
const handleDeleteLesson = async (lessonId: string) => {
|
|
if (!window.confirm('Excluir esta aula?')) return;
|
|
if (!selectedCourse) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/admin/lessons/${lessonId}`, {
|
|
method: 'DELETE',
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
|
|
if (res.ok) {
|
|
fetchCourseModulesAndLessons(selectedCourse.id);
|
|
}
|
|
} catch (err) {
|
|
console.error('Error deleting lesson:', err);
|
|
}
|
|
};
|
|
|
|
if (isLoading && courses.length === 0) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center min-h-[50vh] text-zinc-400 space-y-4">
|
|
<RefreshCw className="w-8 h-8 text-[#E50914] animate-spin" />
|
|
<p className="text-sm font-semibold">Carregando catálogo de treinamentos...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div id="admin-content-top" className="space-y-8">
|
|
{/* Top Header */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between border-b border-white/10 pb-5 gap-4">
|
|
<div>
|
|
<h1 className="text-2xl md:text-3xl font-display font-extrabold text-white tracking-tight flex items-center space-x-2">
|
|
<GraduationCap className="w-8 h-8 text-[#E50914]" />
|
|
<span>Gestão de Cursos, Módulos e Aulas</span>
|
|
</h1>
|
|
<p className="text-xs text-zinc-500 mt-1">Crie cursos, adicione módulos e configure links de vídeo (YouTube ou Upload Direto).</p>
|
|
</div>
|
|
{!selectedCourse && !showCourseForm && (
|
|
<button
|
|
onClick={() => { setEditingCourse(null); setCourseForm({ title: '', description: '', thumbnail: '', category: '', isLocked: false, price: 0, certificateMode: 'FULL_COURSE' }); setShowCourseForm(true); }}
|
|
className="glass-btn-primary text-white font-bold text-xs px-4 py-2.5 rounded-lg flex items-center justify-center space-x-2 cursor-pointer self-start sm:self-auto"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
<span>Adicionar Novo Curso</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Helper Banner for Questionnaires / Activities */}
|
|
{!selectedCourse && !showCourseForm && (
|
|
<div className="glass-card p-4 rounded-xl border border-emerald-500/30 bg-emerald-950/20 text-emerald-300 text-xs leading-relaxed flex items-center space-x-3">
|
|
<div className="p-2.5 bg-emerald-500/20 text-emerald-400 rounded-xl flex-shrink-0">
|
|
<CheckCircle className="w-6 h-6" />
|
|
</div>
|
|
<div className="space-y-1">
|
|
<p className="font-extrabold text-white text-sm">Como Criar as Questões e Atividades dos Alunos?</p>
|
|
<p className="text-zinc-300">
|
|
Escolha um curso abaixo clicando no botão <strong className="text-white bg-white/10 px-1.5 py-0.5 rounded">Gerenciar Módulos & Questões</strong>. Em seguida, em cada módulo, clique no botão verde <strong className="text-emerald-300 bg-emerald-500/30 border border-emerald-400/40 px-2 py-0.5 rounded">📝 Criar / Editar Questões</strong> para adicionar perguntas e respostas!
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* --- RENDER COURSE FORM --- */}
|
|
{showCourseForm && (
|
|
<div className="glass-card p-6 rounded-2xl space-y-6">
|
|
<div className="flex items-center justify-between border-b border-white/5 pb-4">
|
|
<h3 className="text-lg font-bold text-white">
|
|
{editingCourse ? 'Editar Detalhes do Curso' : 'Criar Novo Curso'}
|
|
</h3>
|
|
<button
|
|
onClick={() => setShowCourseForm(false)}
|
|
className="text-zinc-500 hover:text-zinc-300 text-xs font-semibold"
|
|
>
|
|
Cancelar
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSaveCourse} className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="space-y-2">
|
|
<label className="text-xs text-zinc-400 font-bold uppercase tracking-wider">Título do Curso</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={courseForm.title}
|
|
onChange={(e) => setCourseForm({ ...courseForm, title: e.target.value })}
|
|
placeholder="Ex: Infraestrutura de Servidores Linux"
|
|
className="w-full glass-input rounded-lg p-3 text-sm text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-xs text-zinc-400 font-bold uppercase tracking-wider">Categoria / Trilha</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={courseForm.category}
|
|
onChange={(e) => setCourseForm({ ...courseForm, category: e.target.value })}
|
|
placeholder="Ex: Redes e Servidores"
|
|
className="w-full glass-input rounded-lg p-3 text-sm text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="md:col-span-2 space-y-2">
|
|
<label className="text-xs text-zinc-400 font-bold uppercase tracking-wider">Imagem de Capa (Thumbnail URL)</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="url"
|
|
required
|
|
value={courseForm.thumbnail}
|
|
onChange={(e) => setCourseForm({ ...courseForm, thumbnail: e.target.value })}
|
|
placeholder="https://images.unsplash.com/... ou link de imagem direta"
|
|
className="flex-1 glass-input rounded-lg p-3 text-sm text-white focus:outline-none"
|
|
/>
|
|
<label className="bg-zinc-800 hover:bg-zinc-700 text-white text-xs font-bold py-3 px-4 rounded-lg border border-white/10 transition-all flex items-center justify-center space-x-1.5 cursor-pointer select-none">
|
|
{uploadingImage ? 'Enviando...' : 'Upload S3'}
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={handleImageUpload}
|
|
disabled={uploadingImage}
|
|
className="hidden"
|
|
/>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-xs text-zinc-400 font-bold uppercase tracking-wider">Preço do Curso para Desbloqueio (R$)</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
min="0"
|
|
value={courseForm.price}
|
|
onChange={(e) => setCourseForm({ ...courseForm, price: Number(e.target.value) })}
|
|
className="w-full glass-input rounded-lg p-3 text-sm text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-xs text-zinc-400 font-bold uppercase tracking-wider">Modo do Certificado</label>
|
|
<select
|
|
value={courseForm.certificateMode}
|
|
onChange={(e) => setCourseForm({ ...courseForm, certificateMode: e.target.value as 'FULL_COURSE' | 'PER_MODULE' })}
|
|
className="w-full glass-input rounded-lg p-3 text-sm text-white focus:outline-none appearance-none"
|
|
>
|
|
<option value="FULL_COURSE">Por Curso Completo (Ao finalizar tudo)</option>
|
|
<option value="PER_MODULE">Por Módulo (Certificado para cada módulo)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="md:col-span-2 space-y-2 flex flex-col justify-center">
|
|
<label className="text-xs text-zinc-400 font-bold uppercase tracking-wider mb-2">Status de Bloqueio</label>
|
|
<label className="inline-flex items-center space-x-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={courseForm.isLocked}
|
|
onChange={(e) => setCourseForm({ ...courseForm, isLocked: e.target.checked })}
|
|
className="rounded bg-zinc-900 border-white/10 text-[#E50914] focus:ring-[#E50914] focus:ring-offset-0 w-4 h-4"
|
|
/>
|
|
<span className="text-sm font-semibold text-zinc-300">Curso Trancado (Requer pagamento ou desbloqueio manual)</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div className="md:col-span-2 space-y-2">
|
|
<label className="text-xs text-zinc-400 font-bold uppercase tracking-wider">Descrição Detalhada</label>
|
|
<textarea
|
|
required
|
|
value={courseForm.description}
|
|
onChange={(e) => setCourseForm({ ...courseForm, description: e.target.value })}
|
|
placeholder="Fale brevemente sobre o objetivo deste treinamento, o que o aluno vai aprender..."
|
|
className="w-full h-24 glass-input rounded-lg p-3 text-sm text-white focus:outline-none resize-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="md:col-span-2 flex justify-end">
|
|
<button
|
|
type="submit"
|
|
className="glass-btn-primary text-white font-extrabold text-xs md:text-sm py-3 px-6 rounded-xl shadow-lg hover:scale-105 transition-all flex items-center gap-2 cursor-pointer"
|
|
>
|
|
<CheckCircle2 size={18} />
|
|
<span>{editingCourse ? 'Salvar Alterações' : 'Criar Curso'}</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
)}
|
|
|
|
{/* --- RENDER COURSE DETAIL / CURRICULUM MANAGER --- */}
|
|
{selectedCourse ? (
|
|
<div id="admin-content-top" className="space-y-6">
|
|
{/* Active Course Breadcrumb */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 glass-card p-4 rounded-xl">
|
|
<button
|
|
onClick={() => setSelectedCourse(null)}
|
|
className="flex items-center space-x-1.5 text-zinc-400 hover:text-white transition-colors text-xs font-bold"
|
|
>
|
|
<ArrowLeft className="w-4 h-4" />
|
|
<span>Voltar para Catálogo Completo</span>
|
|
</button>
|
|
|
|
<div className="flex items-center space-x-3">
|
|
<span className="text-zinc-600 text-xs font-bold">Curso Selecionado:</span>
|
|
<span className="text-white text-xs font-bold bg-[#E50914] px-2.5 py-1 rounded">
|
|
{selectedCourse.title}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex space-x-2">
|
|
<button
|
|
onClick={() => { setEditingModule(null); setModuleForm({ title: '', order: modules.length + 1 }); setShowModuleForm(true); }}
|
|
className="glass-btn-secondary text-white text-xs font-bold px-3 py-1.5 rounded flex items-center space-x-1"
|
|
>
|
|
<FolderPlus className="w-3.5 h-3.5 text-zinc-400" />
|
|
<span>Criar Módulo</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* --- MODULE FORM DIALOG --- */}
|
|
{showModuleForm && (
|
|
<div className="glass-card p-6 rounded-2xl border-[#E50914]/30 space-y-4">
|
|
<h4 className="text-sm font-bold text-white uppercase tracking-wider">
|
|
{editingModule ? 'Editar Módulo' : 'Adicionar Novo Módulo'}
|
|
</h4>
|
|
<form onSubmit={handleSaveModule} className="flex flex-col sm:flex-row gap-4 items-end">
|
|
<div className="flex-1 space-y-1">
|
|
<label className="text-[10px] text-zinc-500 font-bold uppercase">Título do Módulo</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={moduleForm.title}
|
|
onChange={(e) => setModuleForm({ ...moduleForm, title: e.target.value })}
|
|
placeholder="Ex: Módulo 1 - Fundamentos"
|
|
className="w-full glass-input rounded-lg p-2.5 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
<div className="w-24 space-y-1">
|
|
<label className="text-[10px] text-zinc-500 font-bold uppercase">Ordem</label>
|
|
<input
|
|
type="number"
|
|
required
|
|
value={moduleForm.order}
|
|
onChange={(e) => setModuleForm({ ...moduleForm, order: Number(e.target.value) })}
|
|
className="w-full glass-input rounded-lg p-2.5 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
<div className="flex space-x-2">
|
|
<button type="submit" className="glass-btn-primary text-white font-extrabold text-xs py-2.5 px-6 rounded-xl shadow-lg hover:scale-105 transition-all flex items-center gap-2 cursor-pointer">
|
|
<CheckCircle2 size={16} />
|
|
<span>Salvar Módulo</span>
|
|
</button>
|
|
<button type="button" onClick={() => setShowModuleForm(false)} className="text-xs text-zinc-500 px-2">
|
|
Cancelar
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
)}
|
|
|
|
{/* --- LESSON FORM DIALOG --- */}
|
|
{showLessonForm && (
|
|
<div className="glass-card p-6 rounded-2xl space-y-6">
|
|
<div className="flex items-center justify-between border-b border-white/5 pb-4">
|
|
<h4 className="text-sm font-bold text-white uppercase tracking-wider">
|
|
{editingLesson ? 'Editar Aula' : 'Adicionar Nova Aula'}
|
|
</h4>
|
|
<button type="button" onClick={() => setShowLessonForm(false)} className="text-xs text-zinc-500">
|
|
Cancelar
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSaveLesson} className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div className="md:col-span-2 space-y-2">
|
|
<label className="text-xs text-zinc-400 font-bold uppercase">Título da Aula</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={lessonForm.title}
|
|
onChange={(e) => setLessonForm({ ...lessonForm, title: e.target.value })}
|
|
placeholder="Ex: Como configurar SSH no Servidor Linux"
|
|
className="w-full glass-input rounded-lg p-3 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-xs text-zinc-400 font-bold uppercase">Ordem Numérica</label>
|
|
<input
|
|
type="number"
|
|
required
|
|
value={lessonForm.order}
|
|
onChange={(e) => setLessonForm({ ...lessonForm, order: Number(e.target.value) })}
|
|
className="w-full glass-input rounded-lg p-3 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-xs text-zinc-400 font-bold uppercase">Tipo de Vídeo</label>
|
|
<select
|
|
value={lessonForm.videoType}
|
|
onChange={(e) => setLessonForm({ ...lessonForm, videoType: e.target.value as 'youtube' | 'direct' })}
|
|
className="w-full glass-input rounded-lg p-3 text-xs text-white focus:outline-none"
|
|
>
|
|
<option value="youtube">YouTube Embed (Privado/Não Listado)</option>
|
|
<option value="direct">Upload Direto / Link HTML5 MP4</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="md:col-span-2 space-y-2">
|
|
<label className="text-xs text-zinc-400 font-bold uppercase">
|
|
{lessonForm.videoType === 'youtube'
|
|
? 'Link do Vídeo no YouTube (ou ID do vídeo)'
|
|
: 'Link URL do arquivo de vídeo direto (.mp4 / HLS)'}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={lessonForm.videoUrl}
|
|
onChange={(e) => setLessonForm({ ...lessonForm, videoUrl: e.target.value })}
|
|
placeholder={lessonForm.videoType === 'youtube'
|
|
? 'Ex: https://www.youtube.com/watch?v=K8w3m_pM6lA'
|
|
: 'Ex: https://meuservidor.com/aulas/aula1.mp4'}
|
|
className="w-full glass-input rounded-lg p-3 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="md:col-span-3 space-y-2">
|
|
<label className="text-xs text-zinc-400 font-bold uppercase">Imagem de Capa da Aula (Thumbnail URL - Opcional)</label>
|
|
<input
|
|
type="text"
|
|
value={lessonForm.thumbnailUrl}
|
|
onChange={(e) => setLessonForm({ ...lessonForm, thumbnailUrl: e.target.value })}
|
|
placeholder="Cole a URL da imagem de capa para esta aula (ex: https://meuservidor.com/capa.jpg)"
|
|
className="w-full glass-input rounded-lg p-3 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="md:col-span-3 space-y-2">
|
|
<label className="text-xs text-zinc-400 font-bold uppercase font-sans">Recursos Extras & Material de Apoio (Opcional)</label>
|
|
<textarea
|
|
value={lessonForm.content}
|
|
onChange={(e) => setLessonForm({ ...lessonForm, content: e.target.value })}
|
|
placeholder="Cole códigos de exemplo, links de arquivos zip ou notas de apoio aqui..."
|
|
className="w-full h-24 glass-input rounded-lg p-3 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="md:col-span-3 flex justify-end">
|
|
<button type="submit" className="glass-btn-primary text-white font-extrabold text-xs md:text-sm py-3 px-6 rounded-xl shadow-lg hover:scale-105 transition-all flex items-center gap-2 cursor-pointer">
|
|
<CheckCircle2 size={18} />
|
|
<span>{editingLesson ? 'Salvar Aula' : 'Criar Aula'}</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
)}
|
|
|
|
{/* Curriculum Index (Modules & Lessons list) */}
|
|
<div className="space-y-4">
|
|
{modules.length === 0 ? (
|
|
<div className="p-8 text-center glass-card rounded-xl text-zinc-500">
|
|
<Layers className="w-8 h-8 mx-auto text-zinc-600 mb-2" />
|
|
<p className="text-sm font-semibold">Nenhum módulo cadastrado neste curso.</p>
|
|
<button
|
|
onClick={() => { setEditingModule(null); setModuleForm({ title: '', order: 1 }); setShowModuleForm(true); }}
|
|
className="mt-3 text-xs text-[#E50914] font-bold"
|
|
>
|
|
Criar primeiro módulo agora
|
|
</button>
|
|
</div>
|
|
) : (
|
|
modules.map((mod, modIdx) => (
|
|
<div key={mod.id} className="glass-card rounded-xl overflow-hidden">
|
|
|
|
{/* 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">
|
|
<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>
|
|
<h4 className="font-sans font-extrabold text-sm text-zinc-100 tracking-tight">
|
|
{mod.title}
|
|
</h4>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
{/* Add Lesson */}
|
|
<button
|
|
onClick={() => handleNewLesson(mod.id, mod.lessons.length)}
|
|
className="bg-zinc-800 text-zinc-300 hover:text-white hover:bg-zinc-700 text-[10px] font-bold px-2 py-1 rounded flex items-center space-x-1 border border-zinc-700 transition-colors"
|
|
>
|
|
<FilePlus className="w-3 h-3 text-[#E50914]" />
|
|
<span>Nova Aula</span>
|
|
</button>
|
|
|
|
{/* Manage Activity / Questions */}
|
|
<button
|
|
onClick={() => handleManageActivity(mod.id)}
|
|
className="bg-emerald-600 hover:bg-emerald-500 text-white text-xs font-extrabold px-3 py-1.5 rounded-lg flex items-center space-x-1.5 border border-emerald-400/40 transition-all shadow-[0_0_12px_rgba(16,185,129,0.35)] cursor-pointer"
|
|
title="Criar ou editar o questionário deste módulo"
|
|
>
|
|
<CheckCircle className="w-4 h-4 text-white" />
|
|
<span>📝 Criar / Editar Questões</span>
|
|
</button>
|
|
|
|
{/* Edit Module */}
|
|
<button
|
|
onClick={() => handleEditModule(mod)}
|
|
className="text-zinc-500 hover:text-zinc-300 p-1"
|
|
title="Editar Módulo"
|
|
>
|
|
<Edit className="w-3.5 h-3.5" />
|
|
</button>
|
|
|
|
{/* Delete Module */}
|
|
<button
|
|
onClick={() => handleDeleteModule(mod.id)}
|
|
className="text-zinc-500 hover:text-red-400 p-1"
|
|
title="Excluir Módulo"
|
|
>
|
|
<Trash2 className="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Lessons Listing inside Module */}
|
|
<div className="divide-y divide-white/5 bg-black/10">
|
|
{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">
|
|
<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" />
|
|
) : (
|
|
<Film className="w-4 h-4 text-emerald-500 flex-shrink-0" />
|
|
)}
|
|
<span className="font-semibold text-zinc-300 truncate max-w-sm sm:max-w-md md:max-w-lg">
|
|
{lesson.title}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2 ml-4">
|
|
{/* Edit */}
|
|
<button
|
|
onClick={() => handleEditLesson(lesson)}
|
|
className="text-zinc-500 hover:text-white p-1"
|
|
title="Editar Aula"
|
|
>
|
|
<Edit className="w-3.5 h-3.5" />
|
|
</button>
|
|
{/* Delete */}
|
|
<button
|
|
onClick={() => handleDeleteLesson(lesson.id)}
|
|
className="text-zinc-500 hover:text-red-400 p-1"
|
|
title="Excluir Aula"
|
|
>
|
|
<Trash2 className="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
</div>
|
|
) : (
|
|
/* --- LIST ALL COURSES IN CATALOGUE --- */
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{courses.map(course => (
|
|
<div
|
|
key={course.id}
|
|
className="glass-card rounded-2xl overflow-hidden flex flex-col justify-between"
|
|
>
|
|
<div>
|
|
{/* Thumbnail */}
|
|
<div className="relative w-full aspect-video">
|
|
<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>
|
|
</div>
|
|
|
|
{/* Info */}
|
|
<div className="p-5 space-y-2">
|
|
<h3 className="font-display font-bold text-base text-white tracking-tight line-clamp-1">{course.title}</h3>
|
|
<p className="text-xs text-zinc-500 leading-relaxed line-clamp-3">{course.description}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Actions Footer */}
|
|
<div className="p-5 border-t border-white/5 bg-white/3 flex items-center justify-between">
|
|
<button
|
|
onClick={() => handleSelectCourse(course)}
|
|
className="glass-btn-primary text-white text-xs font-bold px-3.5 py-2 rounded-lg flex items-center space-x-1.5 cursor-pointer shadow-md hover:scale-105 transition-all"
|
|
>
|
|
<Layers className="w-4 h-4 text-white" />
|
|
<span>Gerenciar Módulos & Questões</span>
|
|
<ChevronRight className="w-3.5 h-3.5" />
|
|
</button>
|
|
|
|
<div className="flex space-x-1.5">
|
|
<button
|
|
onClick={() => handleEditCourse(course)}
|
|
className="p-2 text-zinc-400 hover:text-white rounded bg-zinc-800/30 hover:bg-zinc-800 border border-zinc-850 transition-all"
|
|
title="Editar Curso"
|
|
>
|
|
<Edit className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleDeleteCourse(course.id)}
|
|
className="p-2 text-zinc-400 hover:text-red-400 rounded bg-zinc-800/30 hover:bg-zinc-800 border border-zinc-850 transition-all"
|
|
title="Excluir Curso"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* --- ACTIVITY MODAL --- */}
|
|
{showActivityModal && (
|
|
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4 overflow-y-auto">
|
|
<div className="bg-zinc-900 border border-white/10 rounded-2xl w-full max-w-4xl p-6 relative flex flex-col max-h-[90vh]">
|
|
<h3 className="text-xl font-bold text-white mb-4">Gerenciar Atividades do Módulo</h3>
|
|
|
|
<div className="flex-1 overflow-y-auto space-y-6 pr-2">
|
|
{activityQuestions.map((q, qIndex) => (
|
|
<div key={qIndex} className="bg-black/50 border border-white/5 rounded-xl p-4 space-y-4">
|
|
<div className="flex justify-between items-center">
|
|
<h4 className="text-sm font-bold text-zinc-300">Pergunta {qIndex + 1}</h4>
|
|
<button
|
|
onClick={() => setActivityQuestions(activityQuestions.filter((_, i) => i !== qIndex))}
|
|
className="text-red-400 hover:text-red-300 text-xs"
|
|
>
|
|
Remover Pergunta
|
|
</button>
|
|
</div>
|
|
<input
|
|
type="text"
|
|
value={q.question}
|
|
onChange={(e) => {
|
|
const newQ = [...activityQuestions];
|
|
newQ[qIndex].question = e.target.value;
|
|
setActivityQuestions(newQ);
|
|
}}
|
|
placeholder="Digite a pergunta..."
|
|
className="w-full glass-input rounded-lg p-3 text-sm text-white focus:outline-none"
|
|
/>
|
|
|
|
<div className="space-y-2 pl-4 border-l-2 border-white/10">
|
|
<label className="text-xs font-bold text-zinc-500 uppercase">Opções (Marque a correta)</label>
|
|
{q.options.map((opt, optIndex) => (
|
|
<div key={optIndex} className="flex items-center space-x-2">
|
|
<input
|
|
type="radio"
|
|
name={`correct-${qIndex}`}
|
|
checked={q.correctIndex === optIndex}
|
|
onChange={() => {
|
|
const newQ = [...activityQuestions];
|
|
newQ[qIndex].correctIndex = optIndex;
|
|
setActivityQuestions(newQ);
|
|
}}
|
|
className="w-4 h-4"
|
|
/>
|
|
<input
|
|
type="text"
|
|
value={opt}
|
|
onChange={(e) => {
|
|
const newQ = [...activityQuestions];
|
|
newQ[qIndex].options[optIndex] = e.target.value;
|
|
setActivityQuestions(newQ);
|
|
}}
|
|
placeholder={`Opção ${optIndex + 1}`}
|
|
className="w-full bg-zinc-800/50 border border-white/5 rounded p-2 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
<button
|
|
onClick={() => setActivityQuestions([...activityQuestions, { question: '', options: ['', '', '', ''], correctIndex: 0 }])}
|
|
className="w-full py-3 border border-dashed border-white/20 text-zinc-400 hover:text-white hover:border-white/50 rounded-xl text-sm font-bold transition-colors"
|
|
>
|
|
+ Adicionar Pergunta
|
|
</button>
|
|
</div>
|
|
|
|
<div className="mt-6 flex justify-end space-x-4 border-t border-white/10 pt-4">
|
|
<button onClick={() => setShowActivityModal(false)} className="px-4 py-2 text-sm text-zinc-400 hover:text-white">
|
|
Cancelar
|
|
</button>
|
|
<button onClick={handleSaveActivity} className="glass-btn-primary text-white font-extrabold text-xs md:text-sm py-3 px-6 rounded-xl shadow-lg hover:scale-105 transition-all flex items-center gap-2 cursor-pointer">
|
|
<CheckCircle2 size={18} />
|
|
<span>Salvar Atividade</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|