Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions FRONTEND_INVENTORY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# PROJECT INVENTORY — AprovIA

> Local file-structure review on 2026-09-29; this review does not verify remote services.
> Local file-structure review on 2026-09-30; this review does not verify remote services.
> This document is repo-first: if something is not present in the tree or current import graph, it is intentionally not claimed here.
> Despite the historical file name, this inventory covers both frontend and active backend code in the Next.js app.

Expand Down Expand Up @@ -35,14 +35,14 @@ The repository contains no active Supabase Edge Functions. Runtime APIs are Next
| Route | Files | Purpose |
| --- | --- | --- |
| `/` | `app/page.tsx`, `app/_components/home/*.tsx` | Landing page composed from four Server Components |
| `/redacao` | `app/redacao/page.tsx`, `app/redacao/RedacaoPageClient.tsx`, `app/redacao/useEssayWorkflow.ts`, `app/redacao/PhotoUpload.tsx`, `app/redacao/prepareOcrImage.ts` | Per-user local drafts, confirmed OCR replacement, bounded input validation and idempotent correction UI |
| `/redacao` | `app/redacao/page.tsx`, `app/redacao/RedacaoPageClient.tsx`, `app/redacao/useEssayWorkflow.ts`, `app/redacao/PhotoUpload.tsx`, `app/redacao/prepareOcrImage.ts` | Continuous theme/editor/correction flow; timestamped per-origin/user local drafts, confirmed OCR replacement, single accessible word counter and guarded correction UI |
| `/questoes` | `app/questoes/page.tsx`, `app/questoes/QuestoesPageClient.tsx` | Same-tab recovery, native radios, previous/review navigation and frozen manual submission retries |
| `/planos` | `app/planos/page.tsx`, `app/planos/PlanosPageClient.tsx` | Free/Max comparison, subscription status, checkout and portal entry points |
| `/noticias` | `app/noticias/page.tsx`, `app/noticias/NoticiasPageClient.tsx`, `app/noticias/error.tsx` | Public feed with background highlights refresh, recoverable pagination and segment errors |
| `/noticias/[slug]` | `app/noticias/[slug]/page.tsx`, `app/noticias/[slug]/article.module.css` | Sanitized article with local typography and optional related content; missing articles preserve 404 |
| `/noticias/pesquisa` | `app/noticias/pesquisa/page.tsx` | URL-driven archive/AI search (`q`, optional `modo=ia`) with history synchronization |
| `/noticias/admin` | `app/noticias/admin/page.tsx` | News admin panel |
| `/conta` | `app/conta/page.tsx`, `app/conta/ContaPageClient.tsx` | Account dashboard and Max subscription management |
| `/conta` | `app/conta/page.tsx`, `app/conta/ContaPageClient.tsx` | Account dashboard, URL-selected history (`aba=redacoes`) and Max subscription management (`#plano`) |
| `/conta/editar` | `app/conta/editar/page.tsx`, `app/conta/editar/ContaEditarPageClient.tsx` | Profile editing |
| `/resultados/[id]` | `app/resultados/[id]/page.tsx`, `app/resultados/[id]/ResultadosPageClient.tsx` | Essay result view |
| `/doacao` | `app/doacao/page.tsx` | Donation page |
Expand Down Expand Up @@ -83,6 +83,14 @@ The repository contains no active Supabase Edge Functions. Runtime APIs are Next

---

### Study shell and canonical navigation

- Canonical metadata and sitemap use `aproviaedu.vercel.app`. Public GET/HEAD content on the former domain receives 301; auth, study, account, payments, admin and APIs retain their original host.
- `lib/contracts/page-metadata.ts` composes page titles, canonical URLs and private-page robots rules; `lib/contracts/site-routing.ts` defines the public redirect allowlist.
- Account navigation uses an avatar disclosure, existing mobile hamburger and query-selected history. Workspace footers are compact; public news retains the full footer.
- Redação keeps the 5,000-character validation contract, displaying it only when violated. Draft timestamp metadata is optional for backwards compatibility; photos remain in memory.
- Rebrand dismissal tolerates unavailable storage and expires on 2026-10-30; the legacy workspace notice has independent per-session dismissal.

## 3. API Surface

| Route | Methods | Purpose |
Expand Down Expand Up @@ -287,8 +295,8 @@ There are currently no separate `components.css`, `forms.css` or `utilities.css`
| --- | --- | --- |
| `NEXT_PUBLIC_SUPABASE_URL` | app, SSR and server DB access | Required for normal runtime |
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | browser auth and SSR session handling | Required for normal runtime |
| `NEXT_PUBLIC_SITE_URL` | root metadata, redirect safety | Recommended |
| `SITE_URL` | sitemap generation | Build-time |
| `NEXT_PUBLIC_SITE_URL` | trusted request origins; canonical metadata uses site constant | Recommended |
| `SITE_URL` | trusted request origins; sitemap uses explicit canonical configuration | Optional |
| `SUPABASE_SERVICE_ROLE_KEY` | server DB access, admin writes, analytics, imports, maintenance, highlights, public news reads and payment persistence | Required for privileged server flows |
| `GROQ_API_KEY` | essay, themes, quiz generation, AI news summary | Required textual AI key for Free and Max |
| `GROQ_MODEL` | Groq integration | Optional override |
Expand Down Expand Up @@ -345,6 +353,7 @@ Latest system migrations: `20260717180319_reform_essay_quiz_systems.sql` and the
| `tests/systems/student-workflows.test.ts` | Draft isolation/logout, frozen IDs/answers, API errors, input limits, availability and stale requests |
| `tests/systems/news-detail.test.tsx` | Article preservation when related content fails, sanitization, genuine 404 and unavailable service |
| `docs/student-workflows-qa.md` | Workflow verification scenarios, results and limits of controlled browser QA |
| `docs/redacao-qa.md` | Essay presentation, shell, metadata and controlled-browser verification |

---

Expand All @@ -354,7 +363,7 @@ Latest system migrations: `20260717180319_reform_essay_quiz_systems.sql` and the
| --- | --- |
| `next.config.ts` | Next.js config, remote image hosts and security headers |
| `next-sitemap.config.js` | Sitemap and robots generation rules |
| `proxy.ts` | Session-refresh proxy matcher |
| `proxy.ts` | Public legacy-host 301 allowlist and protected session-refresh matcher |
| `eslint.config.mjs` | Flat ESLint config based on Next core-web-vitals |
| `postcss.config.mjs` | PostCSS config |
| `tailwind.config.js` | Tailwind configuration |
Expand Down
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,27 @@ O feedback produzido por IA é uma orientação de estudo. Ele não substitui pr
- Groq atende os fluxos textuais de IA nos planos Free e Max. O SDK não faz retries internos; o orquestrador aplica timeout de 30 segundos e no máximo duas tentativas globais, usando fallback apenas para falhas transitórias ou uma segunda saída estruturada inválida.
- Gemini é usado para OCR pelo SDK oficial `@google/genai`. O servidor tenta, no máximo uma vez por modelo, `gemini-3.5-flash`, `gemini-2.5-flash` e `gemini-3.1-flash-lite`; só avança em falhas transitórias ou leitura evidentemente inválida. As fotos grandes são comprimidas no navegador e permanecem apenas em memória. NewsAPI atende a importação de notícias e Stripe atende assinaturas e doações.
- Limpezas, rate limiting e destaques usam RPCs transacionais acionadas sob demanda pelo próprio app, sem cron externo.
- A interface é exclusivamente dark e usa tokens semânticos em `app/styles/` e o componente `AprovIALogo` para a marca.
- A interface é exclusivamente dark e usa tokens semânticos em `app/styles/` e o componente `AprovIALogo` para a marca. Roxo (`--brand`) identifica a marca e ações primárias; verde (`--ai`) identifica recursos de IA e sucesso.
- Vercel Analytics e Speed Insights só são montados depois do consentimento para métricas opcionais.
- O runtime é inteiramente atendido pelos Route Handlers do Next.js; as Edge Functions remotas legadas foram removidas.

## Recuperação do trabalho e navegação

- Redações salvam texto, tema e `submissionId` no `localStorage`, por usuário. O rascunho é restaurado antes da edição e removido após correção salva, descarte confirmado ou logout explícito. Expiração de sessão e falhas de conexão preservam o rascunho. Fotos do OCR continuam apenas em memória; substituir texto diferente exige confirmação.
- Redações salvam texto, tema, `submissionId` e horário da gravação no `localStorage`, por usuário e origem. Rascunhos anteriores sem horário continuam recuperáveis. A interface só informa salvamento após a gravação; um tema sem redação tem status e descarte próprios. O rascunho é restaurado antes da edição e removido após correção salva, descarte confirmado ou logout explícito. Expiração de sessão e falhas de conexão preservam o rascunho. Fotos do OCR continuam apenas em memória; substituir texto diferente exige confirmação.
- Simulados usam `sessionStorage`, por usuário e por aba, preservando `requestId`, `attemptId`, `expiresAt`, posição e respostas. Retomar não cria outra tentativa automaticamente. O primeiro envio congela as respostas; retries manuais enviam o mesmo snapshot. Respostas 404/410 permitem começar um novo simulado.
- A tela de redação segue Tema → Redação → Correção, sem ocultar etapas no mobile. Um contador mostra 100–500 palavras; o botão explica impedimentos de envio. O limite técnico de caracteres só aparece quando excedido. A foto é transcrita para revisão antes de aplicar o texto; câmera e galeria aceitam JPEG, PNG e WebP, com orientação para converter HEIC.
- “Minhas redações” abre `/conta?aba=redacoes`, sincronizado com voltar/avançar. O menu da conta reúne perfil, histórico, plano e logout; áreas de trabalho têm rodapé compacto.
- A redação aceita tema manual de 5–300 caracteres, 100–500 palavras e até 5.000 caracteres. Texto colado ou extraído acima dos limites permanece no editor para revisão.
- A disponibilidade exibida usa a mesma regra do servidor, das 7h às 23h30 em `America/Sao_Paulo`, e atualiza a cada minuto e ao voltar à aba. O servidor continua autorizando cada operação.
- Notícias sincronizam termo e modo com a URL: `q` pesquisa o acervo e `modo=ia` pede um resumo. Voltar/avançar restaura a pesquisa. Consultas antigas são invalidadas, paginação com erro mantém os artigos e o mesmo offset para retry, e destaques são revalidados em segundo plano pelo GET existente.
- Falhas de armazenamento são informadas na página. O salvamento depende do navegador; logout explícito também invalida gravações e rascunhos antigos de outras abas.

O domínio canônico é `aproviaedu.vercel.app`, registrado como domínio de produção do projeto. `foconoenem.vercel.app` redireciona por 301 somente GET/HEAD de páginas públicas de conteúdo, preservando caminho e query. Login, estudo, conta, resultados, administração, planos, doações e APIs permanecem acessíveis no endereço antigo. Sessões e rascunhos não atravessam origens: recupere e copie o trabalho antes de trocar de endereço. Autenticação e pagamentos mantêm retornos na origem da sessão.

O banner de rebrand respeita o fechamento salvo e expira em 30/10/2026. O aviso de transição nos espaços de trabalho antigos é independente, dispensável por sessão e sem expiração automática.

A verificação das melhorias de Redação está em [docs/redacao-qa.md](docs/redacao-qa.md).

A verificação deste lote, incluindo os limites do QA com respostas controladas, está em [docs/student-workflows-qa.md](docs/student-workflows-qa.md).

## Stack principal
Expand Down Expand Up @@ -79,8 +87,8 @@ Use `.env.example` como referência e nunca versione `.env.local` ou chaves reai
| `NEXT_PUBLIC_SUPABASE_URL` | obrigatória | URL dos clientes Supabase |
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | obrigatória | autenticação e sessão com RLS |
| `SUPABASE_SERVICE_ROLE_KEY` | obrigatória para operações privilegiadas | gravações server-side, administração, manutenção, pagamentos e leituras protegidas |
| `NEXT_PUBLIC_SITE_URL` | recomendada | metadata, redirects e URLs públicas |
| `SITE_URL` | recomendada | geração do sitemap |
| `NEXT_PUBLIC_SITE_URL` | recomendada | URL pública de integrações; canonical definido em `lib/constants/site.ts` |
| `SITE_URL` | opcional | origens confiáveis de requisição; sitemap usa o domínio canônico explícito |
| `GROQ_API_KEY` | obrigatória para a IA textual | redações, temas, questões e notícias nos planos Free e Max |
| `GROQ_MODEL` | opcional | modelo primário da Groq |
| `GROQ_FALLBACK_API_KEY` | opcional | chave do fallback Groq |
Expand Down
10 changes: 5 additions & 5 deletions app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { ReactNode } from 'react';
import type { Metadata } from 'next';
import { createPageMetadata } from '@/lib/contracts/page-metadata';
import Link from 'next/link';
import AprovIALogo from '@/app/components/shared/AprovIALogo';

export const metadata: Metadata = {
title: 'Autenticação | AprovIA',
export const metadata = createPageMetadata({
title: 'Autenticação',
description: 'Acesse sua conta ou crie uma nova para começar a estudar',
robots: 'noindex, nofollow',
};
noIndex: true,
});

/* Brand feature bullets for the left panel */
const FEATURES = [
Expand Down
12 changes: 12 additions & 0 deletions app/auth/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ReactNode } from 'react';
import { createPageMetadata } from '@/lib/contracts/page-metadata';

export const metadata = createPageMetadata({
title: 'Autenticação',
description: 'Acesse sua conta AprovIA.',
noIndex: true,
});

export default function Layout({ children }: { children: ReactNode }) {
return children;
}
39 changes: 30 additions & 9 deletions app/components/layout/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import Link from 'next/link';
import CookiePreferencesButton from '@/app/components/privacy/CookiePreferencesButton';
import AprovIALogo from '@/app/components/shared/AprovIALogo';
import FooterVariant from './FooterVariant';

/* ------------------------------------------------------------------ */
/* Inline SVG Icons (social) */
/* ------------------------------------------------------------------ */
function GitHubIcon({ className }: { className?: string }) {
return (
<svg className={className} width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<svg aria-hidden="true" className={className} width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z" />
</svg>
);
}

function MailIcon({ className }: { className?: string }) {
return (
<svg className={className} width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<svg aria-hidden="true" className={className} width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="4" width="20" height="16" rx="2" />
<path d="M22 7l-10 7L2 7" />
</svg>
Expand Down Expand Up @@ -46,7 +47,7 @@ const SOCIAL = [
/* ------------------------------------------------------------------ */
/* Footer */
/* ------------------------------------------------------------------ */
export default function Footer() {
function FullFooter() {
const currentYear = new Date().getFullYear();

return (
Expand All @@ -56,7 +57,7 @@ export default function Footer() {
<div className="grid grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-4">
{/* Column 1 — Brand */}
<div className="sm:col-span-2 lg:col-span-1">
<Link href="/" className="inline-flex items-center gap-2 group">
<Link href="/" className="inline-flex min-h-12 items-center gap-2 group" aria-label="AprovIA — Página inicial">
<AprovIALogo size="sm" />
</Link>
<p className="mt-4 text-sm leading-relaxed text-[var(--text-3)] max-w-[260px]">
Expand All @@ -73,7 +74,7 @@ export default function Footer() {
<Link
href={href}
className="
text-sm text-[var(--text-3)]
inline-flex min-h-12 items-center text-sm text-[var(--text-2)]
hover:text-[var(--text)]
transition-colors duration-[var(--duration-fast)]
"
Expand All @@ -94,7 +95,7 @@ export default function Footer() {
<Link
href={href}
className="
text-sm text-[var(--text-3)]
inline-flex min-h-12 items-center text-sm text-[var(--text-2)]
hover:text-[var(--text)]
transition-colors duration-[var(--duration-fast)]
"
Expand All @@ -117,8 +118,8 @@ export default function Footer() {
target={href.startsWith('http') ? '_blank' : undefined}
rel={href.startsWith('http') ? 'noopener noreferrer' : undefined}
className="
inline-flex items-center justify-center w-9 h-9
rounded-lg text-[var(--text-3)]
inline-flex items-center justify-center w-12 h-12
rounded-lg text-[var(--text-2)]
hover:text-[var(--text)] hover:bg-[var(--surface-2)]
transition-colors duration-[var(--duration-fast)]
"
Expand All @@ -137,7 +138,7 @@ export default function Footer() {
<div className="container flex flex-col sm:flex-row items-center justify-between py-5 gap-3 text-sm text-[var(--text-3)]">
<p>© {currentYear} AprovIA. Todos os direitos reservados.</p>
<div className="flex flex-wrap items-center justify-center gap-3">
<CookiePreferencesButton className="text-sm text-[var(--text-3)] transition-colors hover:text-[var(--text)]" />
<CookiePreferencesButton className="min-h-12 rounded-lg px-2 text-sm text-[var(--text-2)] transition-colors hover:text-[var(--text)]" />
<span aria-hidden="true" className="hidden sm:inline">•</span>
<p>Feito com ❤️ para estudantes do Brasil</p>
</div>
Expand All @@ -146,3 +147,23 @@ export default function Footer() {
</footer>
);
}

function CompactFooter() {
return (
<footer className="border-t border-[var(--border)] bg-[var(--surface)]">
<div className="container flex flex-col items-center justify-between gap-2 py-4 text-sm text-[var(--text-2)] md:flex-row">
<p>© {new Date().getFullYear()} AprovIA</p>
<nav aria-label="Privacidade e termos" className="flex flex-wrap items-center justify-center gap-x-3">
<Link href="/privacidade" className="inline-flex min-h-12 items-center rounded-lg px-2 text-[var(--text-2)] hover:text-[var(--text)]">Privacidade</Link>
<Link href="/termos" className="inline-flex min-h-12 items-center rounded-lg px-2 text-[var(--text-2)] hover:text-[var(--text)]">Termos</Link>
<CookiePreferencesButton className="min-h-12 rounded-lg px-2 text-sm hover:text-[var(--text)]" />
{SOCIAL.map(({ href, label, Icon }) => <a key={label} href={href} target={href.startsWith('http') ? '_blank' : undefined} rel={href.startsWith('http') ? 'noopener noreferrer' : undefined} className="inline-flex min-h-12 items-center gap-2 rounded-lg px-2 text-[var(--text-2)] hover:text-[var(--text)]"><Icon />{label}</a>)}
</nav>
</div>
</footer>
);
}

export default function Footer() {
return <FooterVariant full={<FullFooter />} compact={<CompactFooter />} />;
}
Loading
Loading