Compare commits
2 Commits
f5aa826b32
...
61359a22a5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61359a22a5 | ||
|
|
bdaff3f5df |
@@ -634,7 +634,7 @@ const CourseModal: React.FC<{
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 uppercase font-bold tracking-wider mb-1 block">Publicação</label>
|
||||
<select value={status} onChange={(e) => setStatus(e.target.value as Course['status'])} className="w-full bg-black/50 text-white border border-white/10 rounded-xl px-3 py-2.5 text-xs focus:outline-none focus:border-orange-500/50">
|
||||
<option value="draft" className="bg-zinc-900">Salvar como rascunho</option>
|
||||
<option value="draft" className="bg-zinc-900">Salvar como não publicado</option>
|
||||
<option value="published" className="bg-zinc-900">Publicar agora</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -745,7 +745,7 @@ const CourseModal: React.FC<{
|
||||
</div>
|
||||
<div className="mt-5 flex flex-col-reverse gap-3 border-t border-white/5 pt-5 sm:flex-row sm:justify-between">
|
||||
<button type="button" onClick={() => setEditorStep(2)} className="inline-flex items-center justify-center gap-2 rounded-xl px-4 py-3 text-sm font-semibold text-gray-300 hover:bg-white/5 hover:text-white"><ChevronLeft className="w-4 h-4" /> Voltar para aulas</button>
|
||||
<button type="button" onClick={() => void handleSubmit()} disabled={isSubmitting} className="inline-flex items-center justify-center gap-2 rounded-xl bg-orange-500 px-6 py-3 text-sm font-extrabold text-white shadow-lg shadow-orange-500/20 transition-colors hover:bg-orange-600 disabled:opacity-60">{isSubmitting ? 'Salvando...' : status === 'draft' ? 'Salvar como rascunho' : 'Salvar e publicar'}</button>
|
||||
<button type="button" onClick={() => void handleSubmit()} disabled={isSubmitting} className="inline-flex items-center justify-center gap-2 rounded-xl bg-orange-500 px-6 py-3 text-sm font-extrabold text-white shadow-lg shadow-orange-500/20 transition-colors hover:bg-orange-600 disabled:opacity-60">{isSubmitting ? 'Salvando...' : status === 'draft' ? 'Salvar como não publicado' : 'Salvar e publicar'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -970,7 +970,7 @@ export const ManageCourses: React.FC = () => {
|
||||
const handleDuplicate = async (id: string) => {
|
||||
try {
|
||||
await duplicateCourse(id);
|
||||
showToast('Cópia criada como rascunho.', 'success');
|
||||
showToast('Cópia criada como não publicada.', 'success');
|
||||
loadData();
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : 'Não foi possível duplicar este curso.', 'error');
|
||||
@@ -1123,7 +1123,7 @@ export const ManageCourses: React.FC = () => {
|
||||
<div className="absolute top-3 left-3 bg-black/70 backdrop-blur-md px-2.5 py-1 rounded-[980px] text-caption font-semibold uppercase tracking-caption text-white border border-white/10">
|
||||
{course.category}
|
||||
</div>
|
||||
{course.status === 'draft' && <div className="absolute top-3 right-3 bg-amber-500/20 backdrop-blur-md px-2.5 py-1 rounded-[980px] text-caption font-semibold uppercase tracking-caption text-amber-300 border border-amber-500/30">Rascunho</div>}
|
||||
{course.status === 'draft' && <div className="absolute top-3 right-3 bg-amber-500/20 backdrop-blur-md px-2.5 py-1 rounded-[980px] text-caption font-semibold uppercase tracking-caption text-amber-300 border border-amber-500/30">Não publicado</div>}
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -58,7 +58,15 @@ const learningPathSchema = z.object({
|
||||
coverImagePositionX: z.number().min(0).max(100).default(50),
|
||||
coverImagePositionY: z.number().min(0).max(100).default(50),
|
||||
status: z.enum(['draft', 'published']).default('draft'),
|
||||
courseIds: z.array(z.string().uuid()).min(1).max(100),
|
||||
courseIds: z.array(z.string().uuid()).max(100),
|
||||
}).superRefine((input, context) => {
|
||||
if (input.status === 'published' && input.courseIds.length === 0) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['courseIds'],
|
||||
message: 'Adicione pelo menos um curso antes de publicar a trilha.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
type LearningPathInput = z.infer<typeof learningPathSchema>;
|
||||
@@ -143,6 +151,17 @@ export const adminRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
app.get('/paths', adminAccess, async () => ({ data: await getLearningPaths() }));
|
||||
|
||||
app.get('/path-courses', adminAccess, async () => {
|
||||
const result = await 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`,
|
||||
);
|
||||
return { data: result.rows };
|
||||
});
|
||||
|
||||
app.get('/home-banners', adminAccess, async () => ({ data: await selectHomeBanners() }));
|
||||
|
||||
app.put('/home-banners', adminAccess, async (request) => {
|
||||
@@ -189,7 +208,7 @@ export const adminRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.code(201).send({ data: paths.find((item) => item.id === path.rows[0].id) });
|
||||
} catch (error) {
|
||||
await client.query('rollback');
|
||||
if (error instanceof Error && /course|Publish/.test(error.message)) return reply.code(400).send({ error: error.message });
|
||||
if (error instanceof Error && /curso|publique/i.test(error.message)) return reply.code(400).send({ error: error.message });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
@@ -223,7 +242,7 @@ export const adminRoutes: FastifyPluginAsync = async (app) => {
|
||||
return { data: paths.find((item) => item.id === pathId) };
|
||||
} catch (error) {
|
||||
await client.query('rollback');
|
||||
if (error instanceof Error && /course|Publish/.test(error.message)) return reply.code(400).send({ error: error.message });
|
||||
if (error instanceof Error && /curso|publique/i.test(error.message)) return reply.code(400).send({ error: error.message });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
|
||||
@@ -74,6 +74,13 @@ export interface LearningPathInput {
|
||||
status: 'draft' | 'published';
|
||||
courseIds: string[];
|
||||
}
|
||||
export interface TrailCourseOption {
|
||||
id: string;
|
||||
title: string;
|
||||
status: 'draft' | 'published';
|
||||
category: string;
|
||||
coverImageUrl: string | null;
|
||||
}
|
||||
export interface HomeBannerInput {
|
||||
kind: 'course' | 'custom';
|
||||
courseId: string | null;
|
||||
@@ -197,6 +204,7 @@ export const adminApi = {
|
||||
async createCategory(name: string) { return apiRequest<{ data: { id: string; name: string; position: number; isActive: boolean } }>('/admin/categories', { method: 'POST', body: JSON.stringify({ name }) }); },
|
||||
async updateCategory(categoryId: string, update: { name: string; isActive?: boolean; position?: number }) { return apiRequest<{ data: { id: string; name: string; position: number; isActive: boolean } }>(`/admin/categories/${categoryId}`, { method: 'PATCH', body: JSON.stringify(update) }); },
|
||||
async listPaths() { return apiRequest<{ data: LearningPath[] }>('/admin/paths'); },
|
||||
async listPathCourses() { return apiRequest<{ data: TrailCourseOption[] }>('/admin/path-courses'); },
|
||||
async createPath(input: LearningPathInput) { return apiRequest<{ data: LearningPath }>('/admin/paths', { method: 'POST', body: JSON.stringify(input) }); },
|
||||
async updatePath(pathId: string, input: LearningPathInput) { return apiRequest<{ data: LearningPath }>(`/admin/paths/${pathId}`, { method: 'PATCH', body: JSON.stringify(input) }); },
|
||||
async deletePath(pathId: string) { return apiRequest<void>(`/admin/paths/${pathId}`, { method: 'DELETE' }); },
|
||||
|
||||
Reference in New Issue
Block a user