Files
portifolio-evandrodevbr/app/components/GoogleAnalytics.tsx
T

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 />;
}