Batch_Melhorias
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 59s Details

This commit is contained in:
Sidney Gomes 2026-07-25 09:28:51 -03:00
parent d4b216b0b6
commit 2bfdfc0240
9 changed files with 332 additions and 109 deletions

View File

@ -81,16 +81,17 @@ model User {
} }
model Course { model Course {
id String @id @default(uuid()) id String @id @default(uuid())
title String title String
description String @db.Text description String @db.Text
thumbnail String thumbnail String
category String category String
certificateMode CertificateMode @default(FULL_COURSE) @map("certificate_mode") certificateMode CertificateMode @default(FULL_COURSE) @map("certificate_mode")
isLocked Boolean @default(false) @map("is_locked") isLocked Boolean @default(false) @map("is_locked")
price Float? price Float?
order Int @default(0) order Int @default(0)
createdAt DateTime @default(now()) @map("created_at") studyMaterialUrl String? @map("study_material_url") @db.Text
createdAt DateTime @default(now()) @map("created_at")
modules Module[] modules Module[]
payments Payment[] payments Payment[]

View File

@ -539,21 +539,28 @@ app.get('/api/courses', authenticateToken, async (req: AuthenticatedRequest, res
}); });
const userProgress = await prisma.progress.findMany({ const userProgress = await prisma.progress.findMany({
where: { userId, completed: true } where: { userId }
}); });
const userProgressLessonIds = userProgress.map(p => p.lessonId); const userProgressLessonIds = userProgress.filter(p => p.completed).map(p => p.lessonId);
const coursesWithProgress = courses.map(course => { const coursesWithProgress = courses.map(course => {
const sortedModules = [...course.modules].sort((a, b) => a.order - b.order); const sortedModules = [...course.modules].sort((a, b) => a.order - b.order);
const courseLessons = sortedModules.flatMap(m => { const courseLessons = sortedModules.flatMap(m => {
const sortedLessons = [...m.lessons].sort((a, b) => a.order - b.order); const sortedLessons = [...m.lessons].sort((a, b) => a.order - b.order);
return sortedLessons.map(l => ({ return sortedLessons.map(l => {
id: l.id, const prog = userProgress.find(p => p.lessonId === l.id);
title: l.title, return {
thumbnailUrl: l.thumbnailUrl, id: l.id,
order: l.order, title: l.title,
moduleId: m.id thumbnailUrl: l.thumbnailUrl,
})); order: l.order,
moduleId: m.id,
progress: prog ? {
watchedPercentage: prog.watchedPercentage,
completed: prog.completed
} : { watchedPercentage: 0, completed: false }
};
});
}); });
const totalLessons = courseLessons.length; const totalLessons = courseLessons.length;
@ -573,6 +580,7 @@ app.get('/api/courses', authenticateToken, async (req: AuthenticatedRequest, res
isLocked: course.isLocked, isLocked: course.isLocked,
price: course.price, price: course.price,
category: course.category, category: course.category,
studyMaterialUrl: course.studyMaterialUrl,
totalLessons, totalLessons,
completedLessons, completedLessons,
progress: progressPercentage, progress: progressPercentage,
@ -648,6 +656,7 @@ app.get('/api/courses/:id', authenticateToken, async (req: AuthenticatedRequest,
res.json({ res.json({
course: { course: {
...courseData, ...courseData,
studyMaterialUrl: course.studyMaterialUrl,
isUnlocked isUnlocked
}, },
modules: modulesWithLessons modules: modulesWithLessons
@ -1616,6 +1625,60 @@ app.delete('/api/admin/courses/:id', authenticateToken, requireAdmin, async (req
} }
}); });
// Upload PDF material de estudo para curso
app.post('/api/admin/courses/:id/upload-material', authenticateToken, requireAdmin, upload.single('file'), async (req: AuthenticatedRequest, res: Response) => {
const courseId = req.params.id;
const file = req.file;
if (!file) {
res.status(400).json({ error: 'Nenhum arquivo enviado.' });
return;
}
const ext = path.extname(file.originalname).toLowerCase();
if (ext !== '.pdf') {
res.status(400).json({ error: 'Apenas arquivos PDF são permitidos.' });
return;
}
try {
const fileName = `materials/${courseId}-${Date.now()}${ext}`;
await s3.send(new PutObjectCommand({
Bucket: 'microtecflix',
Key: fileName,
Body: file.buffer,
ContentType: 'application/pdf',
}));
const publicUrl = `https://s3-estudo.microtecinformaticacurso.com.br/microtecflix/${fileName}`;
const updated = await prisma.course.update({
where: { id: courseId },
data: { studyMaterialUrl: publicUrl }
});
res.json({ success: true, studyMaterialUrl: updated.studyMaterialUrl });
} catch (err: any) {
console.error('Erro ao fazer upload do material:', err);
res.status(500).json({ error: 'Erro ao enviar arquivo para o servidor.' });
}
});
// Remove material de estudo do curso
app.delete('/api/admin/courses/:id/remove-material', authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res: Response) => {
const courseId = req.params.id;
try {
await prisma.course.update({
where: { id: courseId },
data: { studyMaterialUrl: null }
});
res.json({ success: true });
} catch (err) {
res.status(500).json({ error: 'Erro ao remover material.' });
}
});
// 4. CRUD Modules // 4. CRUD Modules
app.post('/api/admin/modules', authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res: Response) => { app.post('/api/admin/modules', authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res: Response) => {
const { courseId, title, order } = req.body; const { courseId, title, order } = req.body;

View File

@ -258,7 +258,7 @@ export default function App() {
setTimeout(() => { setTimeout(() => {
const el = lessonShelfRefs.current[courseId]; const el = lessonShelfRefs.current[courseId];
if (el) { if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); el.scrollIntoView({ behavior: 'smooth', block: 'center' });
} }
}, 80); }, 80);
} }
@ -359,9 +359,22 @@ export default function App() {
referrerPolicy="no-referrer" referrerPolicy="no-referrer"
/> />
<div className="min-w-0"> <div className="min-w-0">
<h3 className="text-[10px] font-extrabold text-[#E50914] uppercase tracking-widest">Aulas do Curso</h3> <h3 className="text-[10px] font-extrabold text-[#E50914] uppercase tracking-widest">
<p className="text-sm md:text-base font-extrabold text-white leading-tight line-clamp-2">{expandedCourse.title}</p> Aulas do Curso: <span className="text-white">{expandedCourse.title}</span>
</h3>
<p className="text-[10px] text-zinc-500 mt-0.5">{expandedCourse.lessons.length} aulas disponíveis</p> <p className="text-[10px] text-zinc-500 mt-0.5">{expandedCourse.lessons.length} aulas disponíveis</p>
{(expandedCourse as any).studyMaterialUrl && (
<a
href={(expandedCourse as any).studyMaterialUrl}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1.5 mt-2 bg-[#E50914]/10 hover:bg-[#E50914]/20 border border-[#E50914]/30 text-[#E50914] text-[10px] font-bold px-2.5 py-1 rounded-md transition-colors"
>
<svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
Baixar Material (PDF)
</a>
)}
</div> </div>
</div> </div>
<button <button
@ -414,6 +427,16 @@ export default function App() {
<span className="absolute top-2 left-2 bg-black/70 text-[#E50914] text-[9px] font-extrabold px-1.5 py-0.5 rounded backdrop-blur-sm border border-white/10"> <span className="absolute top-2 left-2 bg-black/70 text-[#E50914] text-[9px] font-extrabold px-1.5 py-0.5 rounded backdrop-blur-sm border border-white/10">
#{lesson.order} #{lesson.order}
</span> </span>
{/* Progress Bar Overlay */}
{(lesson as any).progress !== undefined && (
<div className="absolute bottom-0 left-0 w-full h-1.5 bg-black/80">
<div
className="h-full bg-[#E50914] transition-all duration-300"
style={{ width: `${(lesson as any).progress.watchedPercentage}%` }}
/>
</div>
)}
</div> </div>
{/* Info below */} {/* Info below */}

View File

@ -2,9 +2,10 @@ import React, { useState, useEffect } from 'react';
import { import {
GraduationCap, Plus, Edit, Trash2, FolderPlus, FilePlus, ChevronRight, GraduationCap, Plus, Edit, Trash2, FolderPlus, FilePlus, ChevronRight,
ArrowLeft, Film, Youtube, RefreshCw, Layers, CheckCircle, CheckCircle2, ArrowLeft, Film, Youtube, RefreshCw, Layers, CheckCircle, CheckCircle2,
GripVertical GripVertical, FileText, Download, X
} from 'lucide-react'; } from 'lucide-react';
import { useModal } from '../contexts/ModalContext'; import { useModal } from '../contexts/ModalContext';
import ToggleSwitch from './ToggleSwitch';
interface Lesson { interface Lesson {
id: string; id: string;
@ -35,6 +36,7 @@ interface Course {
price?: number; price?: number;
certificateMode?: string; certificateMode?: string;
order?: number; order?: number;
studyMaterialUrl?: string;
} }
interface AdminContentManagerProps { interface AdminContentManagerProps {
@ -75,6 +77,8 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
const [activityQuestions, setActivityQuestions] = useState<{question: string, options: string[], correctIndex: number}[]>([]); const [activityQuestions, setActivityQuestions] = useState<{question: string, options: string[], correctIndex: number}[]>([]);
const [uploadingImage, setUploadingImage] = useState(false); const [uploadingImage, setUploadingImage] = useState(false);
const [uploadingMaterial, setUploadingMaterial] = useState(false);
const [editingCourseMaterialUrl, setEditingCourseMaterialUrl] = useState<string | null>(null);
// Drag and Drop States // Drag and Drop States
const [draggedCourseIndex, setDraggedCourseIndex] = useState<number | null>(null); const [draggedCourseIndex, setDraggedCourseIndex] = useState<number | null>(null);
@ -347,6 +351,7 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
certificateMode: (course.certificateMode || 'FULL_COURSE') as 'PER_MODULE' | 'FULL_COURSE', certificateMode: (course.certificateMode || 'FULL_COURSE') as 'PER_MODULE' | 'FULL_COURSE',
order: course.order || 0 order: course.order || 0
}); });
setEditingCourseMaterialUrl(course.studyMaterialUrl || null);
setShowCourseForm(true); setShowCourseForm(true);
setTimeout(() => { setTimeout(() => {
document.getElementById('admin-content-top')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); document.getElementById('admin-content-top')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
@ -709,15 +714,11 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
<div className="md:col-span-2 space-y-2 flex flex-col justify-center"> <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="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"> <ToggleSwitch
<input checked={courseForm.isLocked}
type="checkbox" onChange={(v) => setCourseForm({ ...courseForm, isLocked: v })}
checked={courseForm.isLocked} label="Curso Trancado (Requer pagamento ou desbloqueio manual)"
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>
<div className="md:col-span-2 space-y-2"> <div className="md:col-span-2 space-y-2">
@ -731,6 +732,91 @@ export default function AdminContentManager({ token }: AdminContentManagerProps)
/> />
</div> </div>
{/* Material de Estudo PDF - só aparece ao editar */}
{editingCourse && (
<div className="md:col-span-2 space-y-3 glass-card rounded-xl p-4 border border-white/5">
<label className="text-xs text-zinc-400 font-bold uppercase tracking-wider flex items-center gap-2">
<FileText className="w-3.5 h-3.5 text-[#E50914]" />
Material de Estudo (PDF)
</label>
{editingCourseMaterialUrl ? (
<div className="flex items-center gap-3 bg-zinc-900/50 rounded-lg p-3 border border-white/5">
<FileText className="w-5 h-5 text-[#E50914] flex-shrink-0" />
<a
href={editingCourseMaterialUrl}
target="_blank"
rel="noreferrer"
className="text-xs text-zinc-300 hover:text-white truncate flex-1 underline underline-offset-2"
>
{editingCourseMaterialUrl.split('/').pop()}
</a>
<button
type="button"
onClick={async () => {
await fetch(`/api/admin/courses/${editingCourse.id}/remove-material`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
setEditingCourseMaterialUrl(null);
showSuccess('Material removido!');
}}
className="text-zinc-500 hover:text-red-400 transition-colors p-1"
title="Remover material"
>
<X className="w-4 h-4" />
</button>
</div>
) : (
<p className="text-xs text-zinc-600 italic">Nenhum material de estudo anexado.</p>
)}
<div className="flex items-center gap-3">
<label
className={`flex items-center gap-2 cursor-pointer glass-btn-secondary text-white text-xs font-bold px-4 py-2 rounded-lg transition-all hover:scale-105 ${
uploadingMaterial ? 'opacity-60 pointer-events-none' : ''
}`}
>
<FileText className="w-4 h-4" />
{uploadingMaterial ? 'Enviando PDF...' : 'Upload PDF'}
<input
type="file"
accept=".pdf"
className="hidden"
disabled={uploadingMaterial}
onChange={async (e) => {
const file = e.target.files?.[0];
if (!file) return;
setUploadingMaterial(true);
try {
const formData = new FormData();
formData.append('file', file);
const res = await fetch(`/api/admin/courses/${editingCourse.id}/upload-material`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: formData
});
const data = await res.json();
if (res.ok) {
setEditingCourseMaterialUrl(data.studyMaterialUrl);
showSuccess('Material de estudo enviado com sucesso!');
} else {
showError(data.error || 'Erro ao enviar PDF.');
}
} catch {
showError('Erro de conexão ao enviar PDF.');
} finally {
setUploadingMaterial(false);
}
}}
/>
</label>
<span className="text-[10px] text-zinc-600">Máx. recomendado: 50MB Formato: PDF</span>
</div>
</div>
)}
<div className="md:col-span-2 flex justify-end"> <div className="md:col-span-2 flex justify-end">
<button <button
type="submit" type="submit"

View File

@ -11,6 +11,7 @@ import { AdminPricingManager } from './AdminPricingManager';
import { WhatsappConnectCard } from './WhatsappConnectCard'; import { WhatsappConnectCard } from './WhatsappConnectCard';
import AdminContentManager from './AdminContentManager'; import AdminContentManager from './AdminContentManager';
import AdminMessagesView from './AdminMessagesView'; import AdminMessagesView from './AdminMessagesView';
import ToggleSwitch from './ToggleSwitch';
interface WebhookLog { interface WebhookLog {
id: string; id: string;
@ -986,34 +987,20 @@ export default function AdminDashboard({ token, adminUser, onLogout }: AdminDash
/> />
</div> </div>
<div className="pt-2 md:col-span-2 space-y-1"> <div className="pt-2 md:col-span-2 space-y-1 flex flex-col justify-center">
<label className="flex items-center space-x-3 cursor-pointer bg-black/40 p-3 rounded-lg border border-white/10 hover:border-white/20 transition-colors"> <ToggleSwitch
<input checked={settings.enableBoleto !== false}
type="checkbox" onChange={(v) => setSettings({ ...settings, enableBoleto: v })}
checked={settings.enableBoleto !== false} label="Habilitar Boleto Bancário (Permite pagamentos à vista)"
onChange={(e) => setSettings({ ...settings, enableBoleto: e.target.checked })} />
className="rounded bg-zinc-900 border-white/10 text-[#E50914] focus:ring-[#E50914] focus:ring-offset-0 w-4 h-4 cursor-pointer"
/>
<div className="flex flex-col">
<span className="text-xs font-bold text-white">Habilitar Boleto Bancário</span>
<span className="text-[10px] text-zinc-400">Permite pagamentos à vista via boleto bancário.</span>
</div>
</label>
</div> </div>
<div className="pt-2 md:col-span-2 space-y-1"> <div className="pt-2 md:col-span-2 space-y-1 flex flex-col justify-center">
<label className="flex items-center space-x-3 cursor-pointer bg-black/40 p-3 rounded-lg border border-white/10 hover:border-white/20 transition-colors"> <ToggleSwitch
<input checked={settings.enableInstallmentBoleto === true}
type="checkbox" onChange={(v) => setSettings({ ...settings, enableInstallmentBoleto: v })}
checked={settings.enableInstallmentBoleto === true} label="Habilitar Boleto Parcelado (Permite gerar boletos parcelados)"
onChange={(e) => setSettings({ ...settings, enableInstallmentBoleto: e.target.checked })} />
className="rounded bg-zinc-900 border-white/10 text-[#E50914] focus:ring-[#E50914] focus:ring-offset-0 w-4 h-4 cursor-pointer"
/>
<div className="flex flex-col">
<span className="text-xs font-bold text-white">Habilitar Boleto Parcelado</span>
<span className="text-[10px] text-zinc-400">Permite gerar boletos parcelados para compra de cursos.</span>
</div>
</label>
</div> </div>
{settings.enableInstallmentBoleto && ( {settings.enableInstallmentBoleto && (
@ -1460,17 +1447,13 @@ export default function AdminDashboard({ token, adminUser, onLogout }: AdminDash
/> />
</div> </div>
<div className="space-y-2 flex flex-col justify-center"> <div className="space-y-2 flex flex-col justify-center mt-4">
<label className="flex items-center space-x-3 cursor-pointer mt-4"> <ToggleSwitch
<input checked={settings.smtpSecure || false}
type="checkbox" onChange={(v) => setSettings({...settings, smtpSecure: v})}
checked={settings.smtpSecure || false} label="Usar Conexão Segura (SSL/TLS)"
onChange={(e) => setSettings({...settings, smtpSecure: e.target.checked})} />
className="form-checkbox h-5 w-5 text-[#E50914] rounded border-white/10 bg-black/40 focus:ring-[#E50914] focus:ring-offset-black" <p className="text-[10px] text-zinc-500 mt-1 pl-14">Marque se a porta for 465. Para 587 (STARTTLS), normalmente fica desmarcado.</p>
/>
<span className="text-sm font-bold text-white">Usar Conexão Segura (SSL/TLS)</span>
</label>
<p className="text-[10px] text-zinc-500 mt-1 pl-8">Marque se a porta for 465. Para 587 (STARTTLS), normalmente fica desmarcado.</p>
</div> </div>
</div> </div>

View File

@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { RefreshCw, Save, Plus, Trash2, Edit, CheckCircle2, AlertTriangle, BookOpen, Layers, DollarSign } from 'lucide-react'; import { RefreshCw, Save, Plus, Trash2, Edit, CheckCircle2, AlertTriangle, BookOpen, Layers, DollarSign } from 'lucide-react';
import { Course, Plan } from '../types'; import { Course, Plan } from '../types';
import { useModal } from '../contexts/ModalContext'; import { useModal } from '../contexts/ModalContext';
import ToggleSwitch from './ToggleSwitch';
interface AdminPricingManagerProps { interface AdminPricingManagerProps {
courses: Course[]; courses: Course[];
@ -374,18 +375,13 @@ export const AdminPricingManager: React.FC<AdminPricingManagerProps> = ({ course
</div> </div>
</div> </div>
<label className="flex items-center gap-3 cursor-pointer p-3 bg-black/20 rounded-lg border border-white/5 hover:bg-black/40 transition-colors"> <div className="flex flex-col justify-center">
<input <ToggleSwitch
type="checkbox"
checked={planForm.active} checked={planForm.active}
onChange={e => setPlanForm({...planForm, active: e.target.checked})} onChange={v => setPlanForm({...planForm, active: v})}
className="w-5 h-5 rounded border-zinc-600 text-purple-600 focus:ring-purple-600 focus:ring-offset-zinc-900 bg-zinc-800" label="Plano Ativo (Visível para os alunos comprarem)"
/> />
<div> </div>
<span className="text-sm font-bold text-white block">Plano Ativo (Visível)</span>
<span className="text-xs text-zinc-500">Alunos poderão comprar este plano.</span>
</div>
</label>
</div> </div>
{/* Course Selection */} {/* Course Selection */}
@ -400,41 +396,34 @@ export const AdminPricingManager: React.FC<AdminPricingManagerProps> = ({ course
</div> </div>
<div className="p-4 overflow-y-auto space-y-2 flex-grow custom-scrollbar"> <div className="p-4 overflow-y-auto space-y-2 flex-grow custom-scrollbar">
<label className={`flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-all ${ <div className={`p-3 rounded-lg border transition-all ${
planForm.includedCourses?.length === 0 planForm.includedCourses?.length === 0
? 'bg-purple-900/20 border-purple-500/50' ? 'bg-purple-900/20 border-purple-500/50'
: 'bg-zinc-900/50 border-white/5 hover:border-white/20' : 'bg-zinc-900/50 border-white/5'
}`}> }`}>
<input <ToggleSwitch
type="checkbox"
checked={planForm.includedCourses?.length === 0} checked={planForm.includedCourses?.length === 0}
onChange={() => setPlanForm({...planForm, includedCourses: []})} onChange={(v) => { if(v) setPlanForm({...planForm, includedCourses: []}) }}
className="w-5 h-5 rounded-full border-zinc-600 text-purple-600 focus:ring-purple-600 bg-zinc-800" label="Acesso Global (All-Access) - Libera todos os cursos, presentes e futuros."
/> />
<div> </div>
<span className="text-sm font-bold text-white block">Acesso Global (All-Access)</span>
<span className="text-xs text-zinc-400">Libera todos os cursos, presentes e futuros.</span>
</div>
</label>
<div className="my-4 border-t border-white/10 relative"> <div className="my-4 border-t border-white/10 relative">
<span className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-black px-2 text-xs font-bold text-zinc-600">OU ESCOLHA ESPECÍFICOS</span> <span className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-black px-2 text-xs font-bold text-zinc-600">OU ESCOLHA ESPECÍFICOS</span>
</div> </div>
{courses.map(course => ( {courses.map(course => (
<label key={course.id} className={`flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-all ${ <div key={course.id} className={`p-3 rounded-lg border transition-all ${
planForm.includedCourses?.includes(course.id) planForm.includedCourses?.includes(course.id)
? 'bg-purple-900/20 border-purple-500/50' ? 'bg-purple-900/20 border-purple-500/50'
: 'bg-zinc-900/50 border-white/5 hover:border-white/20' : 'bg-zinc-900/50 border-white/5'
}`}> }`}>
<input <ToggleSwitch
type="checkbox" checked={planForm.includedCourses?.includes(course.id) || false}
checked={planForm.includedCourses?.includes(course.id)}
onChange={() => toggleCourseInPlan(course.id)} onChange={() => toggleCourseInPlan(course.id)}
className="w-5 h-5 rounded border-zinc-600 text-purple-600 focus:ring-purple-600 bg-zinc-800" label={course.title}
/> />
<span className="text-sm font-medium text-zinc-300 truncate">{course.title}</span> </div>
</label>
))} ))}
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { ArrowLeft, BookOpen, ChevronRight, FileText, CheckCircle2, Circle, MessageSquare, AlertCircle, Award } from 'lucide-react'; import { ArrowLeft, BookOpen, ChevronRight, FileText, CheckCircle2, Circle, MessageSquare, AlertCircle, Award, Download } from 'lucide-react';
import VideoPlayer from './VideoPlayer'; import VideoPlayer from './VideoPlayer';
import CourseActivity from './CourseActivity'; import CourseActivity from './CourseActivity';
@ -34,6 +34,7 @@ interface Course {
description: string; description: string;
thumbnail: string; thumbnail: string;
category: string; category: string;
studyMaterialUrl?: string;
} }
interface CourseViewProps { interface CourseViewProps {
@ -331,9 +332,23 @@ export default function CourseView({ courseId, token, onBack, initialLessonId }:
{/* Right Hand: Interactive Modules Accordion */} {/* Right Hand: Interactive Modules Accordion */}
<div className="space-y-4"> <div className="space-y-4">
<div className="glass-card p-4 rounded-xl space-y-1"> <div className="glass-card p-4 rounded-xl space-y-3">
<h3 className="font-sans font-extrabold text-sm text-white tracking-tight">Conteúdo do Curso</h3> <div>
<p className="text-xs text-zinc-500 font-medium">{course.title}</p> <h3 className="font-sans font-extrabold text-sm text-white tracking-tight">Conteúdo do Curso</h3>
<p className="text-xs text-zinc-500 font-medium mt-1">{course.title}</p>
</div>
{course.studyMaterialUrl && (
<a
href={course.studyMaterialUrl}
target="_blank"
rel="noreferrer"
className="flex items-center justify-center gap-2 w-full glass-btn-secondary text-[#E50914] text-xs font-bold py-2 px-3 rounded-lg hover:scale-[1.02] transition-all"
>
<Download className="w-4 h-4" />
Baixar Material de Estudo
</a>
)}
</div> </div>
<div className="space-y-3"> <div className="space-y-3">

View File

@ -91,13 +91,23 @@ export default function HeroBanner({ course, onPlay, onDetail, subscriptionStatu
</button> </button>
)} )}
<button {course.progress > 0 ? (
onClick={() => onDetail(course.id)} <button
className="glass-btn-secondary text-white font-medium px-5 py-2.5 rounded-lg flex items-center space-x-2 cursor-pointer" onClick={() => onPlay(course.id)}
> className="glass-btn-secondary text-white font-medium px-5 py-2.5 rounded-lg flex items-center space-x-2 cursor-pointer"
<Info className="w-5 h-5 text-zinc-300" /> >
<span>Ver Detalhes</span> <Info className="w-5 h-5 text-zinc-300" />
</button> <span>Continuar Treinamento</span>
</button>
) : (
<button
onClick={() => onDetail(course.id)}
className="glass-btn-secondary text-white font-medium px-5 py-2.5 rounded-lg flex items-center space-x-2 cursor-pointer"
>
<Info className="w-5 h-5 text-zinc-300" />
<span>Ver Detalhes</span>
</button>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,53 @@
import React from 'react';
interface ToggleSwitchProps {
checked: boolean;
onChange: (checked: boolean) => void;
label?: string;
disabled?: boolean;
id?: string;
}
export default function ToggleSwitch({ checked, onChange, label, disabled = false, id }: ToggleSwitchProps) {
const uniqueId = id || `toggle-${Math.random().toString(36).slice(2, 9)}`;
return (
<label
htmlFor={uniqueId}
className={`inline-flex items-center gap-2.5 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
>
<div className="relative">
<input
id={uniqueId}
type="checkbox"
className="sr-only peer"
checked={checked}
disabled={disabled}
onChange={(e) => onChange(e.target.checked)}
/>
{/* Track */}
<div
className={`
w-11 h-6 rounded-full border transition-all duration-300
${checked
? 'bg-[#E50914] border-[#E50914] shadow-[0_0_10px_rgba(229,9,20,0.35)]'
: 'bg-zinc-800 border-zinc-700'
}
`}
/>
{/* Thumb */}
<div
className={`
absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white shadow-md
transition-all duration-300 transform
${checked ? 'translate-x-5' : 'translate-x-0'}
`}
/>
</div>
{label && (
<span className="text-sm font-medium text-zinc-300 select-none">
{label}
</span>
)}
</label>
);
}