/** * CookieBanner, banner de consentimento LGPD/ANPD * ================================================== * Template lgpd-compliance. Cumpre o Guia Orientativo da ANPD (23/01/2025): * - Cookies não-essenciais DESATIVADOS por padrão * - Botão "Rejeitar opcionais" com mesma proeminência de "Aceitar todos" * - 2 camadas: banner + modal de granularidade por categoria * - Revogação tão fácil quanto consentir * - Persistência em cookie próprio first-party + envio ao backend pra consent_log * * Stack: Next.js 14+ (App Router) + React 19 + TypeScript + Tailwind. * Cole em `components/legal/CookieBanner.tsx` e renderize no `app/layout.tsx`. * Ajuste o estilo conforme o design system do seu app. */ 'use client'; import { useEffect, useState, useCallback } from 'react'; import Link from 'next/link'; // === Tipos =================================================================== type ConsentCategory = 'essential' | 'functional' | 'analytics' | 'marketing'; type Consent = { /** Versão da política aceita, incremente quando mudar materialmente */ v: string; /** ISO timestamp do consentimento */ ts: string; essential: true; // sempre true functional: boolean; analytics: boolean; marketing: boolean; }; const POLICY_VERSION = '2026-05-24'; // ⚠️ atualize ao mudar a política const COOKIE_NAME = '__cookie_consent'; const COOKIE_MAX_AGE = 60 * 60 * 24 * 365; // 1 ano // === Helpers de cookie ======================================================= function readConsent(): Consent | null { if (typeof document === 'undefined') return null; const raw = document.cookie .split('; ') .find((c) => c.startsWith(`${COOKIE_NAME}=`)); if (!raw) return null; try { return JSON.parse(decodeURIComponent(raw.split('=')[1])); } catch { return null; } } function writeConsent(consent: Consent) { const value = encodeURIComponent(JSON.stringify(consent)); document.cookie = [ `${COOKIE_NAME}=${value}`, `Max-Age=${COOKIE_MAX_AGE}`, 'Path=/', 'SameSite=Lax', location.protocol === 'https:' ? 'Secure' : '', ] .filter(Boolean) .join('; '); } async function postConsentToBackend(consent: Consent) { // Envia pro consent_log do backend. Não bloquear o usuário em caso de erro de rede. try { await fetch('/api/legal/consent', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(consent), keepalive: true, }); } catch { // silenciar, o cookie local já é a prova legal mínima } } // === Componente principal =================================================== export function CookieBanner() { const [visible, setVisible] = useState(false); const [showModal, setShowModal] = useState(false); // Estado dos toggles na 2ª camada const [toggles, setToggles] = useState<{ functional: boolean; analytics: boolean; marketing: boolean; }>({ functional: false, analytics: false, marketing: false }); // Verifica se precisa exibir o banner useEffect(() => { const existing = readConsent(); if (!existing || existing.v !== POLICY_VERSION) { setVisible(true); } }, []); const persist = useCallback((consent: Consent) => { writeConsent(consent); postConsentToBackend(consent); setVisible(false); setShowModal(false); }, []); const acceptAll = () => persist({ v: POLICY_VERSION, ts: new Date().toISOString(), essential: true, functional: true, analytics: true, marketing: true, }); const rejectNonEssential = () => persist({ v: POLICY_VERSION, ts: new Date().toISOString(), essential: true, functional: false, analytics: false, marketing: false, }); const saveCustom = () => persist({ v: POLICY_VERSION, ts: new Date().toISOString(), essential: true, ...toggles, }); if (!visible) return null; return ( <> {/* === 1ª camada: banner =========================================== */}

Usamos cookies estritamente necessários para o funcionamento deste site. Cookies opcionais (funcionais, analíticos e de marketing) só serão usados com seu consentimento, nos termos da{' '} Política de Cookies .

{/* Três botões com PARIDADE visual, exigência ANPD */}
{/* === 2ª camada: modal de granularidade ============================ */} {showModal && (

Preferências de cookies

Escolha quais categorias de cookies você autoriza. Você pode alterar a qualquer momento clicando em "Gerenciar preferências" no rodapé.

{}} /> setToggles((t) => ({ ...t, functional: v })) } /> setToggles((t) => ({ ...t, analytics: v }))} /> setToggles((t) => ({ ...t, marketing: v }))} />
)} ); } // === Botão "Gerenciar preferências" pro footer =============================== /** * Coloque no rodapé do site pra permitir revogação a qualquer momento. * Apaga o cookie atual e força o banner a reaparecer. */ export function ManageConsentButton() { const reopen = () => { document.cookie = `${COOKIE_NAME}=; Max-Age=0; Path=/; SameSite=Lax`; if (typeof window !== 'undefined') window.location.reload(); }; return ( ); } // === Hook utilitário pra ler consentimento em qualquer componente ============ /** * Útil pra renderização condicional de tags de analytics/marketing. * const { analytics, marketing } = useConsent(); * if (analytics) */ export function useConsent(): Partial> { const [consent, setConsent] = useState(null); useEffect(() => { setConsent(readConsent()); }, []); return { essential: true, functional: !!consent?.functional, analytics: !!consent?.analytics, marketing: !!consent?.marketing, }; } // === Subcomponente =========================================================== function CategoryRow(props: { title: string; description: string; checked: boolean; disabled?: boolean; onChange: (v: boolean) => void; }) { return (
props.onChange(e.target.checked)} className="mt-1 h-4 w-4 accent-zinc-900 dark:accent-white" />

{props.title}

{props.description}

); }