Hero e movimento - cena 3D em tempo real com os blocos da marca (kit feito no Blender, 3d/blocos.py): marca, tela, integrações e dados; segue a rolagem, tem vida em repouso, reage ao cursor e, parada no topo, passa sozinha pelos estados com legenda - três níveis de aparelho (full, lite, static); sem WebGL fica o pôster - título novo: "Sites e sistemas sob medida para o jeito que a sua empresa trabalha." - faixa de clientes com o ramo, linha do primeiro passo, hero alinhado às seções - rótulos digitados, entrada dos blocos, cortina de blocos entre páginas e capa compartilhada entre a lista de cases e o case Ofertas - atendimento no WhatsApp com IA e nota fiscal para a reforma tributária (páginas de serviço, cartões da home, formulário e aviso com data de validade) - página /ia atualizada, com fontes - peças 3D em loop nos cartões e topos de página (3d/pecas.py) Design - hover por papel (bloco nos botões, barra nos links), rótulos sem caixa alta - um destino por rótulo: "Falar sobre meu projeto" leva ao formulário, "Chamar no WhatsApp" abre o WhatsApp - logo montado em blocos, 404 redesenhado, grades sem célula vazia - ícones do PWA e manifesto novos Specs em specs/, contexto de design em DESIGN.md e PRODUCT.md.
298 lines
9.8 KiB
TypeScript
298 lines
9.8 KiB
TypeScript
"use client";
|
|
|
|
import Script from "next/script";
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import {
|
|
type AnalyticsConsent,
|
|
CONSENT_STORAGE_KEY,
|
|
COOKIE_BAR_ID,
|
|
GA_DISABLE_KEY,
|
|
GA_MEASUREMENT_ID,
|
|
GOOGLE_ADS_ID,
|
|
type Gtag,
|
|
OPEN_COOKIE_PREFERENCES_EVENT,
|
|
readConsent,
|
|
} from "@/lib/analytics";
|
|
import "./conversion.css";
|
|
|
|
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 barButtonClassName =
|
|
"cv-btn inline-flex min-h-10 min-w-11 items-center justify-center border px-[10px] py-sp-1 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ink)] motion-reduce:transition-none";
|
|
|
|
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 barRef = useRef<HTMLElement>(null);
|
|
const acceptRef = useRef<HTMLButtonElement>(null);
|
|
const openerRef = useRef<HTMLElement | null>(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]);
|
|
|
|
// O link "Preferências de cookies" do rodapé reabre a barra; quem pediu recebe o foco de volta ao fechar.
|
|
useEffect(() => {
|
|
const reopen = () => {
|
|
openerRef.current =
|
|
document.activeElement instanceof HTMLElement
|
|
? document.activeElement
|
|
: null;
|
|
setPreferencesOpen(true);
|
|
};
|
|
window.addEventListener(OPEN_COOKIE_PREFERENCES_EVENT, reopen);
|
|
return () =>
|
|
window.removeEventListener(OPEN_COOKIE_PREFERENCES_EVENT, reopen);
|
|
}, []);
|
|
|
|
const barOpen = consent === null || preferencesOpen;
|
|
|
|
// Enquanto a barra está na tela, publica a altura dela para o botão flutuante do WhatsApp subir.
|
|
useEffect(() => {
|
|
const bar = barRef.current;
|
|
if (!barOpen || !bar) return;
|
|
const root = document.documentElement;
|
|
const publish = () =>
|
|
root.style.setProperty("--cookie-bar-height", `${bar.offsetHeight}px`);
|
|
publish();
|
|
root.setAttribute("data-cookie-bar", "open");
|
|
const observer = new ResizeObserver(publish);
|
|
observer.observe(bar);
|
|
return () => {
|
|
observer.disconnect();
|
|
root.style.removeProperty("--cookie-bar-height");
|
|
root.removeAttribute("data-cookie-bar");
|
|
};
|
|
}, [barOpen]);
|
|
|
|
// Reaberta pelo rodapé, a barra está no começo do documento: leva o foco para ela.
|
|
useEffect(() => {
|
|
if (preferencesOpen) acceptRef.current?.focus();
|
|
}, [preferencesOpen]);
|
|
|
|
function closePreferences() {
|
|
setPreferencesOpen(false);
|
|
openerRef.current?.focus();
|
|
openerRef.current = null;
|
|
}
|
|
|
|
function acceptAnalytics() {
|
|
if (consent === "granted") {
|
|
closePreferences();
|
|
return;
|
|
}
|
|
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() {
|
|
const revoking = consent === "granted";
|
|
if (!persistConsent("denied")) {
|
|
setConsentError(
|
|
revoking
|
|
? "Não foi possível salvar a revogação. Tente novamente para concluir."
|
|
: "Não foi possível salvar sua recusa. Tente novamente para concluir.",
|
|
);
|
|
return;
|
|
}
|
|
setConsentError(null);
|
|
if (revoking) {
|
|
window[GA_DISABLE_KEY] = true;
|
|
window.gtag?.("consent", "update", deniedConsent);
|
|
expireAnalyticsCookies();
|
|
}
|
|
setConsent("denied");
|
|
closePreferences();
|
|
}
|
|
|
|
if (consent === undefined) return null;
|
|
|
|
return (
|
|
<>
|
|
{consent === "granted" && process.env.NODE_ENV === "production" && (
|
|
<Script
|
|
id="google-analytics-script"
|
|
data-testid="google-analytics-script"
|
|
src={GA_SCRIPT_SRC}
|
|
strategy="afterInteractive"
|
|
/>
|
|
)}
|
|
{barOpen && (
|
|
<section
|
|
ref={barRef}
|
|
id={COOKIE_BAR_ID}
|
|
role="dialog"
|
|
aria-modal="false"
|
|
aria-label="Cookies e privacidade"
|
|
aria-describedby="analytics-consent-description"
|
|
onKeyDown={(event) => {
|
|
if (event.key === "Escape" && preferencesOpen) closePreferences();
|
|
}}
|
|
className="ds-enter fixed inset-x-0 bottom-0 z-[9980] border-t border-white/20 bg-[var(--ink)] px-sp-4 pt-sp-2 pb-[max(var(--sp-2),env(safe-area-inset-bottom))] text-white shadow-[0_-8px_24px_rgba(0,0,0,0.35)] [--enter-y:16px] sm:px-sp-6"
|
|
>
|
|
<div className="mx-auto flex max-w-[1100px] items-center gap-sp-3 sm:gap-sp-6">
|
|
{/* Até 72 px de altura em 390 px: três linhas de 13 px ao lado dos botões (15 px de 640 px em diante). */}
|
|
<p
|
|
id="analytics-consent-description"
|
|
className="min-w-0 flex-1 font-display text-[13px] leading-[1.3] text-white/85 sm:text-[15px]"
|
|
>
|
|
Métricas de navegação do Google, só se você permitir.{" "}
|
|
{/* o preenchimento vertical dá ao link 24 px de alvo de toque sem engordar a linha */}
|
|
<a
|
|
className="whitespace-nowrap py-1 underline underline-offset-2 hover:text-white hover:decoration-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
|
|
href="/privacidade"
|
|
>
|
|
Política de privacidade
|
|
</a>
|
|
{preferencesOpen && consent && (
|
|
<span className="sr-only">
|
|
{" "}
|
|
Escolha atual:{" "}
|
|
{consent === "granted" ? "permitido" : "recusado"}.
|
|
</span>
|
|
)}
|
|
</p>
|
|
<div className="flex shrink-0 gap-sp-2">
|
|
<button
|
|
type="button"
|
|
ref={acceptRef}
|
|
className={`${barButtonClassName} cv-fill border-[#f4f2ed] bg-[#f4f2ed] text-[var(--ink)] [--block:var(--slash)]`}
|
|
onClick={acceptAnalytics}
|
|
>
|
|
Aceitar
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={`${barButtonClassName} cv-outline-dark border-white/45 bg-transparent text-white`}
|
|
onClick={denyAnalytics}
|
|
>
|
|
Recusar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{consentError && (
|
|
<p
|
|
role="alert"
|
|
aria-label="Falha ao salvar o consentimento"
|
|
className="mx-auto mt-sp-1 max-w-[1100px] font-display text-[15px] leading-5 text-[#ff9d94]"
|
|
>
|
|
{consentError}
|
|
</p>
|
|
)}
|
|
</section>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* A barra de cookies e o consentimento aparecem em qualquer ambiente: no desenvolvimento a página fica igual à da
|
|
* produção (ordem do Tab, altura da barra, rodapé). Só o carregamento do gtag (o <Script> acima) fica preso à produção.
|
|
*/
|
|
export default function GoogleAnalytics() {
|
|
return <GoogleAnalyticsConsent />;
|
|
}
|