From fd8e9049bf7c8ce12568a122d0c35fd1824ac9be Mon Sep 17 00:00:00 2001 From: ruimoraes Date: Fri, 5 Jun 2026 00:11:45 -0300 Subject: [PATCH] add analytics --- .gitignore | 8 +- app/.env.example | 3 + app/Dockerfile | 10 +- app/package.json | 2 +- app/src/App.jsx | 123 +++++++++++- .../letramento/shared/letramento.css | 59 ++++-- .../components/CookieBanner/CookieBanner.css | 1 + .../components/CookieBanner/CookieBanner.jsx | 90 +++++++++ app/src/components/game/GameBase.jsx | 2 +- app/src/contexts/GameProgressContext.jsx | 15 +- app/src/contexts/GameStateContext.jsx | 63 +++++- .../contexts/GameStateContext.test.debug.js | 35 ++++ app/src/main.jsx | 4 + app/src/pages/Faq/Faq.jsx | 13 ++ app/src/pages/HomePage/Footer.jsx | 8 + app/src/pages/PrivacyPolicy/PrivacyPolicy.jsx | 190 ++++++++++++++++++ .../services/analytics/AnalyticsManager.js | 96 +++++++++ app/src/services/analytics/EventBatcher.js | 62 ++++++ app/src/services/analytics/NetworkDetector.js | 30 +++ app/src/services/analytics/analytics.test.js | 98 +++++++++ app/src/services/analytics/config.js | 12 ++ .../services/analytics/googleConsentMode.js | 53 +++++ app/src/services/analytics/index.js | 8 + .../analytics/providers/BaseProvider.js | 29 +++ .../analytics/providers/GA4Provider.js | 169 ++++++++++++++++ .../analytics/providers/NoopProvider.js | 23 +++ .../services/analytics/useActivityTracking.js | 74 +++++++ .../analytics/useGameActivityTracking.js | 126 ++++++++++++ .../analytics/useLetramentoTracking.js | 68 +++++++ app/src/services/analytics/usePageTracking.js | 17 ++ app/src/services/consent/ConsentManager.js | 38 ++++ app/src/services/consent/index.js | 2 + app/src/services/consent/useConsent.js | 34 ++++ app/stats.html | 2 +- docker-compose.yaml | 3 + 35 files changed, 1540 insertions(+), 30 deletions(-) create mode 100644 app/.env.example create mode 100644 app/src/components/CookieBanner/CookieBanner.css create mode 100644 app/src/components/CookieBanner/CookieBanner.jsx create mode 100644 app/src/contexts/GameStateContext.test.debug.js create mode 100644 app/src/pages/PrivacyPolicy/PrivacyPolicy.jsx create mode 100644 app/src/services/analytics/AnalyticsManager.js create mode 100644 app/src/services/analytics/EventBatcher.js create mode 100644 app/src/services/analytics/NetworkDetector.js create mode 100644 app/src/services/analytics/analytics.test.js create mode 100644 app/src/services/analytics/config.js create mode 100644 app/src/services/analytics/googleConsentMode.js create mode 100644 app/src/services/analytics/index.js create mode 100644 app/src/services/analytics/providers/BaseProvider.js create mode 100644 app/src/services/analytics/providers/GA4Provider.js create mode 100644 app/src/services/analytics/providers/NoopProvider.js create mode 100644 app/src/services/analytics/useActivityTracking.js create mode 100644 app/src/services/analytics/useGameActivityTracking.js create mode 100644 app/src/services/analytics/useLetramentoTracking.js create mode 100644 app/src/services/analytics/usePageTracking.js create mode 100644 app/src/services/consent/ConsentManager.js create mode 100644 app/src/services/consent/index.js create mode 100644 app/src/services/consent/useConsent.js diff --git a/.gitignore b/.gitignore index 85e3640..6c404a4 100644 --- a/.gitignore +++ b/.gitignore @@ -144,4 +144,10 @@ deploy_homolog.sh /SDD app/.vscode/settings.json -app/specs \ No newline at end of file +app/specs +# Environment variables - never commit secrets +.env +.env.*.local +app/.env.local +app/.env.production +app/.env.offline diff --git a/app/.env.example b/app/.env.example new file mode 100644 index 0000000..ffcbe84 --- /dev/null +++ b/app/.env.example @@ -0,0 +1,3 @@ +VITE_ANALYTICS_PROVIDER=ga4 +VITE_GA4_ID= +VITE_GA4_DEBUG=false diff --git a/app/Dockerfile b/app/Dockerfile index 4cc47da..4d4a502 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -3,9 +3,17 @@ FROM node:20-alpine AS builder WORKDIR /app ARG GIT_COMMIT_HASH=unknown -ARG APP_VERSION=1.1.2 +ARG APP_VERSION=1.1.3 +ARG VITE_ANALYTICS_PROVIDER=ga4 +ARG VITE_GA4_ID=G-57HGKF773M +ARG VITE_GA4_DEBUG=false + +ENV NODE_ENV=production ENV VITE_APP_VERSION=$APP_VERSION ENV VITE_GIT_HASH=$GIT_COMMIT_HASH +ENV VITE_ANALYTICS_PROVIDER=$VITE_ANALYTICS_PROVIDER +ENV VITE_GA4_ID=$VITE_GA4_ID +ENV VITE_GA4_DEBUG=$VITE_GA4_DEBUG RUN npm install -g pnpm diff --git a/app/package.json b/app/package.json index 7a27d14..bd56f59 100644 --- a/app/package.json +++ b/app/package.json @@ -2,7 +2,7 @@ "name": "decoda", "private": true, "description": "Aplicação educacional desenvolvida para ensino de programação básica e letramento digital", - "version": "1.1.2", + "version": "1.1.3", "main": "main.cjs", "homepage": "./", "type": "module", diff --git a/app/src/App.jsx b/app/src/App.jsx index d56cb15..dfe40e5 100644 --- a/app/src/App.jsx +++ b/app/src/App.jsx @@ -4,12 +4,106 @@ * @module App */ -import { lazy, Suspense } from "react"; +import { lazy, Suspense, useEffect, useState } from "react"; import { HashRouter as Router, Routes, Route, useLocation } from "react-router-dom"; import "./App.css"; import HomePage from "./pages/HomePage/HomePage"; import LabPython from "./pages/LabPython/LabPython"; import ScrollToTop from "./components/ScrollToTop"; +import { getAnalytics, usePageTracking } from "./services/analytics"; +import { initializeAnalytics, analyticsConfig } from "./services/analytics"; +import { ConsentManager } from "./services/consent"; + +// Inline CookieBanner para evitar bloqueio do Brave +function CookieBanner() { + const [visible, setVisible] = useState(false); + + useEffect(() => { + try { + const stored = localStorage.getItem('decoda_consent'); + if (!stored) { + setVisible(true); + } + } catch (e) { + console.error('Consent check failed:', e); + } + }, []); + + const handleAccept = () => { + try { + localStorage.setItem('decoda_consent', JSON.stringify({ + version: '1', + accepted: true, + timestamp: new Date().toISOString(), + })); + getAnalytics()._setConsentGrantedInternal(true); + setVisible(false); + } catch (e) { + console.error('Failed to accept consent:', e); + } + }; + + const handleReject = () => { + try { + localStorage.setItem('decoda_consent', JSON.stringify({ + version: '1', + accepted: false, + timestamp: new Date().toISOString(), + })); + getAnalytics()._setConsentGrantedInternal(false); + setVisible(false); + } catch (e) { + console.error('Failed to reject consent:', e); + } + }; + + if (!visible) return null; + + return ( +
+
+
+
+
+
+
+

+ Sua privacidade é importante +

+

+ Utilizamos dados sobre como você usa a plataforma para melhorar a experiência educacional + e entender quais recursos são mais úteis.{' '} + + Saiba mais + +

+
+
+ + +
+
+
+
+
+
+ ); +} const Playground = lazy(() => import("./pages/Playground/Playground")); const About = lazy(() => import("./pages/About/About")); @@ -20,6 +114,11 @@ const Iniciativas = lazy(() => import("./pages/Iniciativas/Iniciativas")); const IniciativaDetalhe = lazy(() => import("./pages/Iniciativas/IniciativaDetalhe")); const PrimeirosPassos = lazy(() => import("./pages/PrimeirosPassos/PrimeirosPassos")); const CategoriaLetramentoView = lazy(() => import("./pages/PrimeirosPassos/CategoriaLetramentoView")); +const PrivacyPolicy = lazy(() => + import("./pages/PrivacyPolicy/PrivacyPolicy").then(m => ({ + default: m.PrivacyPolicy + })) +); //Atividades const AspiradorGame = lazy(() => import("./atividades/programacao/aspirador/AspiradorGame")); @@ -61,13 +160,15 @@ const LoadingFallback = () => ( ); // Separated so we can call useLocation (requires being inside Router) -function AppRoutes() { +function AppRoutes({ analyticsReady = false }) { const location = useLocation(); // When navigating to a letramento category, the caller passes // { state: { backgroundLocation: location } } so the previous page // keeps rendering behind the modal overlay. const backgroundLocation = location.state?.backgroundLocation; + usePageTracking(analyticsReady); + return ( <> @@ -79,6 +180,7 @@ function AppRoutes() { } /> } /> } /> + } /> } /> } /> } /> @@ -107,11 +209,26 @@ function AppRoutes() { } export default function App() { + const [analyticsReady, setAnalyticsReady] = useState(false); + + useEffect(() => { + const hasConsent = ConsentManager.hasConsent(); + initializeAnalytics({ + providerType: analyticsConfig.providerType, + measurementId: analyticsConfig.measurementId, + hasConsent, + debugMode: analyticsConfig.debugMode, + }); + + setAnalyticsReady(true); + }, []); + return ( }> - + + ); } diff --git a/app/src/atividades/letramento/shared/letramento.css b/app/src/atividades/letramento/shared/letramento.css index 85645e6..d364920 100644 --- a/app/src/atividades/letramento/shared/letramento.css +++ b/app/src/atividades/letramento/shared/letramento.css @@ -616,6 +616,10 @@ video { pointer-events: none; } +.\!visible { + visibility: visible !important; +} + .visible { visibility: visible; } @@ -984,10 +988,6 @@ video { height: 0.25rem; } -.h-1\.5 { - height: 0.375rem; -} - .h-10 { height: 2.5rem; } @@ -2885,10 +2885,6 @@ video { background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1)); } -.bg-gray-200\/50 { - background-color: rgb(229 231 235 / 0.5); -} - .bg-gray-300 { --tw-bg-opacity: 1; background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1)); @@ -4369,6 +4365,10 @@ video { --tw-gradient-to: #ec4899 var(--tw-gradient-to-position); } +.to-pink-600 { + --tw-gradient-to: #db2777 var(--tw-gradient-to-position); +} + .to-purple-100 { --tw-gradient-to: #f3e8ff var(--tw-gradient-to-position); } @@ -6940,6 +6940,10 @@ video { color: rgb(161 98 7 / 0.95); } +.underline { + text-decoration-line: underline; +} + .underline-offset-4 { text-underline-offset: 4px; } @@ -7125,10 +7129,6 @@ video { transition-duration: 150ms; } -.duration-1000 { - transition-duration: 1000ms; -} - .duration-150 { transition-duration: 150ms; } @@ -7149,10 +7149,6 @@ video { transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); } -.ease-out { - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); -} - .\[-webkit-text-stroke\:2px_black\] { -webkit-text-stroke: 2px black; } @@ -7367,6 +7363,11 @@ video { color: rgb(37 99 235 / var(--tw-text-opacity, 1)); } +.hover\:text-blue-700:hover { + --tw-text-opacity: 1; + color: rgb(29 78 216 / var(--tw-text-opacity, 1)); +} + .hover\:text-gray-300:hover { --tw-text-opacity: 1; color: rgb(209 213 219 / var(--tw-text-opacity, 1)); @@ -7382,6 +7383,11 @@ video { color: rgb(31 41 55 / var(--tw-text-opacity, 1)); } +.hover\:text-red-700:hover { + --tw-text-opacity: 1; + color: rgb(185 28 28 / var(--tw-text-opacity, 1)); +} + .hover\:underline:hover { text-decoration-line: underline; } @@ -7627,6 +7633,10 @@ video { width: 5rem; } + .md\:w-auto { + width: auto; + } + .md\:max-w-\[378\.67px\] { max-width: 378.67px; } @@ -7671,6 +7681,18 @@ video { gap: 1.75rem; } + .md\:gap-8 { + gap: 2rem; + } + + .md\:p-6 { + padding: 1.5rem; + } + + .md\:p-8 { + padding: 2rem; + } + .md\:px-0 { padding-left: 0px; padding-right: 0px; @@ -7709,6 +7731,11 @@ video { padding-top: 3rem; } + .md\:text-2xl { + font-size: 1.5rem; + line-height: 2rem; + } + .md\:text-5xl { font-size: 3rem; line-height: 1; diff --git a/app/src/components/CookieBanner/CookieBanner.css b/app/src/components/CookieBanner/CookieBanner.css new file mode 100644 index 0000000..caa37fb --- /dev/null +++ b/app/src/components/CookieBanner/CookieBanner.css @@ -0,0 +1 @@ +/* CookieBanner styles are now handled by Tailwind CSS in the JSX component */ diff --git a/app/src/components/CookieBanner/CookieBanner.jsx b/app/src/components/CookieBanner/CookieBanner.jsx new file mode 100644 index 0000000..dff6d6b --- /dev/null +++ b/app/src/components/CookieBanner/CookieBanner.jsx @@ -0,0 +1,90 @@ +import { useEffect, useState } from 'react'; + +export function CookieBanner() { + const [visible, setVisible] = useState(false); + const [acceptConsent, rejectConsent] = [null, null]; + + useEffect(() => { + try { + const stored = localStorage.getItem('decoda_consent'); + if (!stored) { + setVisible(true); + } + } catch (e) { + console.error('Consent check failed:', e); + } + }, []); + + const handleAccept = () => { + try { + localStorage.setItem('decoda_consent', JSON.stringify({ + version: '1', + accepted: true, + timestamp: new Date().toISOString(), + })); + setVisible(false); + } catch (e) { + console.error('Failed to accept consent:', e); + } + }; + + const handleReject = () => { + try { + localStorage.setItem('decoda_consent', JSON.stringify({ + version: '1', + accepted: false, + timestamp: new Date().toISOString(), + })); + setVisible(false); + } catch (e) { + console.error('Failed to reject consent:', e); + } + }; + + if (!visible) return null; + + return ( +
+
+
+
+
+
+
+

+ Sua privacidade é importante +

+

+ Utilizamos dados sobre como você usa a plataforma para melhorar a experiência educacional + e entender quais recursos são mais úteis.{' '} + + Saiba mais + +

+
+
+ + +
+
+
+
+
+
+ ); +} diff --git a/app/src/components/game/GameBase.jsx b/app/src/components/game/GameBase.jsx index 0df393d..9f10b31 100644 --- a/app/src/components/game/GameBase.jsx +++ b/app/src/components/game/GameBase.jsx @@ -146,7 +146,7 @@ function GameBaseContent({ currentPhase={currentPhase} gameConfig={gameConfig} onChangePhase={(fase) => { - setCurrentPhase(fase); + setCurrentPhase(fase, "manual_selector"); setModalFasesAberto(false); }} onResetProgress={handleResetProgresso} diff --git a/app/src/contexts/GameProgressContext.jsx b/app/src/contexts/GameProgressContext.jsx index 5673b6a..a161a8c 100644 --- a/app/src/contexts/GameProgressContext.jsx +++ b/app/src/contexts/GameProgressContext.jsx @@ -44,10 +44,21 @@ export function GameProgressProvider({ children, gameConfig }) { // Carrega progresso salvo na montagem; aplica debug mode se necessário. useEffect(() => { - const urlParams = new URLSearchParams(window.location.search); - const debugKey = Array.from(urlParams.keys()).find( + // Try both window.location.search and hash-based query params (for HashRouter) + let urlParams = new URLSearchParams(window.location.search); + let debugKey = Array.from(urlParams.keys()).find( (k) => k.toLowerCase() === "debug", ); + + // If not found in search, try hash (for HashRouter with ?debug=true after hash) + if (!debugKey && window.location.hash.includes('?')) { + const hashSearch = window.location.hash.split('?')[1]; + urlParams = new URLSearchParams(hashSearch); + debugKey = Array.from(urlParams.keys()).find( + (k) => k.toLowerCase() === "debug", + ); + } + const isDebug = urlParams.get(debugKey)?.toLowerCase() === "true"; if (isDebug) { diff --git a/app/src/contexts/GameStateContext.jsx b/app/src/contexts/GameStateContext.jsx index c984618..aefaab3 100644 --- a/app/src/contexts/GameStateContext.jsx +++ b/app/src/contexts/GameStateContext.jsx @@ -16,6 +16,8 @@ import React, { import PropTypes from "prop-types"; import { gameEventBus } from "../utils/gameEvents"; import { GameProgressProvider, useGameProgress } from "./GameProgressContext"; +import { useGameActivityTracking } from "../services/analytics/useGameActivityTracking"; +import { getAnalytics } from "../services/analytics/AnalyticsManager"; export const GAME_STATES = { PARADO: "parado", @@ -83,16 +85,53 @@ function GameStateInnerProvider({ children, gameConfig }) { const [codeEditorContent, setCodeEditorContent] = useState(""); const [failureMessage, setFailureMessage] = useState(""); const [isDebugMode, setIsDebugMode] = useState(() => { - const urlParams = new URLSearchParams(window.location.search); - const debugKey = Array.from(urlParams.keys()).find( + // Try both window.location.search and hash-based query params (for HashRouter) + let urlParams = new URLSearchParams(window.location.search); + let debugKey = Array.from(urlParams.keys()).find( (k) => k.toLowerCase() === "debug", ); - return urlParams.get(debugKey)?.toLowerCase() === "true"; + + // If not found in search, try hash (for HashRouter with ?debug=true after hash) + if (!debugKey && window.location.hash.includes('?')) { + const hashSearch = window.location.hash.split('?')[1]; + urlParams = new URLSearchParams(hashSearch); + debugKey = Array.from(urlParams.keys()).find( + (k) => k.toLowerCase() === "debug", + ); + } + + const isDebug = urlParams.get(debugKey)?.toLowerCase() === "true"; + return isDebug; }); const getCodeFromWorkspace = useRef(null); const getCodeFromEditor = useRef(null); + // Rastrear atividade do jogo (analytics) + useGameActivityTracking(gameConfig, { + executionState, + currentPhase, + currentBlockCount, + editorType, + }); + + const trackGenericEvent = useCallback( + (eventName, extraData = {}) => { + if (!gameConfig?.gameId) return; + + const analytics = getAnalytics(); + analytics.trackEvent(eventName, { + atividade_id: gameConfig.gameId, + atividade_nome: gameConfig.gameName || gameConfig.gameId, + fase_numero: currentPhase, + editor_tipo: editorType, + blocos_atuais: currentBlockCount, + ...extraData, + }); + }, + [gameConfig?.gameId, gameConfig?.gameName, currentPhase, editorType, currentBlockCount], + ); + /** * Executa o código/workspace do editor de forma síncrona. * Registra o código gerado e muda estado para EXECUTANDO. @@ -103,6 +142,14 @@ function GameStateInnerProvider({ children, gameConfig }) { * @throws {console.error} Se editor não registrou a função de execução */ const execute = () => { + trackGenericEvent("tentativa_execucao", { + origem_acao: "botao_executar", + }); + + trackGenericEvent("blocos_usados", { + origem_acao: "execucao", + }); + if (editorType === "code") { if (getCodeFromEditor.current) { const codigo = getCodeFromEditor.current(); @@ -192,7 +239,15 @@ function GameStateInnerProvider({ children, gameConfig }) { * @param {number} numeroFase - Número da fase para navegar (1-indexed) * @returns {void} */ - const changePhase = (numeroFase) => { + const changePhase = (numeroFase, source = "unknown") => { + if (source === "manual_selector") { + trackGenericEvent("troca_fase_manual", { + fase_origem: currentPhase, + fase_destino: numeroFase, + origem_acao: source, + }); + } + progressChangePhase(numeroFase); setExecutionState(GAME_STATES.PARADO); setGeneratedCode(""); diff --git a/app/src/contexts/GameStateContext.test.debug.js b/app/src/contexts/GameStateContext.test.debug.js new file mode 100644 index 0000000..2ed22a0 --- /dev/null +++ b/app/src/contexts/GameStateContext.test.debug.js @@ -0,0 +1,35 @@ +/** + * Test to verify debug mode detection + * Run in browser console after accessing ?debug=true + */ + +// Test URL parsing +function testDebugDetection() { + console.log('=== DEBUG MODE DETECTION TEST ==='); + console.log('window.location.href:', window.location.href); + console.log('window.location.search:', window.location.search); + console.log('window.location.hash:', window.location.hash); + + const urlParams = new URLSearchParams(window.location.search); + const debugKey = Array.from(urlParams.keys()).find( + (k) => k.toLowerCase() === "debug", + ); + + console.log('debugKey found:', debugKey); + console.log('debugKey value:', urlParams.get(debugKey)); + + const isDebug = urlParams.get(debugKey)?.toLowerCase() === "true"; + console.log('isDebugMode:', isDebug); + + // Also test if passing via hash works + const hashParams = new URLSearchParams(window.location.hash.split('?')[1]); + const debugKeyHash = Array.from(hashParams.keys()).find( + (k) => k.toLowerCase() === "debug", + ); + console.log('\n--- Via Hash ---'); + console.log('debugKeyHash found:', debugKeyHash); + console.log('debugKeyHash value:', hashParams.get(debugKeyHash)); +} + +// Run it +testDebugDetection(); diff --git a/app/src/main.jsx b/app/src/main.jsx index e291142..493a2db 100644 --- a/app/src/main.jsx +++ b/app/src/main.jsx @@ -8,6 +8,10 @@ import React from "react"; import ReactDOM from "react-dom/client"; import "./blockly/blocklyConfig.js"; // Configuração global do Blockly (locale PT-BR) import App from "./App.jsx"; +import { initGoogleConsentMode } from "./services/analytics"; + +// Initialize Google Consent Mode before loading analytics +initGoogleConsentMode(); // Log de versão para rastreabilidade em produção if (import.meta.env.PROD) { diff --git a/app/src/pages/Faq/Faq.jsx b/app/src/pages/Faq/Faq.jsx index 28d6475..ea4c51e 100644 --- a/app/src/pages/Faq/Faq.jsx +++ b/app/src/pages/Faq/Faq.jsx @@ -177,6 +177,19 @@ export default function Faq() { ))} + +
+

+ Dúvidas sobre privacidade e como coletamos dados? + {" "} + + Leia nossa Política de Privacidade + +

+
diff --git a/app/src/pages/HomePage/Footer.jsx b/app/src/pages/HomePage/Footer.jsx index 0bc4d31..c83ce42 100644 --- a/app/src/pages/HomePage/Footer.jsx +++ b/app/src/pages/HomePage/Footer.jsx @@ -57,6 +57,14 @@ const Footer = () => { FAQ +
  • + + Privacidade + +
  • diff --git a/app/src/pages/PrivacyPolicy/PrivacyPolicy.jsx b/app/src/pages/PrivacyPolicy/PrivacyPolicy.jsx new file mode 100644 index 0000000..e9e975b --- /dev/null +++ b/app/src/pages/PrivacyPolicy/PrivacyPolicy.jsx @@ -0,0 +1,190 @@ +import Navbar from "../../components/Navbar"; +import Footer from "../HomePage/Footer"; + +const privacyData = [ + { + id: 1, + question: "Que dados o Decoda coleta?", + answer: + "O Decoda coleta dados anônimos sobre como você usa a plataforma para ajudar na melhoria contínua. Coletamos informações como quais páginas você visita, quanto tempo você passa em cada atividade, seu tipo de navegador e sistema operacional. Não coletamos dados pessoais como nome, email ou outras informações de identificação pessoal sem seu consentimento explícito.", + }, + { + id: 2, + question: "Para que são usados esses dados?", + answer: + "Os dados coletados são usados exclusivamente para entender como os estudantes e educadores interagem com a plataforma, permitindo que melhoremos a usabilidade, corrijamos problemas técnicos e desenvolvamos novas funcionalidades mais alinhadas com as necessidades educacionais.", + }, + { + id: 3, + question: "Como funciona o consentimento de cookies?", + answer: + "Respeitamos sua privacidade. Ao visitar o Decoda, você verá um aviso informando sobre a coleta de dados. Você pode aceitar ou rejeitar explicitamente. Se rejeitar, nenhum dado será coletado. Você pode mudar sua decisão a qualquer momento através das configurações de privacidade do site.", + }, + { + id: 4, + question: "Meus dados estão seguros?", + answer: + "Sim. Todos os dados são transmitidos de forma criptografada (HTTPS) e são completamente anônimos. Nenhum usuário individual pode ser identificado pelos dados coletados. Os dados são agregados e processados com os mais altos padrões de segurança.", + }, + { + id: 5, + question: "O que é armazenado localmente no meu navegador?", + answer: + "O Decoda armazena seu progresso localmente no seu navegador (dados como quais atividades você completou, seu código blocos salvos). Esses dados são seus e permanecem no seu dispositivo. Nenhum desses dados pessoais de progresso é enviado para terceiros.", + }, + { + id: 6, + question: "Como funciona o Decoda offline?", + answer: + "O Decoda pode funcionar completamente offline através da versão PWA (Progressive Web App) ou desktop (Electron). Quando offline, nenhum dado é coletado ou enviado. O rastreamento de uso só ocorre quando você está conectado à internet.", + }, + { + id: 7, + question: "Como optar por não ser rastreado?", + answer: + "Você tem várias opções para optar por não ser rastreado:", + list: [ + "Rejeitar cookies no aviso ao entrar no site", + "Usar o navegador em modo incógnito/privado", + "Desabilitar 'Do Not Track' (DNT) nas configurações do seu navegador", + "Limpar cookies e dados do navegador regularmente", + ], + }, + { + id: 8, + question: "Há publicidade no Decoda?", + answer: + "Não. O Decoda não possui publicidade. A plataforma é financiada para servir educadores e estudantes de forma gratuita, sem modelos baseados em publicidade ou venda de dados.", + }, + { + id: 9, + question: "É preciso criar uma conta?", + answer: + "Não é necessário. O Decoda funciona sem cadastro ou login. Todo seu progresso é salvo localmente no seu navegador, mantendo a máxima privacidade e simplicidade.", + }, + { + id: 10, + question: "Com quanto tempo os dados são deletados?", + answer: + "Os dados agregados são mantidos por um período padrão para análise de tendências. Você pode limpar todos os dados em qualquer momento nas configurações de privacidade do seu navegador ou através do aviso de cookies no Decoda.", + }, + { + id: 11, + question: "Posso compartilhar meus dados de atividade?", + answer: + "Seu progresso é mantido privado no seu dispositivo. Se você deseja compartilhar, você pode exportar ou descrever manualmente seus resultados. Nenhum dado é compartilhado automaticamente sem seu consentimento explícito.", + }, + { + id: 12, + question: "Alterações nesta Política", + answer: + "Podemos atualizar esta Política de Privacidade ocasionalmente para refletir melhorias na plataforma ou mudanças legais. Recomendamos revisar esta página periodicamente para estar informado sobre qualquer mudança.", + }, +]; + +export function PrivacyPolicy() { + return ( + <> + {/* Navegação */} + + +
    + {/* Container principal */} +
    +
    +

    + Política de Privacidade +

    + +

    + O Decoda é compromissado com a privacidade e transparência. Esta página + explica como coletamos e usamos dados, e como você pode controlar suas + preferências de privacidade. +

    + +
    + {privacyData.map((item, index) => ( +
    + + +
    +

    + {item.answer} +

    + {item.list && ( +
      + {item.list.map((listItem, idx) => ( +
    • {listItem}
    • + ))} +
    + )} +
    +
    + ))} +
    + +
    +

    + Última atualização: {new Date().toLocaleDateString('pt-BR')} +

    +

    + Se você tiver dúvidas sobre nossa Política de Privacidade, entre em contato conosco. +

    +
    +
    +
    +
    + + {/* Footer */} +