From 714e59e8bc7597f02de24e73b4bde6dae7ac523c Mon Sep 17 00:00:00 2001 From: Sidney Gomes Date: Fri, 24 Jul 2026 23:22:39 -0300 Subject: [PATCH] feat: reverter cards de curso para retrato vertical e aplicar efeito glass hover no carrossel de aulas --- .agents/AGENTS.md | 8 ++ gemini.md | 26 ++++- src/App.tsx | 3 +- src/components/CourseCard.tsx | 173 ++++++++++++++---------------- src/components/CourseCarousel.tsx | 4 +- 5 files changed, 114 insertions(+), 100 deletions(-) diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md index 725adb8..3a55904 100644 --- a/.agents/AGENTS.md +++ b/.agents/AGENTS.md @@ -33,5 +33,13 @@ Este arquivo define as restrições comportamentais e as diretrizes arquiteturai ## 5. Integração com Evolution API v2 & CI/CD * **Parâmetro Integration**: Nas chamadas de criação de instância da Evolution API v2 (`/instance/create`), forneça sempre o parâmetro `"integration": "WHATSAPP-BAILEYS"` para evitar erros `400 Bad Request` na API. +* **Envio de Mensagens (REST)**: O envio de mensagens deve ser feito via HTTP (usando `fetch`) para a rota `/message/sendText/:instance`, com a `apikey` no Header e utilizando o parâmetro `delay: 1200` para estabilidade. **Não** deve-se utilizar SDK do Baileys diretamente no Node.js para conectar; o servidor deve se comunicar estritamente como cliente REST da API Evolution. * **Pipeline do Gitea Runner**: O deploy na VPS é automatizado através de um runner de Gitea Actions conectado ao Portainer. Não há necessidade de build e deploy manual no Portainer ao enviar para a branch `main`. +--- + +## 6. Interface do Usuário (UI) & Modais +* **Proibição de Alertas Nativos**: É **ESTRITAMENTE PROIBIDO** utilizar `alert()`, `window.alert()`, `confirm()`, `window.confirm()` ou `prompt()`. +* **Sistema de Modais Global**: Todo o aplicativo é envolvido por um ``. Utilize o hook `useModal` (importado de `src/contexts/ModalContext.tsx`) que retorna os helpers `{ alert, success, error, confirm }`. +* **Componentes Compartilhados**: Utilize o componente `PaymentCheckoutCard` para cobranças de planos, combos ou cursos avulsos. O layout é unificado e o componente se adapta dinamicamente às propriedades fornecidas. +* **Políticas e Termos**: Textos longos de uso e políticas são exibidos via modais (`useModal`) injetados com o conteúdo de banco de dados (`SaaSConfig`), mantendo o aluno no contexto do App sem recarregar a página. diff --git a/gemini.md b/gemini.md index 6ba2475..5fc34e1 100644 --- a/gemini.md +++ b/gemini.md @@ -163,7 +163,31 @@ Passos executados: ## 7. Integração Evolution API v2 & Runner Gitea -* **Evolution API v2**: A partir da versão `2.3.x`, a rota de criação de instâncias (`POST /instance/create`) obrigatoriamente requer o parâmetro `"integration": "WHATSAPP-BAILEYS"` no corpo da requisição. Sem isso, a API retorna `400 Bad Request`. +* **Evolution API v2**: A partir da versão `2.3.x`, a rota de criação de instâncias (`POST /instance/create`) obrigatoriamente requer o parâmetro `"integration": "WHATSAPP-BAILEYS"` no corpo da requisição. Sem isso, a API retorna `400 Bad Request`. Para o envio de mensagens pelo backend (Node.js), utilizamos chamadas REST com `fetch` na rota `/message/sendText/:instance`, enviando o cabeçalho `apikey`. * **Deploy Contínuo (Gitea Actions)**: O projeto utiliza um Gitea Runner que compila a aplicação (`npm run build`) e faz deploy automático no Portainer. Qualquer alteração empurrada na branch `main` executa a pipeline e substitui a imagem `microtecflix-app:local` de forma transparente. +--- + +## 8. Sistema Global de Modais (UI Nativa) + +* **Proibição de Alertas Nativos**: O uso de `window.alert()` e `window.confirm()` está estritamente proibido. Todas as interações de sistema (confirmações de exclusão, avisos de erro, sucesso) devem utilizar o `ModalProvider` configurado globalmente. +* **Como usar**: Importe o hook `useModal` de `src/contexts/ModalContext.tsx`. Ele provê os métodos `alert`, `success`, `error` e `confirm`. +* **Renderização**: Os modais utilizam a mesma estilização padrão "glassmorphism" do sistema e suportam animações CSS (`animate-fade-in` e `animate-slide-up`). + +--- + +## 9. Termos de Uso, Política de Privacidade e Configurações de SaaS + +* Os textos longos de **Termos de Uso** e **Política de Privacidade** são dinamicamente gravados na tabela `SaaSConfig` do PostgreSQL. +* No **Painel do Aluno**, no rodapé, ao invés de navegar para uma nova página, os links devem abrir Modais flutuantes que renderizam o conteúdo armazenado. +* No **Painel do Admin**, esses dados são editáveis em caixas de texto longas na aba de "Configurações do SaaS". + +--- + +## 10. Assinaturas, Cursos e Certificados + +* **Venda de Cursos (Checkout)**: A tela "Minha Assinatura" (`SubscriptionView.tsx`) exibe os Planos Dinâmicos criados no Admin. Removemos referências locais (hardcoded) a combos ou assinaturas padrão; tudo vem do banco de dados (tabela `Plan`). +* **Cartão de Pagamento**: O componente `PaymentCheckoutCard.tsx` é global e flexível, mudando os valores do Pix/Boleto/Cartão dinamicamente com base no curso avulso ou assinatura que o aluno estiver comprando. +* **Modo do Certificado**: Cada Curso agora tem o campo `certificateMode` no Prisma (opções: `FULL_COURSE` ou `PER_MODULE`), o que define como a lógica de desbloqueio do certificado se comporta. + diff --git a/src/App.tsx b/src/App.tsx index 1ddeda1..955bcfb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -329,7 +329,6 @@ export default function App() { onSelectCourse={handleSelectCourse} onUnlockCourse={(course) => setUnlockingCourse(course)} subscriptionStatus={user.subscriptionStatus} - selectedCourseId={expandedCourseId} /> {/* Expanded Lessons Shelf */} @@ -360,7 +359,7 @@ export default function App() { setSelectedLessonId(lesson.id); setView('course-view'); }} - className="flex-shrink-0 w-60 md:w-64 bg-zinc-900/80 border border-white/5 rounded-xl overflow-hidden hover:border-[#E50914]/80 transition-all duration-300 group/lesson cursor-pointer snap-start" + className="flex-shrink-0 w-60 md:w-64 glass-card glass-card-hover rounded-xl overflow-hidden cursor-pointer snap-start transition-all duration-300 group/lesson" >
{lesson.thumbnailUrl ? ( diff --git a/src/components/CourseCard.tsx b/src/components/CourseCard.tsx index 946ddd9..197a19a 100644 --- a/src/components/CourseCard.tsx +++ b/src/components/CourseCard.tsx @@ -8,25 +8,12 @@ interface CourseCardProps { onSelect: (courseId: string) => void; onUnlockRequest?: (course: any) => void; subscriptionStatus: string; - isSelected?: boolean; } -export default function CourseCard({ course, onSelect, onUnlockRequest, subscriptionStatus, isSelected = false }: CourseCardProps) { +export default function CourseCard({ course, onSelect, onUnlockRequest, subscriptionStatus }: CourseCardProps) { const isBlocked = subscriptionStatus === 'OVERDUE' || subscriptionStatus === 'CANCELED'; const isUnlocked = course.isUnlocked !== false; // defaults to true if undefined - const cardClasses = isSelected - ? "absolute top-0 left-0 flex flex-row w-[380px] max-w-[90vw] h-[220px] bg-zinc-900 border-2 border-[#E50914] rounded-xl overflow-hidden cursor-pointer shadow-2xl z-20 transition-all duration-300" - : "absolute top-0 left-0 w-full h-full flex flex-col bg-zinc-950/40 border border-white/10 rounded-xl overflow-hidden cursor-pointer transition-all duration-300 z-10 md:group-hover:z-30 md:group-hover:flex-row md:group-hover:scale-105 md:group-hover:w-[380px] md:group-hover:h-[220px] md:group-hover:shadow-2xl shadow-lg"; - - const imgContainerClasses = isSelected - ? "relative w-[150px] h-full flex-shrink-0 overflow-hidden" - : "relative w-full h-[180px] md:group-hover:w-[150px] md:group-hover:h-full flex-shrink-0 overflow-hidden transition-all duration-300"; - - const infoContainerClasses = isSelected - ? "p-4 flex-grow flex flex-col justify-between min-w-0 h-full bg-zinc-900" - : "p-3 flex-grow flex flex-col justify-between min-w-0 h-full bg-zinc-900/60 md:group-hover:bg-zinc-900 md:group-hover:p-4 transition-all duration-300"; - return (
-
- {/* Thumbnail */} -
- {course.title} + {/* Thumbnail */} +
+ {course.title} - {/* Lock Overlay */} - {!isUnlocked && ( -
-
- -
- - Bloqueado - + {/* Lock Overlay */} + {!isUnlocked && ( +
+
+
- )} - - {/* Play Overlay */} - {isUnlocked && ( -
-
- -
-
- )} - - {/* Progress bar at the bottom of the thumbnail */} - {course.progress !== undefined && course.progress > 0 && ( -
-
-
- )} -
- - {/* Course Info */} -
-
- {/* Category */} - - {course.category} - {!isUnlocked && course.price !== undefined && ( - - R$ {course.price.toFixed(2)} - - )} + + Curso Trancado - {/* Title */} -

- {course.title} -

- {/* Description */} -

- {course.description} -

+ )} - {/* Footer Metrics */} -
-
- - {course.totalLessons || 0} Aulas + {/* Play Overlay */} + {isUnlocked && ( +
+
+
+
+ )} - {course.progress !== undefined && course.progress > 0 ? ( -
- {course.progress === 100 ? ( -
- - Concluído -
- ) : ( - {course.progress}% - )} -
- ) : ( - - {!isUnlocked && } - {!isUnlocked ? 'Bloqueado' : 'Não iniciado'} + {/* Progress bar at the bottom of the thumbnail */} + {course.progress !== undefined && course.progress > 0 && ( +
+
+
+ )} +
+ + {/* Course Info */} +
+
+ {/* Category */} + + {course.category} + {!isUnlocked && course.price !== undefined && ( + + R$ {course.price.toFixed(2)} )} + + {/* Title */} +

+ {course.title} +

+ {/* Description */} +

+ {course.description} +

+
+ + {/* Footer Metrics */} +
+
+ + {course.totalLessons || 0} Aulas
+ + {course.progress !== undefined && course.progress > 0 ? ( +
+ {course.progress === 100 ? ( +
+ + Concluído +
+ ) : ( + {course.progress}% assistido + )} +
+ ) : ( + + {!isUnlocked && } + {!isUnlocked ? 'Bloqueado' : 'Não iniciado'} + + )}
diff --git a/src/components/CourseCarousel.tsx b/src/components/CourseCarousel.tsx index e174eb1..5b791c5 100644 --- a/src/components/CourseCarousel.tsx +++ b/src/components/CourseCarousel.tsx @@ -9,10 +9,9 @@ interface CourseCarouselProps { onSelectCourse: (courseId: string) => void; onUnlockCourse?: (course: any) => void; subscriptionStatus: string; - selectedCourseId?: string | null; } -export default function CourseCarousel({ title, courses, onSelectCourse, onUnlockCourse, subscriptionStatus, selectedCourseId }: CourseCarouselProps) { +export default function CourseCarousel({ title, courses, onSelectCourse, onUnlockCourse, subscriptionStatus }: CourseCarouselProps) { if (courses.length === 0) return null; return ( @@ -36,7 +35,6 @@ export default function CourseCarousel({ title, courses, onSelectCourse, onUnloc onSelect={onSelectCourse} onUnlockRequest={onUnlockCourse} subscriptionStatus={subscriptionStatus} - isSelected={selectedCourseId === course.id} /> ))}