diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 631c5c3..0254c5e 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -81,16 +81,17 @@ model User {
}
model Course {
- id String @id @default(uuid())
- title String
- description String @db.Text
- thumbnail String
- category String
- certificateMode CertificateMode @default(FULL_COURSE) @map("certificate_mode")
- isLocked Boolean @default(false) @map("is_locked")
- price Float?
- order Int @default(0)
- createdAt DateTime @default(now()) @map("created_at")
+ id String @id @default(uuid())
+ title String
+ description String @db.Text
+ thumbnail String
+ category String
+ certificateMode CertificateMode @default(FULL_COURSE) @map("certificate_mode")
+ isLocked Boolean @default(false) @map("is_locked")
+ price Float?
+ order Int @default(0)
+ studyMaterialUrl String? @map("study_material_url") @db.Text
+ createdAt DateTime @default(now()) @map("created_at")
modules Module[]
payments Payment[]
diff --git a/server.ts b/server.ts
index 58de0c2..303982a 100644
--- a/server.ts
+++ b/server.ts
@@ -539,21 +539,28 @@ app.get('/api/courses', authenticateToken, async (req: AuthenticatedRequest, res
});
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 sortedModules = [...course.modules].sort((a, b) => a.order - b.order);
const courseLessons = sortedModules.flatMap(m => {
const sortedLessons = [...m.lessons].sort((a, b) => a.order - b.order);
- return sortedLessons.map(l => ({
- id: l.id,
- title: l.title,
- thumbnailUrl: l.thumbnailUrl,
- order: l.order,
- moduleId: m.id
- }));
+ return sortedLessons.map(l => {
+ const prog = userProgress.find(p => p.lessonId === l.id);
+ return {
+ id: l.id,
+ title: l.title,
+ 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;
@@ -573,6 +580,7 @@ app.get('/api/courses', authenticateToken, async (req: AuthenticatedRequest, res
isLocked: course.isLocked,
price: course.price,
category: course.category,
+ studyMaterialUrl: course.studyMaterialUrl,
totalLessons,
completedLessons,
progress: progressPercentage,
@@ -648,6 +656,7 @@ app.get('/api/courses/:id', authenticateToken, async (req: AuthenticatedRequest,
res.json({
course: {
...courseData,
+ studyMaterialUrl: course.studyMaterialUrl,
isUnlocked
},
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
app.post('/api/admin/modules', authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res: Response) => {
const { courseId, title, order } = req.body;
diff --git a/src/App.tsx b/src/App.tsx
index fc6f9d4..4356a95 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -258,7 +258,7 @@ export default function App() {
setTimeout(() => {
const el = lessonShelfRefs.current[courseId];
if (el) {
- el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 80);
}
@@ -359,9 +359,22 @@ export default function App() {
referrerPolicy="no-referrer"
/>
-
Aulas do Curso
-
{expandedCourse.title}
+
+ Aulas do Curso: {expandedCourse.title}
+
{expandedCourse.lessons.length} aulas disponíveis
+
+ {(expandedCourse as any).studyMaterialUrl && (
+
+
+ Baixar Material (PDF)
+
+ )}