From 1c496e8d8dcaf799cd8947f3fde024702b59a425 Mon Sep 17 00:00:00 2001 From: ruimoraes Date: Sat, 13 Jun 2026 22:07:00 -0300 Subject: [PATCH] feat: add plausible --- app/.env | 2 + app/.env.production | 2 + app/Dockerfile | 2 +- app/index.html | 28 ++++++------- app/package.json | 2 +- app/src/App.jsx | 7 +++- .../letramento/shared/letramento.css | 20 --------- app/src/contexts/GameStateContext.jsx | 32 +++++++++++++++ app/src/services/plausible.js | 41 +++++++++++++++++++ app/vite.config.js | 3 ++ 10 files changed, 102 insertions(+), 37 deletions(-) create mode 100644 app/.env create mode 100644 app/.env.production create mode 100644 app/src/services/plausible.js diff --git a/app/.env b/app/.env new file mode 100644 index 0000000..4fb735c --- /dev/null +++ b/app/.env @@ -0,0 +1,2 @@ +VITE_PLAUSIBLE_API=http://localhost/api/event +VITE_PLAUSIBLE_DOMAIN=myapp-dev diff --git a/app/.env.production b/app/.env.production new file mode 100644 index 0000000..674d2d8 --- /dev/null +++ b/app/.env.production @@ -0,0 +1,2 @@ +VITE_PLAUSIBLE_API=https://plausible.mtst.tec.br/api/event +VITE_PLAUSIBLE_DOMAIN=https://decoda.mtst.tec.br diff --git a/app/Dockerfile b/app/Dockerfile index 4cc47da..51a2723 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -3,7 +3,7 @@ FROM node:20-alpine AS builder WORKDIR /app ARG GIT_COMMIT_HASH=unknown -ARG APP_VERSION=1.1.2 +ARG APP_VERSION=1.2.0 ENV VITE_APP_VERSION=$APP_VERSION ENV VITE_GIT_HASH=$GIT_COMMIT_HASH diff --git a/app/index.html b/app/index.html index 7869230..0de87cf 100644 --- a/app/index.html +++ b/app/index.html @@ -1,14 +1,14 @@ - - - - - - - - Decoda - - -
- - - + + + + + + + + Decoda + + +
+ + + diff --git a/app/package.json b/app/package.json index 7a27d14..90bf92e 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.2.0", "main": "main.cjs", "homepage": "./", "type": "module", diff --git a/app/src/App.jsx b/app/src/App.jsx index d56cb15..1675cac 100644 --- a/app/src/App.jsx +++ b/app/src/App.jsx @@ -4,12 +4,13 @@ * @module App */ -import { lazy, Suspense } from "react"; +import { lazy, Suspense, useEffect } 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 { trackPageView } from "./services/plausible"; const Playground = lazy(() => import("./pages/Playground/Playground")); const About = lazy(() => import("./pages/About/About")); @@ -68,6 +69,10 @@ function AppRoutes() { // keeps rendering behind the modal overlay. const backgroundLocation = location.state?.backgroundLocation; + useEffect(() => { + trackPageView(location.pathname); + }, [location.pathname]); + return ( <> diff --git a/app/src/atividades/letramento/shared/letramento.css b/app/src/atividades/letramento/shared/letramento.css index 85645e6..858b0ac 100644 --- a/app/src/atividades/letramento/shared/letramento.css +++ b/app/src/atividades/letramento/shared/letramento.css @@ -653,10 +653,6 @@ video { bottom: 0px; } -.bottom-0 { - bottom: 0px; -} - .bottom-10 { bottom: 2.5rem; } @@ -984,10 +980,6 @@ video { height: 0.25rem; } -.h-1\.5 { - height: 0.375rem; -} - .h-10 { height: 2.5rem; } @@ -2885,10 +2877,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)); @@ -7125,10 +7113,6 @@ video { transition-duration: 150ms; } -.duration-1000 { - transition-duration: 1000ms; -} - .duration-150 { transition-duration: 150ms; } @@ -7149,10 +7133,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; } diff --git a/app/src/contexts/GameStateContext.jsx b/app/src/contexts/GameStateContext.jsx index c984618..4951e72 100644 --- a/app/src/contexts/GameStateContext.jsx +++ b/app/src/contexts/GameStateContext.jsx @@ -16,6 +16,7 @@ import React, { import PropTypes from "prop-types"; import { gameEventBus } from "../utils/gameEvents"; import { GameProgressProvider, useGameProgress } from "./GameProgressContext"; +import { trackEvent } from "../services/plausible"; export const GAME_STATES = { PARADO: "parado", @@ -89,6 +90,9 @@ function GameStateInnerProvider({ children, gameConfig }) { ); return urlParams.get(debugKey)?.toLowerCase() === "true"; }); + const [phaseStartTime, setPhaseStartTime] = useState(null); + const [attemptCount, setAttemptCount] = useState(0); + const [failureCount, setFailureCount] = useState(0); const getCodeFromWorkspace = useRef(null); const getCodeFromEditor = useRef(null); @@ -103,6 +107,8 @@ function GameStateInnerProvider({ children, gameConfig }) { * @throws {console.error} Se editor não registrou a função de execução */ const execute = () => { + setAttemptCount(prev => prev + 1); + if (editorType === "code") { if (getCodeFromEditor.current) { const codigo = getCodeFromEditor.current(); @@ -145,6 +151,19 @@ function GameStateInnerProvider({ children, gameConfig }) { if (!completedPhases.includes(currentPhase)) { setCompletedPhases([...completedPhases, currentPhase]); } + + const durationSeconds = phaseStartTime + ? Math.round((Date.now() - phaseStartTime) / 1000) + : null; + + trackEvent('Activity Success', { + activity: gameConfig.gameId, + phase: currentPhase, + blocks: currentBlockCount, + attempts: attemptCount, + failures: failureCount, + durationSeconds, + }); }; /** @@ -157,6 +176,14 @@ function GameStateInnerProvider({ children, gameConfig }) { */ const finalizeWithFailure = () => { setExecutionState(GAME_STATES.FALHA); + setFailureCount(prev => prev + 1); + + trackEvent('Activity Failure', { + activity: gameConfig.gameId, + phase: currentPhase, + blocks: currentBlockCount, + error: failureMessage, + }); }; /** @@ -198,6 +225,9 @@ function GameStateInnerProvider({ children, gameConfig }) { setGeneratedCode(""); setCurrentBlockCount(0); setCodeEditorContent(""); + setPhaseStartTime(Date.now()); + setAttemptCount(0); + setFailureCount(0); }; /** @@ -317,6 +347,8 @@ function GameStateInnerProvider({ children, gameConfig }) { setFailureMessage, isDebugMode, setIsDebugMode, + attemptCount, + failureCount, }} > {children} diff --git a/app/src/services/plausible.js b/app/src/services/plausible.js new file mode 100644 index 0000000..dd3837c --- /dev/null +++ b/app/src/services/plausible.js @@ -0,0 +1,41 @@ +const PLAUSIBLE_API = import.meta.env.VITE_PLAUSIBLE_API || 'http://localhost/api/event'; +const DOMAIN = import.meta.env.VITE_PLAUSIBLE_DOMAIN || 'myapp-dev'; + +const sendEvent = (payload) => { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + + fetch(PLAUSIBLE_API, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + signal: controller.signal, + }) + .catch((e) => console.error('Plausible error:', e)) + .finally(() => clearTimeout(timeout)); +}; + +export const trackPageView = (pathname) => { + if (!pathname) return; + + sendEvent({ + domain: DOMAIN, + url: `${window.location.origin}${pathname}`, + referrer: document.referrer, + screenWidth: window.innerWidth, + name: 'pageview', + }); +}; + +export const trackEvent = (eventName, properties = {}) => { + if (!eventName) return; + + sendEvent({ + domain: DOMAIN, + url: window.location.href, + referrer: document.referrer, + screenWidth: window.innerWidth, + name: eventName, + props: properties, + }); +}; diff --git a/app/vite.config.js b/app/vite.config.js index 4a4676d..aa23009 100644 --- a/app/vite.config.js +++ b/app/vite.config.js @@ -74,6 +74,9 @@ export default defineConfig({ "@": path.resolve(__dirname, "./src"), }, }, + server: { + allowedHosts: ["localhost", "dev.local", "decoda.mtst.tec.br"], + }, plugins: [ react(), copyLetramentoAtividades(),