From d743cb568ac14327267fc2606bed24c8e79c1f8c Mon Sep 17 00:00:00 2001 From: Gabriel Pereira Rodrigues Date: Wed, 30 Sep 2026 14:30:47 -0300 Subject: [PATCH] Improve essay workflow and canonical publication --- FRONTEND_INVENTORY.md | 21 +- README.md | 16 +- app/(auth)/layout.tsx | 10 +- app/auth/layout.tsx | 12 + app/components/layout/Footer.tsx | 39 +- app/components/layout/FooterVariant.tsx | 9 + app/components/layout/Header.tsx | 109 ++++- app/components/shared/RebrandingBanner.tsx | 70 ++- app/conta/ContaPageClient.tsx | 26 +- app/conta/editar/page.tsx | 8 + app/conta/page.tsx | 8 + app/doacao/layout.tsx | 13 + app/layout.tsx | 9 +- app/noticias/[slug]/page.tsx | 22 +- app/noticias/admin/layout.tsx | 13 + app/noticias/page.tsx | 7 + app/noticias/pesquisa/layout.tsx | 13 + app/page.tsx | 9 +- app/planos/page.tsx | 7 +- app/privacidade/page.tsx | 7 + app/questoes/page.tsx | 8 + app/redacao/PhotoUpload.tsx | 47 +- app/redacao/RedacaoPageClient.tsx | 517 +++++++-------------- app/redacao/essay-presentation.ts | 42 ++ app/redacao/page.tsx | 8 + app/redacao/prepareOcrImage.ts | 3 + app/redacao/redacao.module.css | 54 +++ app/redacao/useEssayWorkflow.ts | 22 +- app/resultados/[id]/page.tsx | 7 + app/sobre/page.tsx | 7 + app/styles/base.css | 2 +- app/termos/page.tsx | 7 + docs/redacao-qa.md | 71 +++ lib/client/drafts.ts | 32 +- lib/client/rebranding-notice.ts | 29 ++ lib/client/use-user-draft.ts | 6 +- lib/client/workspace-navigation.ts | 27 ++ lib/constants/site.ts | 7 + lib/contracts/page-metadata.ts | 41 ++ lib/contracts/site-routing.ts | 33 ++ next-sitemap.config.js | 20 +- proxy.ts | 20 +- public/sitemap.xml | 19 +- tests/systems/essay-presentation.test.ts | 39 ++ tests/systems/frontend-navigation.test.ts | 78 ++++ tests/systems/student-workflows.test.ts | 59 ++- 46 files changed, 1126 insertions(+), 507 deletions(-) create mode 100644 app/auth/layout.tsx create mode 100644 app/components/layout/FooterVariant.tsx create mode 100644 app/doacao/layout.tsx create mode 100644 app/noticias/admin/layout.tsx create mode 100644 app/noticias/pesquisa/layout.tsx create mode 100644 app/redacao/essay-presentation.ts create mode 100644 app/redacao/redacao.module.css create mode 100644 docs/redacao-qa.md create mode 100644 lib/client/rebranding-notice.ts create mode 100644 lib/client/workspace-navigation.ts create mode 100644 lib/constants/site.ts create mode 100644 lib/contracts/page-metadata.ts create mode 100644 lib/contracts/site-routing.ts create mode 100644 tests/systems/essay-presentation.test.ts create mode 100644 tests/systems/frontend-navigation.test.ts diff --git a/FRONTEND_INVENTORY.md b/FRONTEND_INVENTORY.md index 8990dfe..341bb3a 100644 --- a/FRONTEND_INVENTORY.md +++ b/FRONTEND_INVENTORY.md @@ -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. @@ -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 | @@ -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 | @@ -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 | @@ -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 | --- @@ -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 | diff --git a/README.md b/README.md index b3adeba..60cb953 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 | diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx index 130cc93..1f2049f 100644 --- a/app/(auth)/layout.tsx +++ b/app/(auth)/layout.tsx @@ -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 = [ diff --git a/app/auth/layout.tsx b/app/auth/layout.tsx new file mode 100644 index 0000000..3e0ce55 --- /dev/null +++ b/app/auth/layout.tsx @@ -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; +} diff --git a/app/components/layout/Footer.tsx b/app/components/layout/Footer.tsx index 9a71dbf..34024e5 100644 --- a/app/components/layout/Footer.tsx +++ b/app/components/layout/Footer.tsx @@ -1,13 +1,14 @@ 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 ( - + ); @@ -15,7 +16,7 @@ function GitHubIcon({ className }: { className?: string }) { function MailIcon({ className }: { className?: string }) { return ( - + @@ -46,7 +47,7 @@ const SOCIAL = [ /* ------------------------------------------------------------------ */ /* Footer */ /* ------------------------------------------------------------------ */ -export default function Footer() { +function FullFooter() { const currentYear = new Date().getFullYear(); return ( @@ -56,7 +57,7 @@ export default function Footer() {
{/* Column 1 — Brand */}
- +

@@ -73,7 +74,7 @@ export default function Footer() {

© {currentYear} AprovIA. Todos os direitos reservados.

- +

Feito com ❤️ para estudantes do Brasil

@@ -146,3 +147,23 @@ export default function Footer() { ); } + +function CompactFooter() { + return ( +
+
+

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

+ +
+
+ ); +} + +export default function Footer() { + return } compact={} />; +} diff --git a/app/components/layout/FooterVariant.tsx b/app/components/layout/FooterVariant.tsx new file mode 100644 index 0000000..6a9a9b8 --- /dev/null +++ b/app/components/layout/FooterVariant.tsx @@ -0,0 +1,9 @@ +'use client'; + +import type { ReactNode } from 'react'; +import { usePathname } from 'next/navigation'; +import { isWorkspacePath } from '@/lib/client/workspace-navigation'; + +export default function FooterVariant({ full, compact }: { full: ReactNode; compact: ReactNode }) { + return isWorkspacePath(usePathname()) ? compact : full; +} diff --git a/app/components/layout/Header.tsx b/app/components/layout/Header.tsx index acfa7c2..9394afe 100644 --- a/app/components/layout/Header.tsx +++ b/app/components/layout/Header.tsx @@ -1,12 +1,13 @@ 'use client'; -import { startTransition, useEffect, useMemo, useState } from 'react'; +import { startTransition, useEffect, useId, useMemo, useRef, useState } from 'react'; import Link from 'next/link'; import { usePathname, useRouter } from 'next/navigation'; import type { User } from '@supabase/supabase-js'; import { createClient } from '@/lib/supabase/client'; import AprovIALogo from '@/app/components/shared/AprovIALogo'; import { clearUserDrafts } from '@/lib/client/drafts'; +import { CreditCard, History, LogOut, UserRound } from 'lucide-react'; const NAV_LINKS = [ { href: '/', label: 'Início' }, @@ -18,6 +19,11 @@ const NAV_LINKS = [ ] as const; const supabase = createClient(); +const ACCOUNT_LINKS = [ + { href: '/conta/editar', label: 'Perfil', Icon: UserRound }, + { href: '/conta?aba=redacoes', label: 'Histórico de redações', Icon: History }, + { href: '/conta#plano', label: 'Plano', Icon: CreditCard }, +] as const; function isActivePath(pathname: string, href: string): boolean { if (href === '/') { @@ -46,6 +52,10 @@ function AuthActions({ }) { const router = useRouter(); const [submitting, setSubmitting] = useState(false); + const [open, setOpen] = useState(false); + const menuId = useId(); + const containerRef = useRef(null); + const triggerRef = useRef(null); const displayName = useMemo(() => { if (!user) { @@ -62,8 +72,33 @@ function AuthActions({ return metadataName || user.email?.split('@')[0] || 'Minha conta'; }, [user]); - const handleSignOut = async () => { + useEffect(() => { + if (!open) return; + const onPointerDown = (event: PointerEvent) => { + if (!containerRef.current?.contains(event.target as Node)) setOpen(false); + }; + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + event.preventDefault(); + setOpen(false); + triggerRef.current?.focus(); + } + }; + document.addEventListener('pointerdown', onPointerDown); + document.addEventListener('keydown', onKeyDown); + return () => { + document.removeEventListener('pointerdown', onPointerDown); + document.removeEventListener('keydown', onKeyDown); + }; + }, [open]); + + const closeMenu = () => { + setOpen(false); onAction?.(); + }; + + const handleSignOut = async () => { + closeMenu(); setSubmitting(true); try { if (user) clearUserDrafts(user.id); @@ -82,14 +117,14 @@ function AuthActions({ Entrar Começar grátis @@ -98,22 +133,49 @@ function AuthActions({ } return ( -
- - {compact ? 'Minha conta' : displayName} - +
{ + if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false); + }} + > + {open && ( + + )}
); } @@ -207,7 +269,8 @@ export default function Header() {
  • - +
    + {user && }
    - + {!user && }
  • )} diff --git a/app/components/shared/RebrandingBanner.tsx b/app/components/shared/RebrandingBanner.tsx index 5e60e0b..330e50a 100644 --- a/app/components/shared/RebrandingBanner.tsx +++ b/app/components/shared/RebrandingBanner.tsx @@ -1,53 +1,97 @@ 'use client'; import { useSyncExternalStore } from 'react'; +import { usePathname } from 'next/navigation'; import { Megaphone, X } from 'lucide-react'; +import { CANONICAL_SITE_ORIGIN, LEGACY_SITE_HOST } from '@/lib/constants/site'; +import { isLegacySessionPath } from '@/lib/client/workspace-navigation'; +import { + createLegacyHostNotice, + createRebrandingNotice, + LEGACY_HOST_DISMISSAL_KEY, + REBRANDING_DISMISSAL_KEY, + REBRANDING_EXPIRES_AT, +} from '@/lib/client/rebranding-notice'; -const STORAGE_KEY = 'aprovia_rebranding_v1_dismissed'; const CHANGE_EVENT = 'aprovia-rebranding-change'; +const REBRANDING_VISIBLE = 1; +const LEGACY_NOTICE_VISIBLE = 2; +const LEGACY_HOST = 4; +const rebrandingNotice = createRebrandingNotice(() => window.localStorage); +const legacyHostNotice = createLegacyHostNotice(() => window.sessionStorage); function subscribe(callback: () => void) { - window.addEventListener('storage', callback); + const onStorage = (event: StorageEvent) => { + if (event.key === null || event.key === REBRANDING_DISMISSAL_KEY || event.key === LEGACY_HOST_DISMISSAL_KEY) callback(); + }; + let expiryTimer: number | undefined; + const scheduleExpiry = () => { + const delay = REBRANDING_EXPIRES_AT - Date.now(); + if (delay <= 0) return; + expiryTimer = window.setTimeout(() => { + callback(); + scheduleExpiry(); + }, Math.min(delay, 2_147_483_647)); + }; + scheduleExpiry(); + window.addEventListener('storage', onStorage); window.addEventListener(CHANGE_EVENT, callback); + document.addEventListener('visibilitychange', callback); return () => { - window.removeEventListener('storage', callback); + window.clearTimeout(expiryTimer); + window.removeEventListener('storage', onStorage); window.removeEventListener(CHANGE_EVENT, callback); + document.removeEventListener('visibilitychange', callback); }; } function getSnapshot() { - return localStorage.getItem(STORAGE_KEY) !== 'true'; + const rebranding = rebrandingNotice.isVisible() ? REBRANDING_VISIBLE : 0; + const isLegacyHost = window.location.hostname === LEGACY_SITE_HOST; + const legacyNotice = isLegacyHost && legacyHostNotice.isVisible() ? LEGACY_NOTICE_VISIBLE : 0; + return rebranding + legacyNotice + (isLegacyHost ? LEGACY_HOST : 0); } function getServerSnapshot() { - return false; + return 0; } export default function RebrandingBanner() { - const visible = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); + const pathname = usePathname(); + const noticeState = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); + const isLegacyWorkspace = Boolean(noticeState & LEGACY_HOST) && isLegacySessionPath(pathname); + const showLegacyHostNotice = isLegacyWorkspace && Boolean(noticeState & LEGACY_NOTICE_VISIBLE); + const showRebranding = !isLegacyWorkspace && Boolean(noticeState & REBRANDING_VISIBLE); const dismiss = () => { - localStorage.setItem(STORAGE_KEY, 'true'); + if (showLegacyHostNotice) legacyHostNotice.dismiss(); + else rebrandingNotice.dismiss(); window.dispatchEvent(new Event(CHANGE_EVENT)); }; - if (!visible) { + if (!showLegacyHostNotice && !showRebranding) { return null; } return ( -
    {fileInput} @@ -277,7 +280,7 @@ export default function PhotoUpload({ onTextExtracted, currentText, disabled }: {fileInput}
    - + Foto da redação @@ -286,7 +289,7 @@ export default function PhotoUpload({ onTextExtracted, currentText, disabled }: onClick={reset} disabled={disabled} aria-label="Fechar envio de foto" - className="p-1 rounded-md text-[var(--text-3)] hover:text-[var(--text)] hover:bg-[var(--surface-2)] transition-colors" + className="h-12 w-12 inline-flex items-center justify-center rounded-md text-[var(--text-3)] hover:text-[var(--text)] hover:bg-[var(--surface-2)] transition-colors" > @@ -313,9 +316,9 @@ export default function PhotoUpload({ onTextExtracted, currentText, disabled }: )}
    - Abrir foto em tamanho completo + Abrir foto em tamanho completo {optimized && ( -

    A foto foi otimizada no seu navegador antes do envio.

    +

    A foto foi otimizada no seu navegador antes do envio.

    )} @@ -344,7 +347,7 @@ export default function PhotoUpload({ onTextExtracted, currentText, disabled }: {state === 'review' && (
    -

    +

    Compare com a foto e revise o texto extraído. Você pode editá-lo antes de usar.