324 lines
9.9 KiB
TypeScript
324 lines
9.9 KiB
TypeScript
"use client";
|
|
|
|
import Script from "next/script";
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import {
|
|
type AnalyticsConsent,
|
|
CONSENT_STORAGE_KEY,
|
|
GA_DISABLE_KEY,
|
|
GA_MEASUREMENT_ID,
|
|
type Gtag,
|
|
readConsent,
|
|
} from "@/lib/analytics";
|
|
|
|
const GOOGLE_ADS_ID = "AW-18432694647";
|
|
const GA_SCRIPT_SRC = `https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`;
|
|
const SITE_DOMAIN = "evandro.dev.br";
|
|
|
|
const deniedConsent = {
|
|
analytics_storage: "denied",
|
|
ad_storage: "denied",
|
|
ad_user_data: "denied",
|
|
ad_personalization: "denied",
|
|
} as const;
|
|
|
|
function persistConsent(value: AnalyticsConsent): boolean {
|
|
try {
|
|
localStorage.setItem(CONSENT_STORAGE_KEY, value);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function getGtag(): Gtag {
|
|
window.dataLayer ??= [];
|
|
const dataLayer = window.dataLayer;
|
|
if (!window.gtag) {
|
|
window.gtag = function gtag() {
|
|
// biome-ignore lint/complexity/noArguments: The official gtag bootstrap requires an IArguments entry, not an Array.
|
|
dataLayer.push(arguments);
|
|
};
|
|
}
|
|
return window.gtag;
|
|
}
|
|
|
|
function removeAnalyticsLoader() {
|
|
document.getElementById("google-analytics-script")?.remove();
|
|
}
|
|
|
|
function writeCookie(value: string) {
|
|
// biome-ignore lint/suspicious/noDocumentCookie: Cookie Store is not universal and domain-scoped GA cookies must also be expired.
|
|
document.cookie = value;
|
|
}
|
|
function expireAnalyticsCookies() {
|
|
const cookieNames = new Set(
|
|
document.cookie
|
|
.split(";")
|
|
.map((cookie) => cookie.split("=")[0]?.trim())
|
|
.filter(
|
|
(name): name is string =>
|
|
name === "_ga" || Boolean(name?.startsWith("_ga_")),
|
|
),
|
|
);
|
|
const { hostname } = window.location;
|
|
const domains =
|
|
hostname === SITE_DOMAIN || hostname.endsWith(`.${SITE_DOMAIN}`)
|
|
? new Set([hostname, SITE_DOMAIN])
|
|
: new Set<string>();
|
|
|
|
for (const name of cookieNames) {
|
|
const expiration = `${name}=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; SameSite=Lax`;
|
|
writeCookie(expiration);
|
|
for (const domain of domains) {
|
|
writeCookie(`${expiration}; Domain=${domain}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const panelClassName =
|
|
"fixed bottom-sp-4 left-sp-4 right-sp-4 z-[9980] ml-auto max-w-[560px] border border-[var(--slash)] bg-[var(--ink)] p-sp-5 text-white shadow-[8px_8px_0_var(--slash)] sm:left-auto";
|
|
const buttonClassName =
|
|
"inline-flex min-h-11 min-w-11 items-center justify-center border px-sp-4 py-sp-2 font-mono text-[11px] font-bold uppercase tracking-[0.08em] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--slash)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ink)]";
|
|
|
|
function GoogleAnalyticsConsent() {
|
|
const [consent, setConsent] = useState<AnalyticsConsent | null>();
|
|
const [preferencesOpen, setPreferencesOpen] = useState(false);
|
|
const [consentError, setConsentError] = useState<string | null>(null);
|
|
const configuredRef = useRef(false);
|
|
const preferencesButtonRef = useRef<HTMLButtonElement>(null);
|
|
|
|
const enableAnalytics = useCallback(() => {
|
|
const gtag = getGtag();
|
|
window[GA_DISABLE_KEY] = false;
|
|
if (!configuredRef.current) {
|
|
gtag("consent", "default", deniedConsent);
|
|
gtag("set", "ads_data_redaction", true);
|
|
gtag("js", new Date());
|
|
configuredRef.current = true;
|
|
}
|
|
gtag("consent", "update", {
|
|
...deniedConsent,
|
|
analytics_storage: "granted",
|
|
});
|
|
gtag("config", GA_MEASUREMENT_ID, {
|
|
allow_ad_personalization_signals: false,
|
|
anonymize_ip: true,
|
|
});
|
|
// One loader serves both destinations; GT IDs are aliases, not extra installs.
|
|
gtag("config", GOOGLE_ADS_ID, {
|
|
allow_ad_personalization_signals: false,
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const storedConsent = readConsent();
|
|
if (storedConsent === "granted") enableAnalytics();
|
|
setConsent(storedConsent);
|
|
}, [enableAnalytics]);
|
|
|
|
useEffect(() => {
|
|
if (consent !== "granted" && consent !== undefined) {
|
|
removeAnalyticsLoader();
|
|
}
|
|
}, [consent]);
|
|
|
|
function closePreferences() {
|
|
setPreferencesOpen(false);
|
|
preferencesButtonRef.current?.focus();
|
|
}
|
|
|
|
function acceptAnalytics() {
|
|
if (!persistConsent("granted")) {
|
|
setConsentError(
|
|
"Não foi possível salvar sua aceitação. Tente novamente para concluir.",
|
|
);
|
|
return;
|
|
}
|
|
setConsentError(null);
|
|
enableAnalytics();
|
|
setConsent("granted");
|
|
closePreferences();
|
|
}
|
|
|
|
function denyAnalytics() {
|
|
if (!persistConsent("denied")) {
|
|
setConsentError(
|
|
"Não foi possível salvar sua recusa. Tente novamente para concluir.",
|
|
);
|
|
return;
|
|
}
|
|
setConsentError(null);
|
|
setConsent("denied");
|
|
closePreferences();
|
|
}
|
|
|
|
function revokeAnalytics() {
|
|
if (!persistConsent("denied")) {
|
|
setConsentError(
|
|
"Não foi possível salvar a revogação. Tente novamente para concluir.",
|
|
);
|
|
return;
|
|
}
|
|
setConsentError(null);
|
|
window[GA_DISABLE_KEY] = true;
|
|
window.gtag?.("consent", "update", deniedConsent);
|
|
expireAnalyticsCookies();
|
|
setConsent("denied");
|
|
closePreferences();
|
|
}
|
|
|
|
if (consent === undefined) return null;
|
|
|
|
if (consent === null) {
|
|
return (
|
|
<section
|
|
role="dialog"
|
|
aria-modal="false"
|
|
aria-labelledby="analytics-consent-title"
|
|
aria-describedby="analytics-consent-description"
|
|
className={panelClassName}
|
|
>
|
|
<p
|
|
id="analytics-consent-title"
|
|
className="font-mono text-[10px] font-bold uppercase tracking-[0.16em] text-[var(--slash)]"
|
|
>
|
|
{"Privacidade // Analytics"}
|
|
</p>
|
|
<p
|
|
id="analytics-consent-description"
|
|
className="mt-sp-2 max-w-[48ch] text-[13px] leading-6 text-white/85"
|
|
>
|
|
Você decide se permite métricas de navegação com a tag compartilhada
|
|
do Google Analytics e Google Ads. Cookies de publicidade e
|
|
personalização continuam desativados.
|
|
</p>
|
|
{consentError && (
|
|
<p
|
|
role="alert"
|
|
aria-label="Falha ao salvar o consentimento"
|
|
className="mt-sp-3 text-[13px] leading-6 text-[var(--slash)]"
|
|
>
|
|
{consentError}
|
|
</p>
|
|
)}
|
|
<div className="mt-sp-4 flex flex-wrap gap-sp-2">
|
|
<button
|
|
type="button"
|
|
className={`${buttonClassName} border-[var(--slash)] bg-[var(--slash)] text-[var(--ink)]`}
|
|
onClick={acceptAnalytics}
|
|
>
|
|
Aceitar analytics
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={`${buttonClassName} border-white/40 bg-transparent text-white`}
|
|
onClick={denyAnalytics}
|
|
>
|
|
Recusar analytics
|
|
</button>
|
|
</div>
|
|
<a
|
|
className="ds-link ds-link-accent mt-sp-3 inline-block"
|
|
href="/privacidade"
|
|
>
|
|
Ler política de privacidade
|
|
</a>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{consent === "granted" && (
|
|
<Script
|
|
id="google-analytics-script"
|
|
data-testid="google-analytics-script"
|
|
src={GA_SCRIPT_SRC}
|
|
strategy="afterInteractive"
|
|
/>
|
|
)}
|
|
<button
|
|
type="button"
|
|
ref={preferencesButtonRef}
|
|
aria-expanded={preferencesOpen}
|
|
aria-controls="analytics-preferences"
|
|
className={`${buttonClassName} fixed bottom-sp-4 right-sp-4 z-[9980] border-white/25 bg-[var(--ink)] text-white shadow-[4px_4px_0_var(--slash)]`}
|
|
onClick={() => setPreferencesOpen((open) => !open)}
|
|
>
|
|
Preferências de analytics
|
|
</button>
|
|
{preferencesOpen && (
|
|
<section
|
|
id="analytics-preferences"
|
|
role="dialog"
|
|
aria-modal="false"
|
|
aria-labelledby="analytics-preferences-title"
|
|
aria-describedby="analytics-preferences-description"
|
|
className={panelClassName}
|
|
>
|
|
<p
|
|
id="analytics-preferences-title"
|
|
className="font-mono text-[10px] font-bold uppercase tracking-[0.16em] text-[var(--slash)]"
|
|
>
|
|
{"Preferências // Analytics"}
|
|
</p>
|
|
<p
|
|
id="analytics-preferences-description"
|
|
className="mt-sp-2 text-[13px] leading-6 text-white/85"
|
|
>
|
|
Analytics está {consent === "granted" ? "ativo" : "desativado"}.
|
|
Cookies de publicidade e personalização permanecem negados.
|
|
</p>
|
|
{consentError && (
|
|
<p
|
|
role="alert"
|
|
aria-label="Falha ao salvar o consentimento"
|
|
className="mt-sp-3 text-[13px] leading-6 text-[var(--slash)]"
|
|
>
|
|
{consentError}
|
|
</p>
|
|
)}
|
|
<div className="mt-sp-4 flex flex-wrap gap-sp-2">
|
|
{consent === "granted" ? (
|
|
<button
|
|
type="button"
|
|
className={`${buttonClassName} border-[var(--slash)] bg-[var(--slash)] text-[var(--ink)]`}
|
|
onClick={revokeAnalytics}
|
|
>
|
|
Revogar analytics
|
|
</button>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
className={`${buttonClassName} border-[var(--slash)] bg-[var(--slash)] text-[var(--ink)]`}
|
|
onClick={acceptAnalytics}
|
|
>
|
|
Aceitar analytics
|
|
</button>
|
|
)}
|
|
<button
|
|
type="button"
|
|
className={`${buttonClassName} border-white/40 bg-transparent text-white`}
|
|
onClick={closePreferences}
|
|
>
|
|
Fechar preferências
|
|
</button>
|
|
</div>
|
|
<a
|
|
className="ds-link ds-link-accent mt-sp-3 inline-block"
|
|
href="/privacidade"
|
|
>
|
|
Ler política de privacidade
|
|
</a>
|
|
</section>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default function GoogleAnalytics() {
|
|
if (process.env.NODE_ENV !== "production") return null;
|
|
return <GoogleAnalyticsConsent />;
|
|
}
|