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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ This project uses modern build tools and development dependencies. For end users

## 📈 Analytics & Privacy

- Google Analytics (GA4) and Google Tag Manager now load from the deferred [`analytics.js`](public/analytics.js) file instead of inline snippets.
- The script waits for `requestIdleCallback` or the first user interaction (click, key press, pointer/touch) before injecting GA/GTM assets, freeing the critical rendering path while keeping telemetry intact.
- Google Analytics (GA4) and Google Tag Manager are now opt-in and only load after the visitor accepts telemetry via the in-app "Ativar métricas" control. The preference is stored in `localStorage`.
- The [`analytics.js`](public/analytics.js) bundle is injected lazily after consent and started programmatically with `window.appAnalytics.init()`, keeping it out of the critical rendering path by default.
- The GTM `<noscript>` iframe remains in the `<body>` to preserve baseline tracking for users without JavaScript.
- If your deployment requires explicit consent, set `window.APP_ANALYTICS_AUTO_START = false` in a script that runs before `analytics.js` and call `window.appAnalytics.init()` once consent is granted (or `window.appAnalytics.enableAutoStart()` to re-enable the deferred behaviour).
- To explicitly re-enable automatic scheduling elsewhere, set `window.APP_ANALYTICS_AUTO_START = true` before loading `analytics.js` or call `window.appAnalytics.enableAutoStart()` after the bundle is available.
- After deploying, validate that GA/GTM receive events (e.g., GTM preview mode or GA real-time dashboard) and rerun the PageSpeed Insights test to compare results with the previous baseline.

## 💾 Local Storage
Expand Down
6 changes: 3 additions & 3 deletions README.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ Este projeto utiliza ferramentas modernas de build e dependências de desenvolvi

## 📈 Métricas e privacidade

- O Google Analytics (GA4) e o Google Tag Manager agora são carregados pelo arquivo [`analytics.js`](public/analytics.js) com `defer`, substituindo os snippets inline.
- O script aguarda `requestIdleCallback` ou a primeira interação do usuário (clique, tecla, pointer/touch) antes de injetar os assets do GA/GTM, liberando o caminho crítico de renderização sem perder telemetria.
- O Google Analytics (GA4) e o Google Tag Manager agora são opt-in e só carregam após o visitante aceitar a coleta de métricas pelo controle "Ativar métricas" na interface. A preferência fica salva no `localStorage`.
- O bundle [`analytics.js`](public/analytics.js) é injetado de forma preguiçosa após o consentimento e iniciado programaticamente via `window.appAnalytics.init()`, mantendo-o fora do caminho crítico de renderização por padrão.
- O `<noscript>` do GTM permanece no `<body>` para manter o rastreamento básico quando o JavaScript estiver desabilitado.
- Se o seu ambiente exigir consentimento explícito, defina `window.APP_ANALYTICS_AUTO_START = false` em um script executado antes do `analytics.js` e chame `window.appAnalytics.init()` quando o consentimento for concedido (ou `window.appAnalytics.enableAutoStart()` para restaurar o agendamento adiado).
- Para reabilitar o agendamento automático em outro cenário, defina `window.APP_ANALYTICS_AUTO_START = true` antes de carregar o `analytics.js` ou chame `window.appAnalytics.enableAutoStart()` após o bundle estar disponível.
- Após o deploy, valide que os eventos continuam chegando ao GA/GTM (ex.: modo preview do GTM ou painel em tempo real do GA) e repita o teste no PageSpeed Insights para comparar com a linha de base anterior.

## 💾 Armazenamento Local
Expand Down
122 changes: 36 additions & 86 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,92 +65,6 @@
}
</script>

<link rel="stylesheet" href="/src/styles/main.css">
<style>
:root {
--color-primary: #2c3e50;
--color-border: rgba(255, 255, 255, 0.3);
--gradient-bg: linear-gradient(135deg, #2ecc71 0%, #3498db 100%);
}

body {
background: var(--gradient-bg);
}

.btn-shine {
position: relative;
overflow: hidden;
}

.btn-shine::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, var(--color-border), transparent);
transform: skewX(-25deg);
transition: left 0.5s ease;
}

.btn-shine:hover::before {
left: 100%;
}

.btn-shine>* {
position: relative;
z-index: 1;
}

input[type="range"] {
-webkit-appearance: none;
appearance: none;
background: var(--color-primary);
cursor: pointer;
width: 100%;
height: 8px;
}

input[type="range"]::-webkit-slider-track,
input[type="range"]::-moz-range-track {
background: var(--color-primary);
height: 8px;
border-radius: 4px;
border: 1px solid var(--color-border);
}

input[type="range"]::-webkit-slider-thumb,
input[type="range"]::-moz-range-thumb {
-webkit-appearance: none;
appearance: none;
background: var(--color-primary);
height: 20px;
width: 20px;
border-radius: 50%;
border: 2px solid var(--color-border);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: all 0.2s ease;
cursor: pointer;
}

input[type="range"]::-webkit-slider-thumb:hover,
input[type="range"]::-moz-range-thumb:hover {
transform: scale(1.1);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}

textarea::placeholder {
color: var(--color-primary);
opacity: 0.6;
}

button:disabled,
input:disabled,
textarea:disabled {
cursor: not-allowed !important;
}
</style>
</head>

<body
Expand Down Expand Up @@ -264,6 +178,28 @@ <h1 class="text-4xl font-bold tracking-tight drop-shadow-lg text-primary">Ouça

<div id="toast" class="fixed inset-x-0 top-4 flex justify-center pointer-events-none z-50"></div>

<section class="fixed inset-x-0 bottom-24 sm:bottom-28 px-4 sm:px-6 z-40 flex justify-center hidden" data-analytics-consent>
<div
class="w-full sm:max-w-2xl bg-white/95 text-primary border border-white/60 rounded-xl shadow-2xl px-4 py-4 sm:px-6 sm:py-5 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="space-y-1 text-sm sm:text-base">
<p class="font-semibold">Controle de métricas</p>
<p class="text-primary/80">Ative o Google Tag Manager apenas se concordar em compartilhar dados anônimos de uso.</p>
</div>
<div class="flex items-center gap-3 justify-end">
<button type="button"
class="px-4 py-2 rounded-lg bg-white text-primary border border-primary/20 hover:bg-primary hover:text-white transition-all"
data-analytics-deny>
Agora não
</button>
<button type="button"
class="px-4 py-2 rounded-lg bg-gradient-to-r from-gradient-start to-gradient-end text-white font-semibold shadow-lg hover:opacity-95 transition-opacity"
data-analytics-allow>
Ativar métricas
</button>
</div>
</div>
</section>

<footer class="fixed bottom-0 left-0 right-0 z-40 flex justify-center p-4">
<div class="bg-white/10 backdrop-blur-md border border-white/20 rounded-xl shadow-2xl w-full px-4 py-4 sm:px-6 sm:max-w-2xl">
<div class="flex flex-col sm:flex-row items-center justify-between gap-3 text-center sm:text-left">
Expand All @@ -286,6 +222,20 @@ <h1 class="text-4xl font-bold tracking-tight drop-shadow-lg text-primary">Ouça
</div>
</footer>

<div class="fixed bottom-2 right-3 sm:right-6 z-50 flex justify-end">
<button type="button"
class="text-xs sm:text-sm inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-white/80 backdrop-blur border border-white/60 shadow-lg text-primary font-semibold hover:bg-white"
data-analytics-manage>
Ativar métricas
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="10"></circle>
<line x1="2" x2="22" y1="12" y2="12"></line>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10"></path>
</svg>
</button>
</div>

<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-N5CK6BH8" height="0" width="0"
style="display:none;visibility:hidden"></iframe></noscript>
Expand Down
28 changes: 4 additions & 24 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { bootstrap } from './app/textReaderApp.js';
import './styles/main.css';
import { setupAnalyticsConsent } from './utils/analyticsConsent.js';

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
Expand All @@ -8,27 +10,5 @@ if (document.readyState === 'loading') {
bootstrap();
}

// Lazy load analytics.js após carregamento completo da página
function loadAnalytics(): void {
const script = document.createElement('script');
script.src = '/analytics.js';
script.async = true;
document.head.appendChild(script);
}

if (document.readyState === 'complete') {
// Usar requestIdleCallback se disponível, senão usar setTimeout
if ('requestIdleCallback' in window) {
window.requestIdleCallback(loadAnalytics, { timeout: 2000 });
} else {
setTimeout(loadAnalytics, 100);
}
} else {
window.addEventListener('load', () => {
if ('requestIdleCallback' in window) {
window.requestIdleCallback(loadAnalytics, { timeout: 2000 });
} else {
setTimeout(loadAnalytics, 100);
}
});
}
// Inicializa o fluxo de consentimento de analytics (opt-in)
setupAnalyticsConsent(window, document);
86 changes: 86 additions & 0 deletions src/styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,89 @@
}
}

@layer base {
:root {
--color-primary: #2c3e50;
--color-border: rgba(255, 255, 255, 0.3);
--gradient-bg: linear-gradient(135deg, #2ecc71 0%, #3498db 100%);
}

body {
background: var(--gradient-bg);
}

.btn-shine {
position: relative;
overflow: hidden;
}

.btn-shine::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, var(--color-border), transparent);
transform: skewX(-25deg);
transition: left 0.5s ease;
}

.btn-shine:hover::before {
left: 100%;
}

.btn-shine > * {
position: relative;
z-index: 1;
}

input[type='range'] {
-webkit-appearance: none;
appearance: none;
background: var(--color-primary);
cursor: pointer;
width: 100%;
height: 8px;
}

input[type='range']::-webkit-slider-track,
input[type='range']::-moz-range-track {
background: var(--color-primary);
height: 8px;
border-radius: 4px;
border: 1px solid var(--color-border);
}

input[type='range']::-webkit-slider-thumb,
input[type='range']::-moz-range-thumb {
-webkit-appearance: none;
appearance: none;
background: var(--color-primary);
height: 20px;
width: 20px;
border-radius: 50%;
border: 2px solid var(--color-border);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: all 0.2s ease;
cursor: pointer;
}

input[type='range']::-webkit-slider-thumb:hover,
input[type='range']::-moz-range-thumb:hover {
transform: scale(1.1);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}

textarea::placeholder {
color: var(--color-primary);
opacity: 0.6;
}

button:disabled,
input:disabled,
textarea:disabled {
cursor: not-allowed !important;
}
}

Loading