From 815d3f93c1e83c060a46409b51734528d95ec460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Tue, 8 Sep 2026 09:30:01 -0300 Subject: [PATCH] feat: group cms courses by trail --- pages/SuperAdmin.tsx | 11 +++++++++-- server/src/routes/admin.ts | 6 +++++- services/api.ts | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/pages/SuperAdmin.tsx b/pages/SuperAdmin.tsx index c3e8ebb..ff294d5 100644 --- a/pages/SuperAdmin.tsx +++ b/pages/SuperAdmin.tsx @@ -4,9 +4,10 @@ import { adminApi, instructorApi, HomeBanner, HomeBannerInput, LearningPath, Lea import { useAuth } from '../context/AuthContext'; import { useToast } from '../context/ToastContext'; import { useNavigate } from 'react-router-dom'; +import { CourseCoverImage } from '../components/CourseCoverImage'; const roleLabel: Record = { admin: 'Superadmin', instructor: 'Instrutor', student: 'Aluno' }; -type CourseOption = { id: string; title: string; status: string }; +type CourseOption = { id: string; title: string; status: string; category: string; coverImageUrl: string | null }; type HomeConfiguration = { featuredCourseId: string | null; courseOrder: string[]; defaultCoverImageUrl: string | null; courses: CourseOption[] }; const emptyPathInput = (): LearningPathInput => ({ title: '', description: '', coverImageUrl: null, coverImageZoom: 1, coverImagePositionX: 50, coverImagePositionY: 50, status: 'draft', courseIds: [] }); const pathToInput = (path: LearningPath): LearningPathInput => ({ title: path.title, description: path.description, coverImageUrl: path.coverImageUrl, coverImageZoom: path.coverImageZoom, coverImagePositionX: path.coverImagePositionX, coverImagePositionY: path.coverImagePositionY, status: path.status === 'published' ? 'published' : 'draft', courseIds: path.courses.map((course) => course.id) }); @@ -83,7 +84,13 @@ const BannerManager: React.FC<{ courses: CourseOption[] }> = ({ courses }) => { return

Banners da página inicial

Envie imagens próprias ou use a capa de cursos publicados. Mais de um banner vira um carrossel.

{banners.map((banner, index) =>
{(banner.imageUrl || banner.courseImageUrl) && }

{banner.kind === 'course' ? banner.courseTitle : banner.title || 'Banner personalizado'}

{banner.kind === 'course' ? 'Capa do curso' : 'Imagem enviada'}

)}
{ const file = event.target.files?.[0]; if (file) void upload(file); event.currentTarget.value = ''; }} />
; }; -const CoursesPanel: React.FC<{ courses: CourseOption[]; onManage: () => void }> = ({ courses, onManage }) =>

Cursos

Edite ou exclua cursos rapidamente no gerenciador.

{courses.length === 0 ?

Nenhum curso cadastrado.

: courses.map((course) =>
{course.title}{course.status === 'published' ? 'Publicado' : 'Rascunho'}
)}
; +const CoursesPanel: React.FC<{ courses: CourseOption[]; onManage: () => void }> = ({ courses, onManage }) => { + const byTrail: Record = {}; + courses.forEach((course) => { + (byTrail[course.category] ||= []).push(course); + }); + return

Cursos

Visualize cada curso pela sua capa e Trilha.

{courses.length === 0 ?

Nenhum curso cadastrado.

:
{Object.entries(byTrail).sort(([a], [b]) => a.localeCompare(b, 'pt-BR')).map(([trail, trailCourses]) =>

{trail}

{trailCourses.length} {trailCourses.length === 1 ? 'curso' : 'cursos'}
{trailCourses.map((course) =>
{course.coverImageUrl ? :
}

{course.title}

{course.status === 'published' ? 'Publicado' : 'Rascunho'}
)}
)}
}
; +}; const PathManager: React.FC<{ title: string; description: string; paths: LearningPath[]; onNew: () => void; onEdit: (path: LearningPath) => void; onDelete: (path: LearningPath) => void }> = ({ title, description, paths, onNew, onEdit, onDelete }) =>

{title}

{description}

{paths.length === 0 ?
Nenhuma {title.toLowerCase()} criada ainda.
: paths.map((path) =>
{path.coverImageUrl ? : }

{path.title}

{path.courses.length} {path.courses.length === 1 ? 'curso' : 'cursos'} · {path.status === 'published' ? 'Publicada' : 'Rascunho'}

)}
; diff --git a/server/src/routes/admin.ts b/server/src/routes/admin.ts index f57029e..28c975d 100644 --- a/server/src/routes/admin.ts +++ b/server/src/routes/admin.ts @@ -266,7 +266,11 @@ export const adminRoutes: FastifyPluginAsync = async (app) => { app.get('/home-configuration', adminAccess, async () => { const [settings, courses] = await Promise.all([ pool.query<{ key: string; value: unknown }>(`select key, value from platform_settings where key in ('home.featuredCourseId', 'home.courseOrder', 'media.defaultCoverImageUrl')`), - pool.query<{ id: string; title: string; status: string }>(`select id, title, status from courses where status <> 'archived' order by title`), + pool.query<{ id: string; title: string; status: string; category: string; coverImageUrl: string | null }>( + `select id, title, status, category, + coalesce(cover_image_url, (select value #>> '{}' from platform_settings where key = 'media.defaultCoverImageUrl')) as "coverImageUrl" + from courses where status <> 'archived' order by category, title`, + ), ]); const values = new Map(settings.rows.map((row) => [row.key, row.value])); return { data: { diff --git a/services/api.ts b/services/api.ts index 05b2a79..bc67d72 100644 --- a/services/api.ts +++ b/services/api.ts @@ -184,7 +184,7 @@ export const adminApi = { async deletePath(pathId: string) { return apiRequest(`/admin/paths/${pathId}`, { method: 'DELETE' }); }, async homeBanners() { return apiRequest<{ data: HomeBanner[] }>('/admin/home-banners'); }, async updateHomeBanners(banners: HomeBannerInput[]) { return apiRequest<{ data: HomeBanner[] }>('/admin/home-banners', { method: 'PUT', body: JSON.stringify({ banners }) }); }, - async homeConfiguration() { return apiRequest<{ data: { featuredCourseId: string | null; courseOrder: string[]; defaultCoverImageUrl: string | null; courses: Array<{ id: string; title: string; status: string }> } }>('/admin/home-configuration'); }, + async homeConfiguration() { return apiRequest<{ data: { featuredCourseId: string | null; courseOrder: string[]; defaultCoverImageUrl: string | null; courses: Array<{ id: string; title: string; status: string; category: string; coverImageUrl: string | null }> } }>('/admin/home-configuration'); }, async updateHomeConfiguration(input: { featuredCourseId: string | null; courseOrder: string[]; defaultCoverImageUrl: string | null }) { return apiRequest<{ data: typeof input }>('/admin/home-configuration', { method: 'PUT', body: JSON.stringify(input) }); }, };