From bb151893b423a1e14ecbdbad8a7e464580f508c3 Mon Sep 17 00:00:00 2001 From: icrcode Date: Tue, 23 Jun 2026 18:16:53 -0300 Subject: [PATCH 1/5] feat: enhance Apresentacao page with new sections and improved navigation; update Nginx config for additional domain support --- .env.production.example | 6 +- backend/src/aplicativo.ts | 14 +- docker-compose.prod.yml | 1 + frontend/src/components/Layout.tsx | 32 +- frontend/src/pages/Apresentacao.tsx | 532 ++++++++++++++++++++++++---- infra/nginx/default.conf.template | 4 +- 6 files changed, 489 insertions(+), 100 deletions(-) diff --git a/.env.production.example b/.env.production.example index 165d640..3ac1567 100644 --- a/.env.production.example +++ b/.env.production.example @@ -7,6 +7,10 @@ # Docker Hub (usado pelo docker-compose.prod.yml) DOCKERHUB_USERNAME=seu-usuario-dockerhub +# ── Domínios ────────────────────────────────────────────── +DOMAIN=valida-ai.online +DOMAIN_ALT=valida-ai.site + # ── Banco de Dados ──────────────────────────────────────── DB_USER=valida_user DB_PASSWORD=TROQUE_POR_SENHA_FORTE @@ -30,7 +34,7 @@ JWT_EXPIRES_IN=7d # ── CORS (URL pública do frontend) ──────────────────────── # Exemplo: http://ec2-XX-XX-XX-XX.compute-1.amazonaws.com # Ou seu domínio: https://app.validaai.com.br -CORS_ORIGIN=http://SEU_IP_OU_DOMINIO +CORS_ORIGIN=https://SEU_DOMINIO,https://SEU_DOMINIO_ALT # ── URL do frontend (usada no QR Code dos certificados) ── FRONTEND_URL=http://valida-ai.online diff --git a/backend/src/aplicativo.ts b/backend/src/aplicativo.ts index 4758af4..d6427aa 100644 --- a/backend/src/aplicativo.ts +++ b/backend/src/aplicativo.ts @@ -17,6 +17,13 @@ import { registrarHandlers } from './eventos/registrar'; const aplicativo: Express = express(); +// Rotas internas (antes do CORS — acesso apenas via rede interna) +aplicativo.use('/', rotasVerificacao); +aplicativo.get('/metrics', async (_req, res) => { + res.set('Content-Type', registro.contentType); + res.end(await registro.metrics()); +}); + // Middlewares de segurança aplicativo.use(helmet()); @@ -53,14 +60,7 @@ aplicativo.use((req, _res, next) => { next(); }); -// Métricas para o Prometheus -aplicativo.get('/metrics', async (_req, res) => { - res.set('Content-Type', registro.contentType); - res.end(await registro.metrics()); -}); - // Rotas -aplicativo.use('/', rotasVerificacao); aplicativo.use('/api/publica', rotasPublica); aplicativo.use('/api/auth', rotasAuth); aplicativo.use('/api/usuarios', rotasUsuarios); diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 154bc38..097c7db 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -8,6 +8,7 @@ services: - "443:443" environment: DOMAIN: ${DOMAIN} + DOMAIN_ALT: ${DOMAIN_ALT} volumes: - ./infra/nginx/default.conf.template:/etc/nginx/templates/default.conf.template:ro - ./certbot/conf:/etc/letsencrypt:ro diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 86233b0..b274171 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -7,7 +7,7 @@ import { PERFIL_LABEL, PERFIL_COR, iniciais } from '../utils/perfil'; import { SunIcon, MoonIcon } from './icons'; const linkCls = ({ isActive }: { isActive: boolean }) => - `px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${ + `px-4 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 ${ isActive ? 'bg-[#618C7C]/15 text-[#7AAA9A] border border-[#618C7C]/25' : 'text-white/55 border border-transparent hover:text-white hover:bg-white/5' @@ -75,11 +75,11 @@ export function Layout() {
{/* Header */}
-
+
{/* Logo */} - -
- + +
+
@@ -90,25 +90,25 @@ export function Layout() { {/* Ações do header */} -
+
{/* Botão tema */}
{usuario?.nome ? iniciais(usuario.nome) : '?'} @@ -125,7 +125,7 @@ export function Layout() { @@ -133,7 +133,7 @@ export function Layout() { + + {/* Desktop nav */} +
+ {NAV_ITEMS.map(({ id, label }) => ( + + ))}
- - Entrar - -
+
+ + Entrar + + + Criar conta + + + {/* Mobile hamburger */} + +
+
+ + {/* Mobile menu */} + {menuOpen && ( +
+
+ {NAV_ITEMS.map(({ id, label }) => ( + + ))} + + Criar conta + +
+
+ )} + - {/* Hero */} -
+ {/* ── Início / Hero ── */} +
+
- Captação e validação de documentos universitários + Plataforma de validação acadêmica -

- Centralize e valide os certificados acadêmicos da sua instituição +

+ Centralize e valide os{' '} + + certificados acadêmicos + {' '} + da sua instituição

-

- O ValidaAI conecta estudantes e coordenadores em um único lugar: estudantes enviam - certificados de cursos, eventos e atividades complementares, e coordenadores - validam cada documento com segurança e rastreabilidade. +

+ O ValidaAI conecta estudantes e coordenadores em um único lugar: envie certificados + de cursos, eventos e atividades complementares, e tenha cada documento + validado com segurança e rastreabilidade.

-
+
- Criar conta de estudante + Começar agora + + + Já tenho uma conta
- {/* Recursos */} -
- {recursos.map((recurso) => ( + {/* Stats */} +
+ {[ + { value: '100%', label: 'Digital' }, + { value: 'QR Code', label: 'Verificável' }, + { value: '24/7', label: 'Disponível' }, + ].map((stat) => ( +
+
{stat.value}
+
{stat.label}
+
+ ))} +
+
+ + {/* Scroll indicator */} + +
+ + {/* ── Funcionalidades ── */} +
+
+ + +
+ {funcionalidades.map((item, i) => ( +
+
+ + {item.icone} + +
+

{item.titulo}

+

{item.descricao}

+
+ ))} +
+
+
+ + {/* ── Benefícios ── */} +
+
+ + +
+ {beneficios.map((grupo, i) => (
-
- - {recurso.icone} +
+
+ + {grupo.icone} + +
+

{grupo.titulo}

+
+
    + {grupo.items.map((item) => ( +
  • + + + + {item} +
  • + ))} +
+
+ ))} +
+
+
+ + {/* ── Segurança ── */} +
+
+ + +
+ {segurancaItems.map((item, i) => ( +
+
+ + {item.icone}
-

{recurso.titulo}

-

{recurso.descricao}

+

{item.titulo}

+

{item.descricao}

))}
-
+ + + + {/* ── Contato ── */} +
+
+ + +
+
+ + + +
+ +

+ O ValidaAI é um projeto acadêmico desenvolvido como + Trabalho de Conclusão de Curso (TCC). Para dúvidas, sugestões ou interesse em + colaborar, entre em contato pelo GitHub: +

+ + + + + + github.com/icrcode + +
+
+
+ + {/* ── Footer ── */} + ); } diff --git a/infra/nginx/default.conf.template b/infra/nginx/default.conf.template index 43bbda5..8e568c8 100644 --- a/infra/nginx/default.conf.template +++ b/infra/nginx/default.conf.template @@ -1,7 +1,7 @@ # ── HTTP: challenge do Certbot + redirect para HTTPS ────────────── server { listen 80; - server_name ${DOMAIN} www.${DOMAIN}; + server_name ${DOMAIN} www.${DOMAIN} ${DOMAIN_ALT} www.${DOMAIN_ALT}; location /.well-known/acme-challenge/ { root /var/www/certbot; @@ -15,7 +15,7 @@ server { # ── HTTPS ────────────────────────────────────────────────────────── server { listen 443 ssl; - server_name ${DOMAIN} www.${DOMAIN}; + server_name ${DOMAIN} www.${DOMAIN} ${DOMAIN_ALT} www.${DOMAIN_ALT}; ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem; From c1a474ba06c636d4072173e776cfa0c95f96dbee Mon Sep 17 00:00:00 2001 From: icrcode Date: Tue, 23 Jun 2026 18:19:39 -0300 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20refatorar=20links=20de=20navega?= =?UTF-8?q?=C3=A7=C3=A3o=20para=20uso=20de=20array=20de=20objetos=20e=20si?= =?UTF-8?q?mplificar=20l=C3=B3gica=20de=20exibi=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Layout.tsx | 68 +++++++++++------------------- 1 file changed, 25 insertions(+), 43 deletions(-) diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index b274171..e036d64 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -20,56 +20,38 @@ export function Layout() { const [menuAberto, setMenuAberto] = useState(false); - const eEstudante = usuario?.perfil === 'estudante'; - const eCoordenadorPuro = usuario?.perfil === 'coordenador'; - const eCoordenador = eCoordenadorPuro || usuario?.perfil === 'admin'; + const perfil = usuario?.perfil; function handleLogout() { logout(); navigate('/login'); } - const navLinks = (mobile = false) => ( - <> - setMenuAberto(false) : undefined}> - Início - - setMenuAberto(false) : undefined}> - Documentos - - {eEstudante && ( - setMenuAberto(false) : undefined}> - Certificados - - )} - {eCoordenador && ( - setMenuAberto(false) : undefined}> - Fila de Análise - - )} - {eCoordenadorPuro && ( - setMenuAberto(false) : undefined}> - Alunos - - )} - setMenuAberto(false) : undefined}> - Perfil + const allLinks: { to: string; label: string; perfis?: string[] }[] = [ + { to: '/dashboard', label: 'Início' }, + { to: '/documentos', label: 'Documentos' }, + { to: '/certificados', label: 'Certificados', perfis: ['estudante'] }, + { to: '/documentos?status=pendente', label: 'Fila de Análise', perfis: ['coordenador', 'admin'] }, + { to: '/alunos', label: 'Alunos', perfis: ['coordenador'] }, + { to: '/perfil', label: 'Perfil' }, + { to: '/usuarios', label: 'Usuários', perfis: ['admin'] }, + { to: '/instituicoes', label: 'Instituições', perfis: ['admin'] }, + { to: '/cursos', label: 'Cursos', perfis: ['admin'] }, + ]; + + const visibleLinks = allLinks.filter((l) => !l.perfis || (perfil && l.perfis.includes(perfil))); + + const navLinks = (mobile = false) => + visibleLinks.map((l) => ( + setMenuAberto(false) : undefined} + > + {l.label} - {usuario?.perfil === 'admin' && ( - <> - setMenuAberto(false) : undefined}> - Usuários - - setMenuAberto(false) : undefined}> - Instituições - - setMenuAberto(false) : undefined}> - Cursos - - - )} - - ); + )); return (
From 25da77cfabd2adb243b42740f526c991bc1245ff Mon Sep 17 00:00:00 2001 From: icrcode Date: Tue, 23 Jun 2026 19:05:05 -0300 Subject: [PATCH 3/5] Refactor code structure for improved readability and maintainability --- frontend/src/index.css | 9 + frontend/src/pages/Apresentacao.tsx | 397 ++++++++++++++-------------- 2 files changed, 210 insertions(+), 196 deletions(-) diff --git a/frontend/src/index.css b/frontend/src/index.css index beac79d..958a4b5 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -75,6 +75,15 @@ body { .delay-225 { animation-delay: 225ms; } .delay-300 { animation-delay: 300ms; } +/* Landing page floating elements */ +@keyframes landingFloat { + 0%, 100% { transform: translateY(0) rotate(0deg); } + 50% { transform: translateY(-20px) rotate(3deg); } +} +.landing-float-slow { animation: landingFloat 8s ease-in-out infinite; } +.landing-float-med { animation: landingFloat 6s ease-in-out infinite 1s; } +.landing-float-fast { animation: landingFloat 4s ease-in-out infinite 0.5s; } + /* ============================================================ MODO CLARO — sobrescreve utilitários Tailwind via seletor de maior especificidade (fora de @layer, sempre vence) diff --git a/frontend/src/pages/Apresentacao.tsx b/frontend/src/pages/Apresentacao.tsx index ade8267..224819c 100644 --- a/frontend/src/pages/Apresentacao.tsx +++ b/frontend/src/pages/Apresentacao.tsx @@ -10,48 +10,43 @@ const NAV_ITEMS = [ { id: 'contato', label: 'Contato' }, ]; +const etapas = [ + { numero: '01', titulo: 'Cadastre-se', descricao: 'Crie sua conta como estudante em poucos segundos.' }, + { numero: '02', titulo: 'Envie', descricao: 'Anexe seus certificados e documentos acadêmicos.' }, + { numero: '03', titulo: 'Validação', descricao: 'O coordenador analisa e valida cada documento.' }, + { numero: '04', titulo: 'Compartilhe', descricao: 'Receba um link único com QR Code verificável.' }, +]; + const funcionalidades = [ { titulo: 'Envio de Certificados', - descricao: 'Anexe certificados de cursos, eventos, estágios e atividades complementares em poucos cliques, direto pelo computador ou celular.', - icone: ( - - ), + descricao: 'Anexe certificados de cursos, eventos, estágios e atividades complementares em poucos cliques.', + icone: 'M9 12h6m-6 4h4m-7 5h10a2 2 0 002-2V7.414a1 1 0 00-.293-.707l-4.414-4.414A1 1 0 0012.586 2H6a2 2 0 00-2 2v14a2 2 0 002 2z', }, { titulo: 'Validação por Coordenadores', - descricao: 'Coordenadores de cada curso analisam, aprovam ou solicitam ajustes nos documentos enviados pelos estudantes.', - icone: ( - - ), + descricao: 'Coordenadores analisam, aprovam ou solicitam ajustes nos documentos enviados.', + icone: 'M9 12.75l1.5 1.5 3-3.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z', }, { titulo: 'Verificação Pública', - descricao: 'Cada certificado validado recebe um link único com QR Code, que pode ser compartilhado para comprovar sua autenticidade.', - icone: ( - - ), + descricao: 'Cada certificado validado recebe um link único com QR Code para comprovar autenticidade.', + icone: 'M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244', }, { titulo: 'Multi-Instituição', - descricao: 'Suporte a múltiplas instituições de ensino, com cursos e usuários organizados de forma independente e segura.', - icone: ( - - ), + descricao: 'Suporte a múltiplas instituições com cursos e usuários organizados de forma independente.', + icone: 'M3.75 21h16.5M4.5 3h15M5.25 3v18m13.5-18v18M9 6.75h1.5m-1.5 3h1.5m-1.5 3h1.5m3-6H15m-1.5 3H15m-1.5 3H15', }, { titulo: 'Dashboard Inteligente', - descricao: 'Painel com visão geral de documentos pendentes, aprovados e rejeitados, com filtros e estatísticas em tempo real.', - icone: ( - - ), + descricao: 'Painel com visão geral de documentos, filtros e estatísticas em tempo real.', + icone: 'M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z', }, { titulo: 'Geração de Certificados', - descricao: 'Gere certificados de atividades complementares com QR Code de verificação automática e assinatura do coordenador.', - icone: ( - - ), + descricao: 'Gere certificados com QR Code de verificação automática e assinatura do coordenador.', + icone: 'M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z', }, ]; @@ -64,9 +59,8 @@ const beneficios = [ 'Compartilhe certificados validados com link verificável', 'Histórico completo de todas as atividades complementares', ], - icone: ( - - ), + icone: 'M4.26 10.147a60.438 60.438 0 00-.491 6.347A48.62 48.62 0 0112 20.904a48.62 48.62 0 018.232-4.41 60.46 60.46 0 00-.491-6.347m-15.482 0a50.636 50.636 0 00-2.658-.813A59.906 59.906 0 0112 3.493a59.903 59.903 0 0110.399 5.84c-.896.248-1.783.52-2.658.814m-15.482 0A50.717 50.717 0 0112 13.489a50.702 50.702 0 017.74-3.342M6.75 15a.75.75 0 100-1.5.75.75 0 000 1.5zm0 0v-3.675A55.378 55.378 0 0112 8.443m-7.007 11.55A5.981 5.981 0 006.75 15.75v-1.5', + cor: '#618C7C', }, { titulo: 'Para Coordenadores', @@ -76,9 +70,8 @@ const beneficios = [ 'Visão completa dos alunos e seus certificados', 'Assinatura digital automática nos certificados gerados', ], - icone: ( - - ), + icone: 'M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z', + cor: '#5B8DB8', }, { titulo: 'Para Instituições', @@ -88,57 +81,63 @@ const beneficios = [ 'Rastreabilidade completa de todos os documentos', 'Relatórios e métricas de atividades complementares', ], - icone: ( - - ), + icone: 'M12 21v-8.25M15.75 21v-8.25M8.25 21v-8.25M3 9l9-6 9 6m-1.5 12V10.332A48.36 48.36 0 0012 9.75c-2.551 0-5.056.2-7.5.582V21M3 21h18M12 6.75h.008v.008H12V6.75z', + cor: '#8B6BAE', }, ]; const segurancaItems = [ - { - titulo: 'Criptografia TLS 1.2/1.3', - descricao: 'Todas as comunicações são protegidas com criptografia de ponta a ponta usando os protocolos mais recentes.', - icone: ( - - ), - }, - { - titulo: 'Autenticação Segura', - descricao: 'Login com JWT tokens, senhas criptografadas com bcrypt e suporte a autenticação via Microsoft OAuth.', - icone: ( - - ), - }, - { - titulo: 'Controle de Acesso por Perfil', - descricao: 'Três níveis de acesso (estudante, coordenador, admin) com permissões granulares para cada funcionalidade.', - icone: ( - - ), - }, - { - titulo: 'QR Code Verificável', - descricao: 'Cada certificado possui um QR Code único que permite verificação instantânea da autenticidade por qualquer pessoa.', - icone: ( - - ), - }, - { - titulo: 'HSTS & Headers de Segurança', - descricao: 'Proteção contra ataques com Strict-Transport-Security, Helmet.js e headers de segurança configurados no servidor.', - icone: ( - - ), - }, - { - titulo: 'Armazenamento Seguro', - descricao: 'Documentos armazenados com segurança na AWS S3 com acesso controlado e URLs assinadas temporárias.', - icone: ( - - ), - }, + { titulo: 'Criptografia TLS 1.2/1.3', descricao: 'Comunicações protegidas com criptografia de ponta a ponta.', icone: 'M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z' }, + { titulo: 'Autenticação Segura', descricao: 'JWT tokens, bcrypt e suporte a Microsoft OAuth.', icone: 'M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z' }, + { titulo: 'Controle de Acesso', descricao: 'Três perfis com permissões granulares.', icone: 'M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z' }, + { titulo: 'QR Code Verificável', descricao: 'Verificação instantânea da autenticidade por qualquer pessoa.', icone: 'M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z' }, + { titulo: 'Headers de Segurança', descricao: 'HSTS, Helmet.js e proteção contra ataques comuns.', icone: 'M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z' }, + { titulo: 'Armazenamento S3', descricao: 'Documentos na AWS S3 com URLs assinadas temporárias.', icone: 'M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125' }, ]; +function GridPattern({ className = '' }: { className?: string }) { + return ( + + + + + + + + + ); +} + +function FloatingShapes() { + return ( +
+
+
+
+
+
+ {/* Corner accents */} + + + + + + + + + +
+ ); +} + +function Icon({ d, className = 'h-5 w-5' }: { d: string; className?: string }) { + return ( + + + + ); +} + function useActiveSection() { const [active, setActive] = useState('inicio'); @@ -170,11 +169,22 @@ function useActiveSection() { function SectionHeading({ badge, title, subtitle }: { badge: string; title: string; subtitle: string }) { return (
- + + {badge} -

{title}

-

{subtitle}

+

{title}

+

{subtitle}

+
+ ); +} + +function Divider() { + return ( +
+
+
+
); } @@ -183,12 +193,6 @@ export function Apresentacao() { const { isAuthenticated } = useAuth(); const activeSection = useActiveSection(); const [menuOpen, setMenuOpen] = useState(false); - const [scrolled, setScrolled] = useState(false); - useEffect(() => { - const onScroll = () => setScrolled(window.scrollY > 20); - window.addEventListener('scroll', onScroll, { passive: true }); - return () => window.removeEventListener('scroll', onScroll); - }, []); if (isAuthenticated) return ; @@ -199,7 +203,7 @@ export function Apresentacao() { return (
- {/* ── Navbar fixa ── */} + {/* ── Navbar ── */} - {/* ── Início / Hero ── */} -
-
+ {/* ── Hero ── */} +
+ + + +
- + {/* Badge animado */} + + + + + Plataforma de validação acadêmica -

+

Centralize e valide os{' '} - + certificados acadêmicos + + + {' '} da sua instituição

-

+

O ValidaAI conecta estudantes e coordenadores em um único lugar: envie certificados de cursos, eventos e atividades complementares, e tenha cada documento validado com segurança e rastreabilidade. @@ -313,68 +302,92 @@ export function Apresentacao() {

Começar agora - + - + Já tenho uma conta
- {/* Stats */} -
+ {/* Stats com ícones */} +
{[ - { value: '100%', label: 'Digital' }, - { value: 'QR Code', label: 'Verificável' }, - { value: '24/7', label: 'Disponível' }, + { value: '100%', label: 'Digital', icon: 'M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25' }, + { value: 'QR Code', label: 'Verificável', icon: 'M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z' }, + { value: '24/7', label: 'Disponível', icon: 'M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z' }, ].map((stat) => ( -
-
{stat.value}
-
{stat.label}
+
+ +
{stat.value}
+
{stat.label}
))}
{/* Scroll indicator */} -
+ + + {/* ── Como Funciona ── */} +
+
+ + +
+ {/* Linha conectora vertical (mobile) / horizontal (desktop) */} +
+ +
+ {etapas.map((etapa, i) => ( +
+
+ {etapa.numero} +
+
+

{etapa.titulo}

+

{etapa.descricao}

+
+
+ ))} +
+
+
+
+ + + {/* ── Funcionalidades ── */} -
-
- +
+ +
+ -
+
{funcionalidades.map((item, i) => (
-
- - {item.icone} - + {/* Número decorativo */} + + {String(i + 1).padStart(2, '0')} + +
+

{item.titulo}

{item.descricao}

@@ -384,34 +397,33 @@ export function Apresentacao() {
+ + {/* ── Benefícios ── */} -
+
- + -
+
{beneficios.map((grupo, i) => (
+ {/* Top accent bar */} +
+
-
- - {grupo.icone} - +
+

{grupo.titulo}

    {grupo.items.map((item) => (
  • - + {item} @@ -424,26 +436,27 @@ export function Apresentacao() {
+ + {/* ── Segurança ── */} -
-
- +
+ {/* Shield background decorativo */} + + + -
+
+ + +
{segurancaItems.map((item, i) => (
-
- - {item.icone} - +
+

{item.titulo}

{item.descricao}

@@ -453,14 +466,12 @@ export function Apresentacao() {
+ + {/* ── Contato ── */} -
+
- +
@@ -506,19 +517,13 @@ export function Apresentacao() {
{NAV_ITEMS.map(({ id, label }) => ( - ))}
-

- © {new Date().getFullYear()} ValidaAI -

+

© {new Date().getFullYear()} ValidaAI

From d140365bacd6b9cc48c99d0acd95d073bf38bbbc Mon Sep 17 00:00:00 2001 From: icrcode Date: Tue, 23 Jun 2026 19:31:14 -0300 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20adicionar=20verifica=C3=A7=C3=A3o?= =?UTF-8?q?=20de=20suporte=20ao=20IntersectionObserver=20e=20atualizar=20t?= =?UTF-8?q?exto=20do=20link=20na=20tela=20de=20apresenta=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/Apresentacao.tsx | 2 ++ frontend/src/test/App.test.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/Apresentacao.tsx b/frontend/src/pages/Apresentacao.tsx index 224819c..7b559f5 100644 --- a/frontend/src/pages/Apresentacao.tsx +++ b/frontend/src/pages/Apresentacao.tsx @@ -142,6 +142,8 @@ function useActiveSection() { const [active, setActive] = useState('inicio'); useEffect(() => { + if (typeof IntersectionObserver === 'undefined') return; + const observer = new IntersectionObserver( (entries) => { const visible = entries.filter((e) => e.isIntersecting); diff --git a/frontend/src/test/App.test.tsx b/frontend/src/test/App.test.tsx index 85d8e17..fb1195d 100644 --- a/frontend/src/test/App.test.tsx +++ b/frontend/src/test/App.test.tsx @@ -40,7 +40,7 @@ describe('App', () => { it('exibe convite para criar conta na tela de apresentação quando não autenticado', () => { render(); - const link = screen.queryByRole('link', { name: /criar conta de estudante/i }); + const link = screen.queryByRole('link', { name: /começar agora/i }); expect(link).toBeInTheDocument(); }); }); From 7e624972e6fa7a6c132301e3985ebea4520d07d8 Mon Sep 17 00:00:00 2001 From: icrcode Date: Tue, 23 Jun 2026 19:40:09 -0300 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20adicionar=20manipulador=20de=20erro?= =?UTF-8?q?s=20com=20suporte=20a=20next=20function=20e=20criar=20testes=20?= =?UTF-8?q?para=20a=20p=C3=A1gina=20Apresentacao?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/aplicativo.ts | 2 +- frontend/src/test/pages/Apresentacao.test.tsx | 224 ++++++++++++++++++ 2 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 frontend/src/test/pages/Apresentacao.test.tsx diff --git a/backend/src/aplicativo.ts b/backend/src/aplicativo.ts index d6427aa..83def3a 100644 --- a/backend/src/aplicativo.ts +++ b/backend/src/aplicativo.ts @@ -79,7 +79,7 @@ aplicativo.use((_req, res) => { }); // Manipulador de erros -aplicativo.use((err: unknown, _req: express.Request, res: express.Response) => { +aplicativo.use((err: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => { const mensagem = err instanceof Error ? err.message : 'Erro Interno do Servidor'; const status = (err as { status?: number }).status || 500; diff --git a/frontend/src/test/pages/Apresentacao.test.tsx b/frontend/src/test/pages/Apresentacao.test.tsx new file mode 100644 index 0000000..be7c6ce --- /dev/null +++ b/frontend/src/test/pages/Apresentacao.test.tsx @@ -0,0 +1,224 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { screen, fireEvent } from '@testing-library/react'; +import { renderWithProviders } from '../helpers/renderWithProviders'; +import { Apresentacao } from '../../pages/Apresentacao'; + +vi.mock('../../services/api', () => ({ + default: { + get: vi.fn().mockResolvedValue({ data: [] }), + post: vi.fn().mockResolvedValue({ data: {} }), + interceptors: { + request: { use: vi.fn() }, + response: { use: vi.fn() }, + }, + }, +})); + +beforeEach(() => { + localStorage.clear(); +}); + +function renderPage() { + return renderWithProviders(); +} + +describe('Apresentacao', () => { + it('renderiza sem erros', () => { + const { container } = renderPage(); + expect(container).toBeTruthy(); + }); + + it('exibe o logo ValidaAI', () => { + renderPage(); + const logos = screen.getAllByText(/ValidaAI/i); + expect(logos.length).toBeGreaterThan(0); + }); + + it('exibe o título principal do hero', () => { + renderPage(); + expect(screen.getByText(/centralize e valide os/i)).toBeInTheDocument(); + expect(screen.getByText(/certificados acadêmicos/i)).toBeInTheDocument(); + }); + + it('exibe o badge de plataforma', () => { + renderPage(); + expect(screen.getByText(/plataforma de validação acadêmica/i)).toBeInTheDocument(); + }); + + it('exibe o CTA "Começar agora"', () => { + renderPage(); + const links = screen.getAllByRole('link', { name: /começar agora/i }); + expect(links.length).toBeGreaterThan(0); + }); + + it('exibe o link "Já tenho uma conta"', () => { + renderPage(); + const links = screen.getAllByRole('link', { name: /já tenho uma conta/i }); + expect(links.length).toBeGreaterThan(0); + }); + + it('exibe os stats do hero', () => { + renderPage(); + expect(screen.getByText('100%')).toBeInTheDocument(); + expect(screen.getByText('QR Code')).toBeInTheDocument(); + expect(screen.getByText('24/7')).toBeInTheDocument(); + }); + + it('exibe os labels dos stats', () => { + renderPage(); + expect(screen.getByText('Digital')).toBeInTheDocument(); + expect(screen.getByText('Verificável')).toBeInTheDocument(); + expect(screen.getByText('Disponível')).toBeInTheDocument(); + }); + + // Navbar + it('exibe os itens de navegação no desktop', () => { + renderPage(); + expect(screen.getAllByText('Início').length).toBeGreaterThan(0); + expect(screen.getAllByText('Funcionalidades').length).toBeGreaterThan(0); + expect(screen.getAllByText('Benefícios').length).toBeGreaterThan(0); + expect(screen.getAllByText('Segurança').length).toBeGreaterThan(0); + expect(screen.getAllByText('Contato').length).toBeGreaterThan(0); + }); + + it('exibe os botões Entrar e Criar conta na navbar', () => { + renderPage(); + expect(screen.getAllByText('Entrar')[0]).toBeInTheDocument(); + expect(screen.getAllByText('Criar conta')[0]).toBeInTheDocument(); + }); + + it('abre o menu mobile ao clicar no hamburger', () => { + renderPage(); + const hamburgers = screen.getAllByRole('button'); + const hamburger = hamburgers.find((b) => b.querySelector('path[d*="3.75 6.75"]')); + expect(hamburger).toBeTruthy(); + fireEvent.click(hamburger!); + // Após abrir, o botão muda para o X (close icon) + const closeBtn = screen.getAllByRole('button').find((b) => b.querySelector('path[d*="M6 18"]')); + expect(closeBtn).toBeTruthy(); + }); + + // Seção "Como funciona" + it('exibe as 4 etapas do fluxo', () => { + renderPage(); + expect(screen.getByText('Cadastre-se')).toBeInTheDocument(); + expect(screen.getByText('Envie')).toBeInTheDocument(); + expect(screen.getByText('Validação')).toBeInTheDocument(); + expect(screen.getByText('Compartilhe')).toBeInTheDocument(); + }); + + it('exibe os números das etapas', () => { + renderPage(); + expect(screen.getAllByText('01').length).toBeGreaterThan(0); + expect(screen.getAllByText('02').length).toBeGreaterThan(0); + expect(screen.getAllByText('03').length).toBeGreaterThan(0); + expect(screen.getAllByText('04').length).toBeGreaterThan(0); + }); + + // Seção Funcionalidades + it('exibe os cards de funcionalidades', () => { + renderPage(); + expect(screen.getByText('Envio de Certificados')).toBeInTheDocument(); + expect(screen.getByText('Validação por Coordenadores')).toBeInTheDocument(); + expect(screen.getByText('Verificação Pública')).toBeInTheDocument(); + expect(screen.getByText('Multi-Instituição')).toBeInTheDocument(); + expect(screen.getByText('Dashboard Inteligente')).toBeInTheDocument(); + expect(screen.getByText('Geração de Certificados')).toBeInTheDocument(); + }); + + // Seção Benefícios + it('exibe os três perfis de benefícios', () => { + renderPage(); + expect(screen.getByText('Para Estudantes')).toBeInTheDocument(); + expect(screen.getByText('Para Coordenadores')).toBeInTheDocument(); + expect(screen.getByText('Para Instituições')).toBeInTheDocument(); + }); + + it('exibe os itens de benefícios dos estudantes', () => { + renderPage(); + expect(screen.getByText(/envie certificados de qualquer lugar/i)).toBeInTheDocument(); + expect(screen.getByText(/acompanhe o status/i)).toBeInTheDocument(); + }); + + // Seção Segurança + it('exibe os cards de segurança', () => { + renderPage(); + expect(screen.getByText('Criptografia TLS 1.2/1.3')).toBeInTheDocument(); + expect(screen.getByText('Autenticação Segura')).toBeInTheDocument(); + expect(screen.getByText('Controle de Acesso')).toBeInTheDocument(); + expect(screen.getByText('QR Code Verificável')).toBeInTheDocument(); + expect(screen.getByText('Headers de Segurança')).toBeInTheDocument(); + expect(screen.getByText('Armazenamento S3')).toBeInTheDocument(); + }); + + // Seção Contato + it('exibe a seção de contato com menção ao TCC', () => { + renderPage(); + const tccTexts = screen.getAllByText(/trabalho de conclusão de curso/i); + expect(tccTexts.length).toBeGreaterThan(0); + }); + + it('exibe o link para o GitHub', () => { + renderPage(); + const ghLink = screen.getByRole('link', { name: /github\.com\/icrcode/i }); + expect(ghLink).toBeInTheDocument(); + expect(ghLink).toHaveAttribute('href', 'https://github.com/icrcode'); + expect(ghLink).toHaveAttribute('target', '_blank'); + }); + + // Footer + it('exibe o footer com copyright', () => { + renderPage(); + const year = new Date().getFullYear().toString(); + expect(screen.getByText(new RegExp(year))).toBeInTheDocument(); + }); + + it('exibe links de navegação no footer', () => { + renderPage(); + // Footer duplica os nav items + const inicioButtons = screen.getAllByText('Início'); + expect(inicioButtons.length).toBeGreaterThanOrEqual(2); + }); + + // Seções heading + it('exibe os headings das seções', () => { + renderPage(); + expect(screen.getByText('Tudo que você precisa em um só lugar')).toBeInTheDocument(); + expect(screen.getByText('Vantagens para cada perfil')).toBeInTheDocument(); + expect(screen.getByText('Seus dados estão protegidos')).toBeInTheDocument(); + expect(screen.getAllByText('Entre em contato').length).toBeGreaterThan(0); + }); + + // Redirect + it('redireciona para /dashboard quando autenticado', () => { + renderWithProviders(, { + token: 'tok-test', + usuario: { + id: 'u-1', nome: 'Teste', email: 'teste@test.com', perfil: 'estudante', + matricula: '2021001', cpf: null, endereco: null, curso_id: 'c-1', + instituicao_id: 'i-1', instituicao_nome: 'Universidade Teste', ativo: true, + }, + }); + expect(screen.queryByText(/plataforma de validação acadêmica/i)).not.toBeInTheDocument(); + }); + + // Elementos gráficos + it('renderiza o grid pattern SVG', () => { + const { container } = renderPage(); + const patterns = container.querySelectorAll('pattern#grid'); + expect(patterns.length).toBeGreaterThan(0); + }); + + it('renderiza as formas flutuantes', () => { + const { container } = renderPage(); + const floats = container.querySelectorAll('.landing-float-slow, .landing-float-med, .landing-float-fast'); + expect(floats.length).toBeGreaterThan(0); + }); + + it('renderiza os dividers entre seções', () => { + const { container } = renderPage(); + // Dividers have the gradient line + dot pattern + const dots = container.querySelectorAll('.rounded-full.bg-\\[\\#618C7C\\]\\/25'); + expect(dots.length).toBeGreaterThan(0); + }); +});