@whatworks/analytics 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ export default function Analytics(): import("react").JSX.Element | null;
@@ -0,0 +1,32 @@
1
+ // This is just an example file of how you would use this package in your own project
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { CookieBannerProvider } from './CookieBannerProvider.js';
4
+ import FacebookPixel from './FacebookPixel.js';
5
+ import GoogleAnalytics from './GoogleAnalytics.js';
6
+ import GoogleTagManager from './GoogleTagManager.js';
7
+ import MicrosoftClarity from './MicrosoftClarity.js';
8
+ export default function Analytics() {
9
+ if (process.env.NODE_ENV !== 'production') {
10
+ return null;
11
+ }
12
+ return /*#__PURE__*/ _jsxs(CookieBannerProvider, {
13
+ consentApiPath: "/api/consent",
14
+ consentStrategy: "load-scripts-then-revoke-consent-after-geolocation-check",
15
+ children: [
16
+ process.env.NEXT_PUBLIC_FACEBOOK_PIXEL_ID && /*#__PURE__*/ _jsx(FacebookPixel, {
17
+ pixelId: process.env.NEXT_PUBLIC_FACEBOOK_PIXEL_ID
18
+ }),
19
+ process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_ID && !process.env.NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID && /*#__PURE__*/ _jsx(GoogleAnalytics, {
20
+ gaId: process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_ID
21
+ }),
22
+ process.env.NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID && /*#__PURE__*/ _jsx(GoogleTagManager, {
23
+ gtmId: process.env.NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID
24
+ }),
25
+ process.env.NEXT_PUBLIC_MS_CLARITY_ID && /*#__PURE__*/ _jsx(MicrosoftClarity, {
26
+ clarityId: process.env.NEXT_PUBLIC_MS_CLARITY_ID
27
+ })
28
+ ]
29
+ });
30
+ }
31
+
32
+ //# sourceMappingURL=Analytics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/Analytics.tsx"],"sourcesContent":["// This is just an example file of how you would use this package in your own project\n\nimport { CookieBannerProvider } from './CookieBannerProvider.js'\nimport FacebookPixel from './FacebookPixel.js'\nimport GoogleAnalytics from './GoogleAnalytics.js'\nimport GoogleTagManager from './GoogleTagManager.js'\nimport MicrosoftClarity from './MicrosoftClarity.js'\n\nexport default function Analytics() {\n if (process.env.NODE_ENV !== 'production') {\n return null\n }\n\n return (\n <CookieBannerProvider\n consentApiPath=\"/api/consent\"\n consentStrategy=\"load-scripts-then-revoke-consent-after-geolocation-check\"\n >\n {process.env.NEXT_PUBLIC_FACEBOOK_PIXEL_ID && (\n <FacebookPixel pixelId={process.env.NEXT_PUBLIC_FACEBOOK_PIXEL_ID} />\n )}\n {/* Avoid double-tracking: prefer GTM when both IDs are present */}\n {process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_ID &&\n !process.env.NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID && (\n <GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_ID} />\n )}\n {process.env.NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID && (\n <GoogleTagManager gtmId={process.env.NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID} />\n )}\n {process.env.NEXT_PUBLIC_MS_CLARITY_ID && (\n <MicrosoftClarity clarityId={process.env.NEXT_PUBLIC_MS_CLARITY_ID} />\n )}\n </CookieBannerProvider>\n )\n}\n"],"names":["CookieBannerProvider","FacebookPixel","GoogleAnalytics","GoogleTagManager","MicrosoftClarity","Analytics","process","env","NODE_ENV","consentApiPath","consentStrategy","NEXT_PUBLIC_FACEBOOK_PIXEL_ID","pixelId","NEXT_PUBLIC_GOOGLE_ANALYTICS_ID","NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID","gaId","gtmId","NEXT_PUBLIC_MS_CLARITY_ID","clarityId"],"mappings":"AAAA,qFAAqF;;AAErF,SAASA,oBAAoB,QAAQ,4BAA2B;AAChE,OAAOC,mBAAmB,qBAAoB;AAC9C,OAAOC,qBAAqB,uBAAsB;AAClD,OAAOC,sBAAsB,wBAAuB;AACpD,OAAOC,sBAAsB,wBAAuB;AAEpD,eAAe,SAASC;IACtB,IAAIC,QAAQC,GAAG,CAACC,QAAQ,KAAK,cAAc;QACzC,OAAO;IACT;IAEA,qBACE,MAACR;QACCS,gBAAe;QACfC,iBAAgB;;YAEfJ,QAAQC,GAAG,CAACI,6BAA6B,kBACxC,KAACV;gBAAcW,SAASN,QAAQC,GAAG,CAACI,6BAA6B;;YAGlEL,QAAQC,GAAG,CAACM,+BAA+B,IAC1C,CAACP,QAAQC,GAAG,CAACO,iCAAiC,kBAC5C,KAACZ;gBAAgBa,MAAMT,QAAQC,GAAG,CAACM,+BAA+B;;YAErEP,QAAQC,GAAG,CAACO,iCAAiC,kBAC5C,KAACX;gBAAiBa,OAAOV,QAAQC,GAAG,CAACO,iCAAiC;;YAEvER,QAAQC,GAAG,CAACU,yBAAyB,kBACpC,KAACb;gBAAiBc,WAAWZ,QAAQC,GAAG,CAACU,yBAAyB;;;;AAI1E"}
@@ -0,0 +1,8 @@
1
+ import type { ReactNode } from 'react';
2
+ import './styles.css';
3
+ export default function CookieBanner({ acceptText, description, rejectText, title, }: {
4
+ acceptText?: string;
5
+ description?: ReactNode;
6
+ rejectText?: string;
7
+ title?: string;
8
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,75 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import './styles.css';
4
+ import Link from 'next/link';
5
+ import { CookieBannerPortal } from './CookieBannerPortal.js';
6
+ import { useCookieBanner } from './CookieBannerProvider.js';
7
+ export default function CookieBanner({ acceptText = 'Accept', description = /*#__PURE__*/ _jsxs(_Fragment, {
8
+ children: [
9
+ 'We use cookies to improve your experience. By clicking "Accept", you agree to the use of cookies. To learn more, please view our',
10
+ ' ',
11
+ /*#__PURE__*/ _jsx(Link, {
12
+ className: "underline",
13
+ href: "/privacy-policy",
14
+ children: "Privacy Policy"
15
+ }),
16
+ "."
17
+ ]
18
+ }), rejectText = 'Reject', title = 'Cookies' }) {
19
+ const { accept, reject, shouldShowBanner } = useCookieBanner();
20
+ if (!shouldShowBanner) {
21
+ return null;
22
+ }
23
+ return /*#__PURE__*/ _jsx(CookieBannerPortal, {
24
+ children: /*#__PURE__*/ _jsx("div", {
25
+ className: "ww",
26
+ children: /*#__PURE__*/ _jsx("div", {
27
+ className: "fixed bottom-4 inset-x-4 z-50 border border-gray-200 rounded-lg bg-white py-8 shadow",
28
+ children: /*#__PURE__*/ _jsx("div", {
29
+ className: "container mx-auto px-4 sm:px-6 lg:px-8",
30
+ children: /*#__PURE__*/ _jsxs("div", {
31
+ className: "flex flex-col justify-between gap-4 md:flex-row md:items-center",
32
+ children: [
33
+ /*#__PURE__*/ _jsxs("div", {
34
+ className: "space-y-2",
35
+ children: [
36
+ /*#__PURE__*/ _jsx("h3", {
37
+ className: "text-base font-medium",
38
+ children: title
39
+ }),
40
+ /*#__PURE__*/ _jsx("p", {
41
+ className: "text-sm text-gray-600",
42
+ children: description
43
+ })
44
+ ]
45
+ }),
46
+ /*#__PURE__*/ _jsxs("div", {
47
+ className: "flex shrink-0 items-center gap-4",
48
+ children: [
49
+ /*#__PURE__*/ _jsx("button", {
50
+ className: "py-3 px-6 bg-black text-white rounded-md hover:bg-stone-800",
51
+ onClick: ()=>{
52
+ reject();
53
+ },
54
+ type: "button",
55
+ children: rejectText
56
+ }),
57
+ /*#__PURE__*/ _jsx("button", {
58
+ className: "py-3 px-6 bg-black text-white rounded-md hover:bg-stone-800",
59
+ onClick: ()=>{
60
+ accept();
61
+ },
62
+ type: "button",
63
+ children: acceptText
64
+ })
65
+ ]
66
+ })
67
+ ]
68
+ })
69
+ })
70
+ })
71
+ })
72
+ });
73
+ }
74
+
75
+ //# sourceMappingURL=CookieBanner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/CookieBanner.tsx"],"sourcesContent":["'use client'\nimport type { ReactNode } from 'react'\n\nimport './styles.css'\n\nimport Link from 'next/link'\n\nimport { CookieBannerPortal } from './CookieBannerPortal.js'\nimport { useCookieBanner } from './CookieBannerProvider.js'\n\nexport default function CookieBanner({\n acceptText = 'Accept',\n description = (\n <>\n We use cookies to improve your experience. By clicking \"Accept\", you agree to the use of\n cookies. To learn more, please view our{' '}\n <Link className=\"underline\" href=\"/privacy-policy\">\n Privacy Policy\n </Link>\n .\n </>\n ),\n rejectText = 'Reject',\n title = 'Cookies',\n}: {\n acceptText?: string\n description?: ReactNode\n rejectText?: string\n title?: string\n}) {\n const { accept, reject, shouldShowBanner } = useCookieBanner()\n\n if (!shouldShowBanner) {\n return null\n }\n\n return (\n <CookieBannerPortal>\n <div className=\"ww\">\n <div className=\"fixed bottom-4 inset-x-4 z-50 border border-gray-200 rounded-lg bg-white py-8 shadow\">\n <div className=\"container mx-auto px-4 sm:px-6 lg:px-8\">\n <div className=\"flex flex-col justify-between gap-4 md:flex-row md:items-center\">\n <div className=\"space-y-2\">\n <h3 className=\"text-base font-medium\">{title}</h3>\n <p className=\"text-sm text-gray-600\">{description}</p>\n </div>\n <div className=\"flex shrink-0 items-center gap-4\">\n <button\n className=\"py-3 px-6 bg-black text-white rounded-md hover:bg-stone-800\"\n onClick={() => {\n reject()\n }}\n type=\"button\"\n >\n {rejectText}\n </button>\n <button\n className=\"py-3 px-6 bg-black text-white rounded-md hover:bg-stone-800\"\n onClick={() => {\n accept()\n }}\n type=\"button\"\n >\n {acceptText}\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n </CookieBannerPortal>\n )\n}\n"],"names":["Link","CookieBannerPortal","useCookieBanner","CookieBanner","acceptText","description","className","href","rejectText","title","accept","reject","shouldShowBanner","div","h3","p","button","onClick","type"],"mappings":"AAAA;;AAGA,OAAO,eAAc;AAErB,OAAOA,UAAU,YAAW;AAE5B,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,eAAe,QAAQ,4BAA2B;AAE3D,eAAe,SAASC,aAAa,EACnCC,aAAa,QAAQ,EACrBC,4BACE;;QAAE;QAEwC;sBACxC,KAACL;YAAKM,WAAU;YAAYC,MAAK;sBAAkB;;QAE5C;;EAGV,EACDC,aAAa,QAAQ,EACrBC,QAAQ,SAAS,EAMlB;IACC,MAAM,EAAEC,MAAM,EAAEC,MAAM,EAAEC,gBAAgB,EAAE,GAAGV;IAE7C,IAAI,CAACU,kBAAkB;QACrB,OAAO;IACT;IAEA,qBACE,KAACX;kBACC,cAAA,KAACY;YAAIP,WAAU;sBACb,cAAA,KAACO;gBAAIP,WAAU;0BACb,cAAA,KAACO;oBAAIP,WAAU;8BACb,cAAA,MAACO;wBAAIP,WAAU;;0CACb,MAACO;gCAAIP,WAAU;;kDACb,KAACQ;wCAAGR,WAAU;kDAAyBG;;kDACvC,KAACM;wCAAET,WAAU;kDAAyBD;;;;0CAExC,MAACQ;gCAAIP,WAAU;;kDACb,KAACU;wCACCV,WAAU;wCACVW,SAAS;4CACPN;wCACF;wCACAO,MAAK;kDAEJV;;kDAEH,KAACQ;wCACCV,WAAU;wCACVW,SAAS;4CACPP;wCACF;wCACAQ,MAAK;kDAEJd;;;;;;;;;;AASnB"}
@@ -0,0 +1,5 @@
1
+ import type { ReactNode } from 'react';
2
+ export declare function CookieBannerPortal({ children, portalId, }: {
3
+ children: ReactNode;
4
+ portalId?: string;
5
+ }): import("react").ReactPortal | null;
@@ -0,0 +1,28 @@
1
+ 'use client';
2
+ import { useEffect, useState } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ const DEFAULT_PORTAL_ID = 'cookie-banner-root';
5
+ const getPortalRoot = (portalId)=>{
6
+ const existing = document.getElementById(portalId);
7
+ if (existing) {
8
+ return existing;
9
+ }
10
+ const root = document.createElement('div');
11
+ root.id = portalId;
12
+ document.body.appendChild(root);
13
+ return root;
14
+ };
15
+ export function CookieBannerPortal({ children, portalId = DEFAULT_PORTAL_ID }) {
16
+ const [portalRoot, setPortalRoot] = useState(null);
17
+ useEffect(()=>{
18
+ setPortalRoot(getPortalRoot(portalId));
19
+ }, [
20
+ portalId
21
+ ]);
22
+ if (!portalRoot) {
23
+ return null;
24
+ }
25
+ return /*#__PURE__*/ createPortal(children, portalRoot);
26
+ }
27
+
28
+ //# sourceMappingURL=CookieBannerPortal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/CookieBannerPortal.tsx"],"sourcesContent":["'use client'\n\nimport type { ReactNode } from 'react'\n\nimport { useEffect, useState } from 'react'\nimport { createPortal } from 'react-dom'\n\nconst DEFAULT_PORTAL_ID = 'cookie-banner-root'\n\nconst getPortalRoot = (portalId: string): HTMLElement => {\n const existing = document.getElementById(portalId)\n if (existing) {\n return existing\n }\n\n const root = document.createElement('div')\n root.id = portalId\n document.body.appendChild(root)\n return root\n}\n\nexport function CookieBannerPortal({\n children,\n portalId = DEFAULT_PORTAL_ID,\n}: {\n children: ReactNode\n portalId?: string\n}) {\n const [portalRoot, setPortalRoot] = useState<HTMLElement | null>(null)\n\n useEffect(() => {\n setPortalRoot(getPortalRoot(portalId))\n }, [portalId])\n\n if (!portalRoot) {\n return null\n }\n\n return createPortal(children, portalRoot)\n}\n"],"names":["useEffect","useState","createPortal","DEFAULT_PORTAL_ID","getPortalRoot","portalId","existing","document","getElementById","root","createElement","id","body","appendChild","CookieBannerPortal","children","portalRoot","setPortalRoot"],"mappings":"AAAA;AAIA,SAASA,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAC3C,SAASC,YAAY,QAAQ,YAAW;AAExC,MAAMC,oBAAoB;AAE1B,MAAMC,gBAAgB,CAACC;IACrB,MAAMC,WAAWC,SAASC,cAAc,CAACH;IACzC,IAAIC,UAAU;QACZ,OAAOA;IACT;IAEA,MAAMG,OAAOF,SAASG,aAAa,CAAC;IACpCD,KAAKE,EAAE,GAAGN;IACVE,SAASK,IAAI,CAACC,WAAW,CAACJ;IAC1B,OAAOA;AACT;AAEA,OAAO,SAASK,mBAAmB,EACjCC,QAAQ,EACRV,WAAWF,iBAAiB,EAI7B;IACC,MAAM,CAACa,YAAYC,cAAc,GAAGhB,SAA6B;IAEjED,UAAU;QACRiB,cAAcb,cAAcC;IAC9B,GAAG;QAACA;KAAS;IAEb,IAAI,CAACW,YAAY;QACf,OAAO;IACT;IAEA,qBAAOd,aAAaa,UAAUC;AAChC"}
@@ -0,0 +1,33 @@
1
+ import type { ReactNode } from 'react';
2
+ export type ConsentStrategy = 'load-scripts-revoke-consent-immediately' | 'load-scripts-then-revoke-consent-after-geolocation-check' | 'require-consent-before-loading-scripts';
3
+ type ConsentStatus = 'denied' | 'granted';
4
+ interface CookieBannerContextType {
5
+ accept: () => void;
6
+ consentStatus: ConsentStatus;
7
+ reject: () => void;
8
+ shouldLoadScripts: boolean;
9
+ shouldShowBanner: boolean;
10
+ }
11
+ export declare function useCookieBanner(): CookieBannerContextType;
12
+ interface CookieBannerProviderProps {
13
+ children: ReactNode;
14
+ consentApiPath: string;
15
+ /**
16
+ * `load-scripts-revoke-consent-immediately`
17
+ * - Render scripts immediately.
18
+ * - Default consent is denied until a user grants.
19
+ * - Banner shown only if geolocation requires consent.
20
+ *
21
+ * `load-scripts-then-revoke-consent-after-geolocation-check`
22
+ * - Render scripts immediately.
23
+ * - Default consent is granted until geolocation requires consent.
24
+ * - If consent is required, revoke and show banner.
25
+ *
26
+ * `require-consent-before-loading-scripts`
27
+ * - Do not render scripts until consent is granted when required.
28
+ * - Banner shown only if geolocation requires consent.
29
+ */
30
+ consentStrategy: ConsentStrategy;
31
+ }
32
+ export declare function CookieBannerProvider({ children, consentApiPath, consentStrategy, }: CookieBannerProviderProps): import("react").JSX.Element;
33
+ export {};
@@ -0,0 +1,107 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext, useEffect, useMemo, useState } from 'react';
4
+ const CookieBannerContext = /*#__PURE__*/ createContext(undefined);
5
+ export function useCookieBanner() {
6
+ const context = useContext(CookieBannerContext);
7
+ if (!context) {
8
+ throw new Error('useCookieBanner must be used within a CookieBannerProvider');
9
+ }
10
+ return context;
11
+ }
12
+ const CONSENT_STORAGE_KEY = 'cookiesAllowed';
13
+ const getStoredDecision = ()=>{
14
+ const stored = localStorage.getItem(CONSENT_STORAGE_KEY);
15
+ if (stored === 'true') {
16
+ return 'granted';
17
+ }
18
+ if (stored === 'false') {
19
+ return 'denied';
20
+ }
21
+ return null;
22
+ };
23
+ export function CookieBannerProvider({ children, consentApiPath, consentStrategy }) {
24
+ const [userDecision, setUserDecision] = useState(null);
25
+ const [requiresConsent, setRequiresConsent] = useState(null);
26
+ useEffect(()=>{
27
+ const storedDecision = getStoredDecision();
28
+ setUserDecision(storedDecision);
29
+ if (storedDecision !== null) {
30
+ return;
31
+ }
32
+ let isActive = true;
33
+ const fetchRequiresConsent = async ()=>{
34
+ try {
35
+ const response = await fetch(consentApiPath, {
36
+ method: 'GET'
37
+ });
38
+ if (!response.ok) {
39
+ // TODO: Just console error
40
+ throw new Error('Consent API response not ok');
41
+ }
42
+ const data = await response.json();
43
+ if (isActive) {
44
+ setRequiresConsent(Boolean(data?.requiresConsent));
45
+ }
46
+ } catch {
47
+ if (isActive) {
48
+ setRequiresConsent(true);
49
+ }
50
+ }
51
+ };
52
+ void fetchRequiresConsent();
53
+ return ()=>{
54
+ isActive = false;
55
+ };
56
+ }, [
57
+ consentApiPath
58
+ ]);
59
+ const value = useMemo(()=>{
60
+ const hasUserDecision = userDecision !== null;
61
+ const shouldShowBanner = !hasUserDecision && requiresConsent === true;
62
+ let shouldLoadScripts = false;
63
+ let consentStatus = 'denied';
64
+ if (hasUserDecision) {
65
+ consentStatus = userDecision;
66
+ shouldLoadScripts = consentStrategy === 'require-consent-before-loading-scripts' ? userDecision === 'granted' : true;
67
+ } else {
68
+ switch(consentStrategy){
69
+ case 'load-scripts-revoke-consent-immediately':
70
+ shouldLoadScripts = true;
71
+ consentStatus = requiresConsent === false ? 'granted' : 'denied';
72
+ break;
73
+ case 'load-scripts-then-revoke-consent-after-geolocation-check':
74
+ shouldLoadScripts = true;
75
+ consentStatus = requiresConsent === true ? 'denied' : 'granted';
76
+ break;
77
+ case 'require-consent-before-loading-scripts':
78
+ shouldLoadScripts = requiresConsent === false;
79
+ consentStatus = requiresConsent === false ? 'granted' : 'denied';
80
+ break;
81
+ }
82
+ }
83
+ return {
84
+ accept: ()=>{
85
+ setUserDecision('granted');
86
+ localStorage.setItem(CONSENT_STORAGE_KEY, 'true');
87
+ },
88
+ consentStatus,
89
+ reject: ()=>{
90
+ setUserDecision('denied');
91
+ localStorage.setItem(CONSENT_STORAGE_KEY, 'false');
92
+ },
93
+ shouldLoadScripts,
94
+ shouldShowBanner
95
+ };
96
+ }, [
97
+ consentStrategy,
98
+ requiresConsent,
99
+ userDecision
100
+ ]);
101
+ return /*#__PURE__*/ _jsx(CookieBannerContext.Provider, {
102
+ value: value,
103
+ children: children
104
+ });
105
+ }
106
+
107
+ //# sourceMappingURL=CookieBannerProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/CookieBannerProvider.tsx"],"sourcesContent":["'use client'\n\nimport type { ReactNode } from 'react'\n\nimport { createContext, useContext, useEffect, useMemo, useState } from 'react'\n\nexport type ConsentStrategy =\n | 'load-scripts-revoke-consent-immediately'\n | 'load-scripts-then-revoke-consent-after-geolocation-check'\n | 'require-consent-before-loading-scripts'\n\ntype ConsentStatus = 'denied' | 'granted'\n\ninterface CookieBannerContextType {\n accept: () => void\n consentStatus: ConsentStatus\n reject: () => void\n shouldLoadScripts: boolean\n shouldShowBanner: boolean\n}\n\nconst CookieBannerContext = createContext<CookieBannerContextType | undefined>(undefined)\n\nexport function useCookieBanner() {\n const context = useContext(CookieBannerContext)\n if (!context) {\n throw new Error('useCookieBanner must be used within a CookieBannerProvider')\n }\n return context\n}\n\ninterface CookieBannerProviderProps {\n children: ReactNode\n consentApiPath: string\n /**\n * `load-scripts-revoke-consent-immediately`\n * - Render scripts immediately.\n * - Default consent is denied until a user grants.\n * - Banner shown only if geolocation requires consent.\n *\n * `load-scripts-then-revoke-consent-after-geolocation-check`\n * - Render scripts immediately.\n * - Default consent is granted until geolocation requires consent.\n * - If consent is required, revoke and show banner.\n *\n * `require-consent-before-loading-scripts`\n * - Do not render scripts until consent is granted when required.\n * - Banner shown only if geolocation requires consent.\n */\n consentStrategy: ConsentStrategy\n}\n\nconst CONSENT_STORAGE_KEY = 'cookiesAllowed'\n\nconst getStoredDecision = (): ConsentStatus | null => {\n const stored = localStorage.getItem(CONSENT_STORAGE_KEY)\n if (stored === 'true') {\n return 'granted'\n }\n if (stored === 'false') {\n return 'denied'\n }\n return null\n}\n\nexport function CookieBannerProvider({\n children,\n consentApiPath,\n consentStrategy,\n}: CookieBannerProviderProps) {\n const [userDecision, setUserDecision] = useState<ConsentStatus | null>(null)\n const [requiresConsent, setRequiresConsent] = useState<boolean | null>(null)\n\n useEffect(() => {\n const storedDecision = getStoredDecision()\n setUserDecision(storedDecision)\n if (storedDecision !== null) {\n return\n }\n\n let isActive = true\n const fetchRequiresConsent = async () => {\n try {\n const response = await fetch(consentApiPath, { method: 'GET' })\n if (!response.ok) {\n // TODO: Just console error\n throw new Error('Consent API response not ok')\n }\n const data = (await response.json()) as { requiresConsent?: boolean }\n if (isActive) {\n setRequiresConsent(Boolean(data?.requiresConsent))\n }\n } catch {\n if (isActive) {\n setRequiresConsent(true)\n }\n }\n }\n\n void fetchRequiresConsent()\n\n return () => {\n isActive = false\n }\n }, [consentApiPath])\n\n const value = useMemo<CookieBannerContextType>(() => {\n const hasUserDecision = userDecision !== null\n const shouldShowBanner = !hasUserDecision && requiresConsent === true\n\n let shouldLoadScripts = false\n let consentStatus: ConsentStatus = 'denied'\n\n if (hasUserDecision) {\n consentStatus = userDecision\n shouldLoadScripts =\n consentStrategy === 'require-consent-before-loading-scripts'\n ? userDecision === 'granted'\n : true\n } else {\n switch (consentStrategy) {\n case 'load-scripts-revoke-consent-immediately':\n shouldLoadScripts = true\n consentStatus = requiresConsent === false ? 'granted' : 'denied'\n break\n case 'load-scripts-then-revoke-consent-after-geolocation-check':\n shouldLoadScripts = true\n consentStatus = requiresConsent === true ? 'denied' : 'granted'\n break\n case 'require-consent-before-loading-scripts':\n shouldLoadScripts = requiresConsent === false\n consentStatus = requiresConsent === false ? 'granted' : 'denied'\n break\n }\n }\n\n return {\n accept: () => {\n setUserDecision('granted')\n localStorage.setItem(CONSENT_STORAGE_KEY, 'true')\n },\n consentStatus,\n reject: () => {\n setUserDecision('denied')\n localStorage.setItem(CONSENT_STORAGE_KEY, 'false')\n },\n shouldLoadScripts,\n shouldShowBanner,\n }\n }, [consentStrategy, requiresConsent, userDecision])\n\n return <CookieBannerContext.Provider value={value}>{children}</CookieBannerContext.Provider>\n}\n"],"names":["createContext","useContext","useEffect","useMemo","useState","CookieBannerContext","undefined","useCookieBanner","context","Error","CONSENT_STORAGE_KEY","getStoredDecision","stored","localStorage","getItem","CookieBannerProvider","children","consentApiPath","consentStrategy","userDecision","setUserDecision","requiresConsent","setRequiresConsent","storedDecision","isActive","fetchRequiresConsent","response","fetch","method","ok","data","json","Boolean","value","hasUserDecision","shouldShowBanner","shouldLoadScripts","consentStatus","accept","setItem","reject","Provider"],"mappings":"AAAA;;AAIA,SAASA,aAAa,EAAEC,UAAU,EAAEC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AAiB/E,MAAMC,oCAAsBL,cAAmDM;AAE/E,OAAO,SAASC;IACd,MAAMC,UAAUP,WAAWI;IAC3B,IAAI,CAACG,SAAS;QACZ,MAAM,IAAIC,MAAM;IAClB;IACA,OAAOD;AACT;AAuBA,MAAME,sBAAsB;AAE5B,MAAMC,oBAAoB;IACxB,MAAMC,SAASC,aAAaC,OAAO,CAACJ;IACpC,IAAIE,WAAW,QAAQ;QACrB,OAAO;IACT;IACA,IAAIA,WAAW,SAAS;QACtB,OAAO;IACT;IACA,OAAO;AACT;AAEA,OAAO,SAASG,qBAAqB,EACnCC,QAAQ,EACRC,cAAc,EACdC,eAAe,EACW;IAC1B,MAAM,CAACC,cAAcC,gBAAgB,GAAGhB,SAA+B;IACvE,MAAM,CAACiB,iBAAiBC,mBAAmB,GAAGlB,SAAyB;IAEvEF,UAAU;QACR,MAAMqB,iBAAiBZ;QACvBS,gBAAgBG;QAChB,IAAIA,mBAAmB,MAAM;YAC3B;QACF;QAEA,IAAIC,WAAW;QACf,MAAMC,uBAAuB;YAC3B,IAAI;gBACF,MAAMC,WAAW,MAAMC,MAAMV,gBAAgB;oBAAEW,QAAQ;gBAAM;gBAC7D,IAAI,CAACF,SAASG,EAAE,EAAE;oBAChB,2BAA2B;oBAC3B,MAAM,IAAIpB,MAAM;gBAClB;gBACA,MAAMqB,OAAQ,MAAMJ,SAASK,IAAI;gBACjC,IAAIP,UAAU;oBACZF,mBAAmBU,QAAQF,MAAMT;gBACnC;YACF,EAAE,OAAM;gBACN,IAAIG,UAAU;oBACZF,mBAAmB;gBACrB;YACF;QACF;QAEA,KAAKG;QAEL,OAAO;YACLD,WAAW;QACb;IACF,GAAG;QAACP;KAAe;IAEnB,MAAMgB,QAAQ9B,QAAiC;QAC7C,MAAM+B,kBAAkBf,iBAAiB;QACzC,MAAMgB,mBAAmB,CAACD,mBAAmBb,oBAAoB;QAEjE,IAAIe,oBAAoB;QACxB,IAAIC,gBAA+B;QAEnC,IAAIH,iBAAiB;YACnBG,gBAAgBlB;YAChBiB,oBACElB,oBAAoB,2CAChBC,iBAAiB,YACjB;QACR,OAAO;YACL,OAAQD;gBACN,KAAK;oBACHkB,oBAAoB;oBACpBC,gBAAgBhB,oBAAoB,QAAQ,YAAY;oBACxD;gBACF,KAAK;oBACHe,oBAAoB;oBACpBC,gBAAgBhB,oBAAoB,OAAO,WAAW;oBACtD;gBACF,KAAK;oBACHe,oBAAoBf,oBAAoB;oBACxCgB,gBAAgBhB,oBAAoB,QAAQ,YAAY;oBACxD;YACJ;QACF;QAEA,OAAO;YACLiB,QAAQ;gBACNlB,gBAAgB;gBAChBP,aAAa0B,OAAO,CAAC7B,qBAAqB;YAC5C;YACA2B;YACAG,QAAQ;gBACNpB,gBAAgB;gBAChBP,aAAa0B,OAAO,CAAC7B,qBAAqB;YAC5C;YACA0B;YACAD;QACF;IACF,GAAG;QAACjB;QAAiBG;QAAiBF;KAAa;IAEnD,qBAAO,KAACd,oBAAoBoC,QAAQ;QAACR,OAAOA;kBAAQjB;;AACtD"}
@@ -0,0 +1,10 @@
1
+ interface FacebookPixelProps {
2
+ pixelId: string;
3
+ }
4
+ export default function FacebookPixel({ pixelId }: FacebookPixelProps): import("react").JSX.Element | null;
5
+ declare global {
6
+ interface Window {
7
+ fbq: (...args: any[]) => void;
8
+ }
9
+ }
10
+ export {};
@@ -0,0 +1,84 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { usePathname } from 'next/navigation';
4
+ import Script from 'next/script';
5
+ import { useCallback, useEffect } from 'react';
6
+ import { useCookieBanner } from './CookieBannerProvider.js';
7
+ export default function FacebookPixel({ pixelId }) {
8
+ const pathname = usePathname();
9
+ const { consentStatus, shouldLoadScripts } = useCookieBanner();
10
+ const trackPageView = useCallback(()=>{
11
+ if (typeof window.fbq === 'function') {
12
+ window.fbq('track', 'PageView');
13
+ }
14
+ }, []);
15
+ const applyConsent = useCallback((status)=>{
16
+ if (typeof window.fbq !== 'function') {
17
+ return;
18
+ }
19
+ if (status === 'granted') {
20
+ window.fbq('consent', 'grant');
21
+ } else {
22
+ window.fbq('consent', 'revoke');
23
+ }
24
+ }, []);
25
+ useEffect(()=>{
26
+ applyConsent(consentStatus);
27
+ }, [
28
+ applyConsent,
29
+ consentStatus
30
+ ]);
31
+ useEffect(()=>{
32
+ if (pixelId && consentStatus === 'granted') {
33
+ trackPageView();
34
+ }
35
+ // eslint-disable-next-line react-hooks/exhaustive-deps
36
+ }, [
37
+ pathname,
38
+ consentStatus
39
+ ]);
40
+ if (!pixelId || !shouldLoadScripts) {
41
+ return null;
42
+ }
43
+ return /*#__PURE__*/ _jsxs(_Fragment, {
44
+ children: [
45
+ /*#__PURE__*/ _jsx(Script, {
46
+ dangerouslySetInnerHTML: {
47
+ __html: `
48
+ !function(f,b,e,v,n,t,s)
49
+ {if(f.fbq)return;n=f.fbq=function(){n.callMethod?
50
+ n.callMethod.apply(n,arguments):n.queue.push(arguments)};
51
+ if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
52
+ n.queue=[];t=b.createElement(e);t.async=!0;
53
+ t.src=v;s=b.getElementsByTagName(e)[0];
54
+ s.parentNode.insertBefore(t,s)}(window, document,'script',
55
+ 'https://connect.facebook.net/en_US/fbevents.js');
56
+ fbq('init', '${pixelId}');
57
+ `
58
+ },
59
+ id: "fb-pixel",
60
+ onLoad: ()=>{
61
+ applyConsent(consentStatus);
62
+ if (consentStatus === 'granted') {
63
+ trackPageView();
64
+ }
65
+ },
66
+ strategy: "afterInteractive"
67
+ }),
68
+ consentStatus === 'granted' && /*#__PURE__*/ _jsx("noscript", {
69
+ children: /*#__PURE__*/ _jsx("img", {
70
+ alt: "",
71
+ height: "1",
72
+ loading: "lazy",
73
+ src: `https://www.facebook.com/tr?id=${pixelId}&ev=PageView&noscript=1`,
74
+ style: {
75
+ display: 'none'
76
+ },
77
+ width: "1"
78
+ })
79
+ })
80
+ ]
81
+ });
82
+ }
83
+
84
+ //# sourceMappingURL=FacebookPixel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/FacebookPixel.tsx"],"sourcesContent":["'use client'\nimport { usePathname } from 'next/navigation'\nimport Script from 'next/script'\nimport { useCallback, useEffect } from 'react'\n\nimport { useCookieBanner } from './CookieBannerProvider.js'\n\ninterface FacebookPixelProps {\n pixelId: string\n}\n\nexport default function FacebookPixel({ pixelId }: FacebookPixelProps) {\n const pathname = usePathname()\n const { consentStatus, shouldLoadScripts } = useCookieBanner()\n\n const trackPageView = useCallback(() => {\n if (typeof window.fbq === 'function') {\n window.fbq('track', 'PageView')\n }\n }, [])\n\n const applyConsent = useCallback((status: 'denied' | 'granted') => {\n if (typeof window.fbq !== 'function') {\n return\n }\n\n if (status === 'granted') {\n window.fbq('consent', 'grant')\n } else {\n window.fbq('consent', 'revoke')\n }\n }, [])\n\n useEffect(() => {\n applyConsent(consentStatus)\n }, [applyConsent, consentStatus])\n\n useEffect(() => {\n if (pixelId && consentStatus === 'granted') {\n trackPageView()\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [pathname, consentStatus])\n\n if (!pixelId || !shouldLoadScripts) {\n return null\n }\n\n return (\n <>\n <Script\n dangerouslySetInnerHTML={{\n __html: `\n !function(f,b,e,v,n,t,s)\n {if(f.fbq)return;n=f.fbq=function(){n.callMethod?\n n.callMethod.apply(n,arguments):n.queue.push(arguments)};\n if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';\n n.queue=[];t=b.createElement(e);t.async=!0;\n t.src=v;s=b.getElementsByTagName(e)[0];\n s.parentNode.insertBefore(t,s)}(window, document,'script',\n 'https://connect.facebook.net/en_US/fbevents.js');\n fbq('init', '${pixelId}');\n `,\n }}\n id=\"fb-pixel\"\n onLoad={() => {\n applyConsent(consentStatus)\n if (consentStatus === 'granted') {\n trackPageView()\n }\n }}\n strategy=\"afterInteractive\"\n />\n {consentStatus === 'granted' && (\n <noscript>\n <img\n alt=\"\"\n height=\"1\"\n loading=\"lazy\"\n src={`https://www.facebook.com/tr?id=${pixelId}&ev=PageView&noscript=1`}\n style={{ display: 'none' }}\n width=\"1\"\n />\n </noscript>\n )}\n </>\n )\n}\n\ndeclare global {\n interface Window {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n fbq: (...args: any[]) => void\n }\n}\n"],"names":["usePathname","Script","useCallback","useEffect","useCookieBanner","FacebookPixel","pixelId","pathname","consentStatus","shouldLoadScripts","trackPageView","window","fbq","applyConsent","status","dangerouslySetInnerHTML","__html","id","onLoad","strategy","noscript","img","alt","height","loading","src","style","display","width"],"mappings":"AAAA;;AACA,SAASA,WAAW,QAAQ,kBAAiB;AAC7C,OAAOC,YAAY,cAAa;AAChC,SAASC,WAAW,EAAEC,SAAS,QAAQ,QAAO;AAE9C,SAASC,eAAe,QAAQ,4BAA2B;AAM3D,eAAe,SAASC,cAAc,EAAEC,OAAO,EAAsB;IACnE,MAAMC,WAAWP;IACjB,MAAM,EAAEQ,aAAa,EAAEC,iBAAiB,EAAE,GAAGL;IAE7C,MAAMM,gBAAgBR,YAAY;QAChC,IAAI,OAAOS,OAAOC,GAAG,KAAK,YAAY;YACpCD,OAAOC,GAAG,CAAC,SAAS;QACtB;IACF,GAAG,EAAE;IAEL,MAAMC,eAAeX,YAAY,CAACY;QAChC,IAAI,OAAOH,OAAOC,GAAG,KAAK,YAAY;YACpC;QACF;QAEA,IAAIE,WAAW,WAAW;YACxBH,OAAOC,GAAG,CAAC,WAAW;QACxB,OAAO;YACLD,OAAOC,GAAG,CAAC,WAAW;QACxB;IACF,GAAG,EAAE;IAELT,UAAU;QACRU,aAAaL;IACf,GAAG;QAACK;QAAcL;KAAc;IAEhCL,UAAU;QACR,IAAIG,WAAWE,kBAAkB,WAAW;YAC1CE;QACF;IACA,uDAAuD;IACzD,GAAG;QAACH;QAAUC;KAAc;IAE5B,IAAI,CAACF,WAAW,CAACG,mBAAmB;QAClC,OAAO;IACT;IAEA,qBACE;;0BACE,KAACR;gBACCc,yBAAyB;oBACvBC,QAAQ,CAAC;;;;;;;;;yBASM,EAAEV,QAAQ;UACzB,CAAC;gBACH;gBACAW,IAAG;gBACHC,QAAQ;oBACNL,aAAaL;oBACb,IAAIA,kBAAkB,WAAW;wBAC/BE;oBACF;gBACF;gBACAS,UAAS;;YAEVX,kBAAkB,2BACjB,KAACY;0BACC,cAAA,KAACC;oBACCC,KAAI;oBACJC,QAAO;oBACPC,SAAQ;oBACRC,KAAK,CAAC,+BAA+B,EAAEnB,QAAQ,uBAAuB,CAAC;oBACvEoB,OAAO;wBAAEC,SAAS;oBAAO;oBACzBC,OAAM;;;;;AAMlB"}
@@ -0,0 +1,11 @@
1
+ interface GoogleAnalyticsProps {
2
+ gaId: string;
3
+ }
4
+ export default function GoogleAnalytics({ gaId }: GoogleAnalyticsProps): import("react").JSX.Element | null;
5
+ declare global {
6
+ interface Window {
7
+ dataLayer: any[];
8
+ gtag: (...args: any[]) => void;
9
+ }
10
+ }
11
+ export {};
@@ -0,0 +1,154 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { usePathname } from 'next/navigation';
4
+ import Script from 'next/script';
5
+ import { useCallback, useEffect, useRef } from 'react';
6
+ import { useCookieBanner } from './CookieBannerProvider.js';
7
+ export default function GoogleAnalytics({ gaId }) {
8
+ const pathname = usePathname();
9
+ const { consentStatus, shouldLoadScripts } = useCookieBanner();
10
+ const hasSentInitialRef = useRef(false);
11
+ const lastTrackedPathnameRef = useRef(null);
12
+ const trackPageView = useCallback(()=>{
13
+ if (typeof window.gtag === 'function') {
14
+ window.gtag('config', gaId, {
15
+ page_path: pathname
16
+ });
17
+ }
18
+ }, [
19
+ gaId,
20
+ pathname
21
+ ]);
22
+ const updateConsent = useCallback((status)=>{
23
+ if (typeof window.gtag !== 'function') {
24
+ return;
25
+ }
26
+ if (status === 'granted') {
27
+ window.gtag('consent', 'update', {
28
+ ad_personalization: 'granted',
29
+ ad_storage: 'granted',
30
+ ad_user_data: 'granted',
31
+ analytics_storage: 'granted',
32
+ functionality_storage: 'granted',
33
+ personalization_storage: 'granted'
34
+ });
35
+ } else {
36
+ window.gtag('consent', 'update', {
37
+ ad_personalization: 'denied',
38
+ ad_storage: 'denied',
39
+ ad_user_data: 'denied',
40
+ analytics_storage: 'denied',
41
+ functionality_storage: 'denied',
42
+ personalization_storage: 'denied',
43
+ security_storage: 'granted'
44
+ });
45
+ }
46
+ }, []);
47
+ const getConsentDefaults = useCallback((status)=>{
48
+ if (status === 'granted') {
49
+ return {
50
+ ad_personalization: 'granted',
51
+ ad_storage: 'granted',
52
+ ad_user_data: 'granted',
53
+ analytics_storage: 'granted',
54
+ functionality_storage: 'granted',
55
+ personalization_storage: 'granted',
56
+ security_storage: 'granted'
57
+ };
58
+ }
59
+ return {
60
+ ad_personalization: 'denied',
61
+ ad_storage: 'denied',
62
+ ad_user_data: 'denied',
63
+ analytics_storage: 'denied',
64
+ functionality_storage: 'denied',
65
+ personalization_storage: 'denied',
66
+ security_storage: 'granted'
67
+ };
68
+ }, []);
69
+ useEffect(()=>{
70
+ updateConsent(consentStatus);
71
+ }, [
72
+ consentStatus,
73
+ updateConsent
74
+ ]);
75
+ useEffect(()=>{
76
+ if (consentStatus === 'granted' && gaId && typeof window.gtag === 'function') {
77
+ if (!hasSentInitialRef.current) {
78
+ window.gtag('config', gaId, {
79
+ page_path: window.location.pathname
80
+ });
81
+ hasSentInitialRef.current = true;
82
+ lastTrackedPathnameRef.current = window.location.pathname;
83
+ }
84
+ }
85
+ }, [
86
+ consentStatus,
87
+ gaId
88
+ ]);
89
+ useEffect(()=>{
90
+ if (consentStatus === 'granted' && gaId && typeof window.gtag === 'function') {
91
+ if (hasSentInitialRef.current) {
92
+ if (lastTrackedPathnameRef.current !== pathname) {
93
+ trackPageView();
94
+ lastTrackedPathnameRef.current = pathname;
95
+ }
96
+ }
97
+ }
98
+ }, [
99
+ pathname,
100
+ consentStatus,
101
+ gaId,
102
+ trackPageView
103
+ ]);
104
+ if (!gaId || !shouldLoadScripts) {
105
+ return null;
106
+ }
107
+ return /*#__PURE__*/ _jsxs(_Fragment, {
108
+ children: [
109
+ /*#__PURE__*/ _jsx(Script, {
110
+ dangerouslySetInnerHTML: {
111
+ __html: `
112
+ window.dataLayer = window.dataLayer || [];
113
+ function gtag(){dataLayer.push(arguments);}
114
+ window.gtag = window.gtag || gtag;
115
+ gtag('consent', 'default', ${JSON.stringify(getConsentDefaults(consentStatus))});
116
+ `
117
+ },
118
+ id: "ga-consent-default",
119
+ strategy: "beforeInteractive"
120
+ }),
121
+ /*#__PURE__*/ _jsx(Script, {
122
+ id: "gtag-base",
123
+ src: `https://www.googletagmanager.com/gtag/js?id=${gaId}`,
124
+ strategy: "afterInteractive"
125
+ }),
126
+ /*#__PURE__*/ _jsx(Script, {
127
+ dangerouslySetInnerHTML: {
128
+ __html: `
129
+ window.dataLayer = window.dataLayer || [];
130
+ function gtag(){dataLayer.push(arguments);}
131
+ window.gtag = gtag;
132
+ gtag('js', new Date());
133
+ `
134
+ },
135
+ id: "gtag-init",
136
+ onLoad: ()=>{
137
+ if (typeof window.gtag === 'function') {
138
+ updateConsent(consentStatus);
139
+ if (consentStatus === 'granted') {
140
+ window.gtag('config', gaId, {
141
+ page_path: window.location.pathname
142
+ });
143
+ hasSentInitialRef.current = true;
144
+ lastTrackedPathnameRef.current = window.location.pathname;
145
+ }
146
+ }
147
+ },
148
+ strategy: "afterInteractive"
149
+ })
150
+ ]
151
+ });
152
+ }
153
+
154
+ //# sourceMappingURL=GoogleAnalytics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/GoogleAnalytics.tsx"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\n'use client'\n\nimport { usePathname } from 'next/navigation'\nimport Script from 'next/script'\nimport { useCallback, useEffect, useRef } from 'react'\n\nimport { useCookieBanner } from './CookieBannerProvider.js'\n\ninterface GoogleAnalyticsProps {\n gaId: string\n}\n\nexport default function GoogleAnalytics({ gaId }: GoogleAnalyticsProps) {\n const pathname = usePathname()\n const { consentStatus, shouldLoadScripts } = useCookieBanner()\n const hasSentInitialRef = useRef(false)\n const lastTrackedPathnameRef = useRef<null | string>(null)\n\n const trackPageView = useCallback(() => {\n if (typeof window.gtag === 'function') {\n window.gtag('config', gaId, {\n page_path: pathname,\n })\n }\n }, [gaId, pathname])\n\n const updateConsent = useCallback((status: 'denied' | 'granted') => {\n if (typeof window.gtag !== 'function') {\n return\n }\n\n if (status === 'granted') {\n window.gtag('consent', 'update', {\n ad_personalization: 'granted',\n ad_storage: 'granted',\n ad_user_data: 'granted',\n analytics_storage: 'granted',\n functionality_storage: 'granted',\n personalization_storage: 'granted',\n })\n } else {\n window.gtag('consent', 'update', {\n ad_personalization: 'denied',\n ad_storage: 'denied',\n ad_user_data: 'denied',\n analytics_storage: 'denied',\n functionality_storage: 'denied',\n personalization_storage: 'denied',\n security_storage: 'granted',\n })\n }\n }, [])\n\n const getConsentDefaults = useCallback((status: 'denied' | 'granted') => {\n if (status === 'granted') {\n return {\n ad_personalization: 'granted',\n ad_storage: 'granted',\n ad_user_data: 'granted',\n analytics_storage: 'granted',\n functionality_storage: 'granted',\n personalization_storage: 'granted',\n security_storage: 'granted',\n }\n }\n\n return {\n ad_personalization: 'denied',\n ad_storage: 'denied',\n ad_user_data: 'denied',\n analytics_storage: 'denied',\n functionality_storage: 'denied',\n personalization_storage: 'denied',\n security_storage: 'granted',\n }\n }, [])\n\n useEffect(() => {\n updateConsent(consentStatus)\n }, [consentStatus, updateConsent])\n\n useEffect(() => {\n if (consentStatus === 'granted' && gaId && typeof window.gtag === 'function') {\n if (!hasSentInitialRef.current) {\n window.gtag('config', gaId, { page_path: window.location.pathname })\n hasSentInitialRef.current = true\n lastTrackedPathnameRef.current = window.location.pathname\n }\n }\n }, [consentStatus, gaId])\n\n useEffect(() => {\n if (consentStatus === 'granted' && gaId && typeof window.gtag === 'function') {\n if (hasSentInitialRef.current) {\n if (lastTrackedPathnameRef.current !== pathname) {\n trackPageView()\n lastTrackedPathnameRef.current = pathname\n }\n }\n }\n }, [pathname, consentStatus, gaId, trackPageView])\n\n if (!gaId || !shouldLoadScripts) {\n return null\n }\n\n return (\n <>\n <Script\n dangerouslySetInnerHTML={{\n __html: `\n window.dataLayer = window.dataLayer || [];\n function gtag(){dataLayer.push(arguments);}\n window.gtag = window.gtag || gtag;\n gtag('consent', 'default', ${JSON.stringify(getConsentDefaults(consentStatus))});\n `,\n }}\n id=\"ga-consent-default\"\n strategy=\"beforeInteractive\"\n />\n <Script\n id=\"gtag-base\"\n src={`https://www.googletagmanager.com/gtag/js?id=${gaId}`}\n strategy=\"afterInteractive\"\n />\n <Script\n dangerouslySetInnerHTML={{\n __html: `\n window.dataLayer = window.dataLayer || [];\n function gtag(){dataLayer.push(arguments);} \n window.gtag = gtag;\n gtag('js', new Date());\n `,\n }}\n id=\"gtag-init\"\n onLoad={() => {\n if (typeof window.gtag === 'function') {\n updateConsent(consentStatus)\n if (consentStatus === 'granted') {\n window.gtag('config', gaId, { page_path: window.location.pathname })\n hasSentInitialRef.current = true\n lastTrackedPathnameRef.current = window.location.pathname\n }\n }\n }}\n strategy=\"afterInteractive\"\n />\n </>\n )\n}\n\ndeclare global {\n interface Window {\n dataLayer: any[]\n gtag: (...args: any[]) => void\n }\n}\n"],"names":["usePathname","Script","useCallback","useEffect","useRef","useCookieBanner","GoogleAnalytics","gaId","pathname","consentStatus","shouldLoadScripts","hasSentInitialRef","lastTrackedPathnameRef","trackPageView","window","gtag","page_path","updateConsent","status","ad_personalization","ad_storage","ad_user_data","analytics_storage","functionality_storage","personalization_storage","security_storage","getConsentDefaults","current","location","dangerouslySetInnerHTML","__html","JSON","stringify","id","strategy","src","onLoad"],"mappings":"AAAA,qDAAqD,GACrD;;AAEA,SAASA,WAAW,QAAQ,kBAAiB;AAC7C,OAAOC,YAAY,cAAa;AAChC,SAASC,WAAW,EAAEC,SAAS,EAAEC,MAAM,QAAQ,QAAO;AAEtD,SAASC,eAAe,QAAQ,4BAA2B;AAM3D,eAAe,SAASC,gBAAgB,EAAEC,IAAI,EAAwB;IACpE,MAAMC,WAAWR;IACjB,MAAM,EAAES,aAAa,EAAEC,iBAAiB,EAAE,GAAGL;IAC7C,MAAMM,oBAAoBP,OAAO;IACjC,MAAMQ,yBAAyBR,OAAsB;IAErD,MAAMS,gBAAgBX,YAAY;QAChC,IAAI,OAAOY,OAAOC,IAAI,KAAK,YAAY;YACrCD,OAAOC,IAAI,CAAC,UAAUR,MAAM;gBAC1BS,WAAWR;YACb;QACF;IACF,GAAG;QAACD;QAAMC;KAAS;IAEnB,MAAMS,gBAAgBf,YAAY,CAACgB;QACjC,IAAI,OAAOJ,OAAOC,IAAI,KAAK,YAAY;YACrC;QACF;QAEA,IAAIG,WAAW,WAAW;YACxBJ,OAAOC,IAAI,CAAC,WAAW,UAAU;gBAC/BI,oBAAoB;gBACpBC,YAAY;gBACZC,cAAc;gBACdC,mBAAmB;gBACnBC,uBAAuB;gBACvBC,yBAAyB;YAC3B;QACF,OAAO;YACLV,OAAOC,IAAI,CAAC,WAAW,UAAU;gBAC/BI,oBAAoB;gBACpBC,YAAY;gBACZC,cAAc;gBACdC,mBAAmB;gBACnBC,uBAAuB;gBACvBC,yBAAyB;gBACzBC,kBAAkB;YACpB;QACF;IACF,GAAG,EAAE;IAEL,MAAMC,qBAAqBxB,YAAY,CAACgB;QACtC,IAAIA,WAAW,WAAW;YACxB,OAAO;gBACLC,oBAAoB;gBACpBC,YAAY;gBACZC,cAAc;gBACdC,mBAAmB;gBACnBC,uBAAuB;gBACvBC,yBAAyB;gBACzBC,kBAAkB;YACpB;QACF;QAEA,OAAO;YACLN,oBAAoB;YACpBC,YAAY;YACZC,cAAc;YACdC,mBAAmB;YACnBC,uBAAuB;YACvBC,yBAAyB;YACzBC,kBAAkB;QACpB;IACF,GAAG,EAAE;IAELtB,UAAU;QACRc,cAAcR;IAChB,GAAG;QAACA;QAAeQ;KAAc;IAEjCd,UAAU;QACR,IAAIM,kBAAkB,aAAaF,QAAQ,OAAOO,OAAOC,IAAI,KAAK,YAAY;YAC5E,IAAI,CAACJ,kBAAkBgB,OAAO,EAAE;gBAC9Bb,OAAOC,IAAI,CAAC,UAAUR,MAAM;oBAAES,WAAWF,OAAOc,QAAQ,CAACpB,QAAQ;gBAAC;gBAClEG,kBAAkBgB,OAAO,GAAG;gBAC5Bf,uBAAuBe,OAAO,GAAGb,OAAOc,QAAQ,CAACpB,QAAQ;YAC3D;QACF;IACF,GAAG;QAACC;QAAeF;KAAK;IAExBJ,UAAU;QACR,IAAIM,kBAAkB,aAAaF,QAAQ,OAAOO,OAAOC,IAAI,KAAK,YAAY;YAC5E,IAAIJ,kBAAkBgB,OAAO,EAAE;gBAC7B,IAAIf,uBAAuBe,OAAO,KAAKnB,UAAU;oBAC/CK;oBACAD,uBAAuBe,OAAO,GAAGnB;gBACnC;YACF;QACF;IACF,GAAG;QAACA;QAAUC;QAAeF;QAAMM;KAAc;IAEjD,IAAI,CAACN,QAAQ,CAACG,mBAAmB;QAC/B,OAAO;IACT;IAEA,qBACE;;0BACE,KAACT;gBACC4B,yBAAyB;oBACvBC,QAAQ,CAAC;;;;uCAIoB,EAAEC,KAAKC,SAAS,CAACN,mBAAmBjB,gBAAgB;UACjF,CAAC;gBACH;gBACAwB,IAAG;gBACHC,UAAS;;0BAEX,KAACjC;gBACCgC,IAAG;gBACHE,KAAK,CAAC,4CAA4C,EAAE5B,MAAM;gBAC1D2B,UAAS;;0BAEX,KAACjC;gBACC4B,yBAAyB;oBACvBC,QAAQ,CAAC;;;;;UAKT,CAAC;gBACH;gBACAG,IAAG;gBACHG,QAAQ;oBACN,IAAI,OAAOtB,OAAOC,IAAI,KAAK,YAAY;wBACrCE,cAAcR;wBACd,IAAIA,kBAAkB,WAAW;4BAC/BK,OAAOC,IAAI,CAAC,UAAUR,MAAM;gCAAES,WAAWF,OAAOc,QAAQ,CAACpB,QAAQ;4BAAC;4BAClEG,kBAAkBgB,OAAO,GAAG;4BAC5Bf,uBAAuBe,OAAO,GAAGb,OAAOc,QAAQ,CAACpB,QAAQ;wBAC3D;oBACF;gBACF;gBACA0B,UAAS;;;;AAIjB"}
@@ -0,0 +1,11 @@
1
+ interface GoogleTagManagerProps {
2
+ gtmId: string;
3
+ }
4
+ export default function GoogleTagManager({ gtmId }: GoogleTagManagerProps): import("react").JSX.Element | null;
5
+ declare global {
6
+ interface Window {
7
+ dataLayer: any[];
8
+ gtag: (...args: any[]) => void;
9
+ }
10
+ }
11
+ export {};
@@ -0,0 +1,99 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import Script from 'next/script';
4
+ import { useEffect } from 'react';
5
+ import { useCookieBanner } from './CookieBannerProvider.js';
6
+ const updateConsent = (consentStatus)=>{
7
+ if (typeof window === 'undefined') {
8
+ return;
9
+ }
10
+ window.dataLayer = window.dataLayer || [];
11
+ window.gtag = window.gtag || function gtag(...args) {
12
+ window.dataLayer.push(args);
13
+ };
14
+ if (typeof window.gtag === 'function') {
15
+ if (consentStatus === 'granted') {
16
+ window.gtag('consent', 'update', {
17
+ ad_personalization: 'granted',
18
+ ad_storage: 'granted',
19
+ ad_user_data: 'granted',
20
+ analytics_storage: 'granted',
21
+ functionality_storage: 'granted',
22
+ personalization_storage: 'granted'
23
+ });
24
+ } else {
25
+ window.gtag('consent', 'update', {
26
+ ad_personalization: 'denied',
27
+ ad_storage: 'denied',
28
+ ad_user_data: 'denied',
29
+ analytics_storage: 'denied',
30
+ functionality_storage: 'denied',
31
+ personalization_storage: 'denied',
32
+ security_storage: 'granted'
33
+ });
34
+ }
35
+ }
36
+ };
37
+ const getConsentDefaults = (consentStatus)=>{
38
+ if (consentStatus === 'granted') {
39
+ return {
40
+ ad_personalization: 'granted',
41
+ ad_storage: 'granted',
42
+ ad_user_data: 'granted',
43
+ analytics_storage: 'granted',
44
+ functionality_storage: 'granted',
45
+ personalization_storage: 'granted',
46
+ security_storage: 'granted'
47
+ };
48
+ }
49
+ return {
50
+ ad_personalization: 'denied',
51
+ ad_storage: 'denied',
52
+ ad_user_data: 'denied',
53
+ analytics_storage: 'denied',
54
+ functionality_storage: 'denied',
55
+ personalization_storage: 'denied',
56
+ security_storage: 'granted'
57
+ };
58
+ };
59
+ export default function GoogleTagManager({ gtmId }) {
60
+ const { consentStatus, shouldLoadScripts } = useCookieBanner();
61
+ useEffect(()=>{
62
+ updateConsent(consentStatus);
63
+ }, [
64
+ consentStatus
65
+ ]);
66
+ if (!gtmId || !shouldLoadScripts) {
67
+ return null;
68
+ }
69
+ return /*#__PURE__*/ _jsxs(_Fragment, {
70
+ children: [
71
+ shouldLoadScripts && /*#__PURE__*/ _jsx(Script, {
72
+ dangerouslySetInnerHTML: {
73
+ __html: `
74
+ window.dataLayer = window.dataLayer || [];
75
+ function gtag(){dataLayer.push(arguments);}
76
+ window.gtag = window.gtag || gtag;
77
+ gtag('consent', 'default', ${JSON.stringify(getConsentDefaults(consentStatus))});
78
+ `
79
+ },
80
+ id: "gtm-consent-default",
81
+ strategy: "beforeInteractive"
82
+ }),
83
+ shouldLoadScripts && /*#__PURE__*/ _jsx(Script, {
84
+ dangerouslySetInnerHTML: {
85
+ __html: `
86
+ (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
87
+ new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
88
+ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
89
+ 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
90
+ })(window,document,'script','dataLayer','${gtmId}');
91
+ `
92
+ },
93
+ id: "gtm"
94
+ })
95
+ ]
96
+ });
97
+ }
98
+
99
+ //# sourceMappingURL=GoogleTagManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/GoogleTagManager.tsx"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\n'use client'\n\nimport Script from 'next/script'\nimport { useEffect } from 'react'\n\nimport { useCookieBanner } from './CookieBannerProvider.js'\n\ninterface GoogleTagManagerProps {\n gtmId: string\n}\n\nconst updateConsent = (consentStatus: 'denied' | 'granted') => {\n if (typeof window === 'undefined') {\n return\n }\n\n window.dataLayer = window.dataLayer || []\n window.gtag =\n window.gtag ||\n function gtag(...args: any[]) {\n window.dataLayer.push(args)\n }\n\n if (typeof window.gtag === 'function') {\n if (consentStatus === 'granted') {\n window.gtag('consent', 'update', {\n ad_personalization: 'granted',\n ad_storage: 'granted',\n ad_user_data: 'granted',\n analytics_storage: 'granted',\n functionality_storage: 'granted',\n personalization_storage: 'granted',\n })\n } else {\n window.gtag('consent', 'update', {\n ad_personalization: 'denied',\n ad_storage: 'denied',\n ad_user_data: 'denied',\n analytics_storage: 'denied',\n functionality_storage: 'denied',\n personalization_storage: 'denied',\n security_storage: 'granted',\n })\n }\n }\n}\n\nconst getConsentDefaults = (consentStatus: 'denied' | 'granted') => {\n if (consentStatus === 'granted') {\n return {\n ad_personalization: 'granted',\n ad_storage: 'granted',\n ad_user_data: 'granted',\n analytics_storage: 'granted',\n functionality_storage: 'granted',\n personalization_storage: 'granted',\n security_storage: 'granted',\n }\n }\n\n return {\n ad_personalization: 'denied',\n ad_storage: 'denied',\n ad_user_data: 'denied',\n analytics_storage: 'denied',\n functionality_storage: 'denied',\n personalization_storage: 'denied',\n security_storage: 'granted',\n }\n}\n\nexport default function GoogleTagManager({ gtmId }: GoogleTagManagerProps) {\n const { consentStatus, shouldLoadScripts } = useCookieBanner()\n\n useEffect(() => {\n updateConsent(consentStatus)\n }, [consentStatus])\n\n if (!gtmId || !shouldLoadScripts) {\n return null\n }\n\n return (\n <>\n {shouldLoadScripts && (\n <Script\n dangerouslySetInnerHTML={{\n __html: `\n window.dataLayer = window.dataLayer || [];\n function gtag(){dataLayer.push(arguments);}\n window.gtag = window.gtag || gtag;\n gtag('consent', 'default', ${JSON.stringify(\n getConsentDefaults(consentStatus),\n )});\n `,\n }}\n id=\"gtm-consent-default\"\n strategy=\"beforeInteractive\"\n />\n )}\n {shouldLoadScripts && (\n <Script\n dangerouslySetInnerHTML={{\n __html: `\n (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':\n new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],\n j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=\n 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);\n })(window,document,'script','dataLayer','${gtmId}');\n `,\n }}\n id=\"gtm\"\n />\n )}\n </>\n )\n}\n\ndeclare global {\n interface Window {\n dataLayer: any[]\n gtag: (...args: any[]) => void\n }\n}\n"],"names":["Script","useEffect","useCookieBanner","updateConsent","consentStatus","window","dataLayer","gtag","args","push","ad_personalization","ad_storage","ad_user_data","analytics_storage","functionality_storage","personalization_storage","security_storage","getConsentDefaults","GoogleTagManager","gtmId","shouldLoadScripts","dangerouslySetInnerHTML","__html","JSON","stringify","id","strategy"],"mappings":"AAAA,qDAAqD,GACrD;;AAEA,OAAOA,YAAY,cAAa;AAChC,SAASC,SAAS,QAAQ,QAAO;AAEjC,SAASC,eAAe,QAAQ,4BAA2B;AAM3D,MAAMC,gBAAgB,CAACC;IACrB,IAAI,OAAOC,WAAW,aAAa;QACjC;IACF;IAEAA,OAAOC,SAAS,GAAGD,OAAOC,SAAS,IAAI,EAAE;IACzCD,OAAOE,IAAI,GACTF,OAAOE,IAAI,IACX,SAASA,KAAK,GAAGC,IAAW;QAC1BH,OAAOC,SAAS,CAACG,IAAI,CAACD;IACxB;IAEF,IAAI,OAAOH,OAAOE,IAAI,KAAK,YAAY;QACrC,IAAIH,kBAAkB,WAAW;YAC/BC,OAAOE,IAAI,CAAC,WAAW,UAAU;gBAC/BG,oBAAoB;gBACpBC,YAAY;gBACZC,cAAc;gBACdC,mBAAmB;gBACnBC,uBAAuB;gBACvBC,yBAAyB;YAC3B;QACF,OAAO;YACLV,OAAOE,IAAI,CAAC,WAAW,UAAU;gBAC/BG,oBAAoB;gBACpBC,YAAY;gBACZC,cAAc;gBACdC,mBAAmB;gBACnBC,uBAAuB;gBACvBC,yBAAyB;gBACzBC,kBAAkB;YACpB;QACF;IACF;AACF;AAEA,MAAMC,qBAAqB,CAACb;IAC1B,IAAIA,kBAAkB,WAAW;QAC/B,OAAO;YACLM,oBAAoB;YACpBC,YAAY;YACZC,cAAc;YACdC,mBAAmB;YACnBC,uBAAuB;YACvBC,yBAAyB;YACzBC,kBAAkB;QACpB;IACF;IAEA,OAAO;QACLN,oBAAoB;QACpBC,YAAY;QACZC,cAAc;QACdC,mBAAmB;QACnBC,uBAAuB;QACvBC,yBAAyB;QACzBC,kBAAkB;IACpB;AACF;AAEA,eAAe,SAASE,iBAAiB,EAAEC,KAAK,EAAyB;IACvE,MAAM,EAAEf,aAAa,EAAEgB,iBAAiB,EAAE,GAAGlB;IAE7CD,UAAU;QACRE,cAAcC;IAChB,GAAG;QAACA;KAAc;IAElB,IAAI,CAACe,SAAS,CAACC,mBAAmB;QAChC,OAAO;IACT;IAEA,qBACE;;YACGA,mCACC,KAACpB;gBACCqB,yBAAyB;oBACvBC,QAAQ,CAAC;;;;2CAIsB,EAAEC,KAAKC,SAAS,CACzCP,mBAAmBb,gBACnB;cACJ,CAAC;gBACL;gBACAqB,IAAG;gBACHC,UAAS;;YAGZN,mCACC,KAACpB;gBACCqB,yBAAyB;oBACvBC,QAAQ,CAAC;;;;;yDAKoC,EAAEH,MAAM;cACnD,CAAC;gBACL;gBACAM,IAAG;;;;AAKb"}
@@ -0,0 +1,10 @@
1
+ interface MicrosoftClarityProps {
2
+ clarityId: string;
3
+ }
4
+ export default function MicrosoftClarity({ clarityId }: MicrosoftClarityProps): import("react").JSX.Element | null;
5
+ declare global {
6
+ interface Window {
7
+ clarity: (...args: any[]) => void;
8
+ }
9
+ }
10
+ export {};
@@ -0,0 +1,49 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import Script from 'next/script';
4
+ import { useCallback, useEffect } from 'react';
5
+ import { useCookieBanner } from './CookieBannerProvider.js';
6
+ export default function MicrosoftClarity({ clarityId }) {
7
+ const { consentStatus, shouldLoadScripts } = useCookieBanner();
8
+ const applyConsent = useCallback((status)=>{
9
+ if (typeof window.clarity !== 'function') {
10
+ return;
11
+ }
12
+ if (status === 'granted') {
13
+ window.clarity('consentv2', {
14
+ ad_Storage: 'granted',
15
+ analytics_Storage: 'granted'
16
+ });
17
+ } else {
18
+ window.clarity('consentv2', {
19
+ ad_Storage: 'denied',
20
+ analytics_Storage: 'denied'
21
+ });
22
+ window.clarity('consent', false);
23
+ }
24
+ }, []);
25
+ useEffect(()=>{
26
+ applyConsent(consentStatus);
27
+ }, [
28
+ applyConsent,
29
+ consentStatus
30
+ ]);
31
+ if (!clarityId || !shouldLoadScripts) {
32
+ return null;
33
+ }
34
+ return /*#__PURE__*/ _jsx(Script, {
35
+ id: "ms-clarity",
36
+ onLoad: ()=>{
37
+ applyConsent(consentStatus);
38
+ },
39
+ children: `
40
+ (function(c,l,a,r,i,t,y){
41
+ c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
42
+ t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
43
+ y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
44
+ })(window, document, "clarity", "script", "${clarityId}");
45
+ `
46
+ });
47
+ }
48
+
49
+ //# sourceMappingURL=MicrosoftClarity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/MicrosoftClarity.tsx"],"sourcesContent":["'use client'\n\nimport Script from 'next/script'\nimport { useCallback, useEffect } from 'react'\n\nimport { useCookieBanner } from './CookieBannerProvider.js'\n\n// WARNING: Cookies must be disabled in Clarity dashboard for this to be GDPR compliant\n// See https://learn.microsoft.com/en-us/clarity/setup-and-installation/cookie-consent\n\ninterface MicrosoftClarityProps {\n clarityId: string\n}\n\nexport default function MicrosoftClarity({ clarityId }: MicrosoftClarityProps) {\n const { consentStatus, shouldLoadScripts } = useCookieBanner()\n\n const applyConsent = useCallback((status: 'denied' | 'granted') => {\n if (typeof window.clarity !== 'function') {\n return\n }\n\n if (status === 'granted') {\n window.clarity('consentv2', {\n ad_Storage: 'granted',\n analytics_Storage: 'granted',\n })\n } else {\n window.clarity('consentv2', {\n ad_Storage: 'denied',\n analytics_Storage: 'denied',\n })\n window.clarity('consent', false)\n }\n }, [])\n\n useEffect(() => {\n applyConsent(consentStatus)\n }, [applyConsent, consentStatus])\n\n if (!clarityId || !shouldLoadScripts) {\n return null\n }\n\n return (\n <Script\n id=\"ms-clarity\"\n onLoad={() => {\n applyConsent(consentStatus)\n }}\n >\n {`\n (function(c,l,a,r,i,t,y){\n c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};\n t=l.createElement(r);t.async=1;t.src=\"https://www.clarity.ms/tag/\"+i;\n y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);\n })(window, document, \"clarity\", \"script\", \"${clarityId}\");\n `}\n </Script>\n )\n}\n\ndeclare global {\n interface Window {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n clarity: (...args: any[]) => void\n }\n}\n"],"names":["Script","useCallback","useEffect","useCookieBanner","MicrosoftClarity","clarityId","consentStatus","shouldLoadScripts","applyConsent","status","window","clarity","ad_Storage","analytics_Storage","id","onLoad"],"mappings":"AAAA;;AAEA,OAAOA,YAAY,cAAa;AAChC,SAASC,WAAW,EAAEC,SAAS,QAAQ,QAAO;AAE9C,SAASC,eAAe,QAAQ,4BAA2B;AAS3D,eAAe,SAASC,iBAAiB,EAAEC,SAAS,EAAyB;IAC3E,MAAM,EAAEC,aAAa,EAAEC,iBAAiB,EAAE,GAAGJ;IAE7C,MAAMK,eAAeP,YAAY,CAACQ;QAChC,IAAI,OAAOC,OAAOC,OAAO,KAAK,YAAY;YACxC;QACF;QAEA,IAAIF,WAAW,WAAW;YACxBC,OAAOC,OAAO,CAAC,aAAa;gBAC1BC,YAAY;gBACZC,mBAAmB;YACrB;QACF,OAAO;YACLH,OAAOC,OAAO,CAAC,aAAa;gBAC1BC,YAAY;gBACZC,mBAAmB;YACrB;YACAH,OAAOC,OAAO,CAAC,WAAW;QAC5B;IACF,GAAG,EAAE;IAELT,UAAU;QACRM,aAAaF;IACf,GAAG;QAACE;QAAcF;KAAc;IAEhC,IAAI,CAACD,aAAa,CAACE,mBAAmB;QACpC,OAAO;IACT;IAEA,qBACE,KAACP;QACCc,IAAG;QACHC,QAAQ;YACNP,aAAaF;QACf;kBAEC,CAAC;;;;;mDAK2C,EAAED,UAAU;MACzD,CAAC;;AAGP"}
@@ -0,0 +1 @@
1
+ export declare function GET(request: Request): Promise<Response>;
@@ -0,0 +1,44 @@
1
+ const EEA_UK_CH_COUNTRY_CODES = new Set([
2
+ 'AT',
3
+ 'BE',
4
+ 'BG',
5
+ 'CH',
6
+ 'CY',
7
+ 'CZ',
8
+ 'DE',
9
+ 'DK',
10
+ 'EE',
11
+ 'ES',
12
+ 'FI',
13
+ 'FR',
14
+ 'GB',
15
+ 'GR',
16
+ 'HR',
17
+ 'HU',
18
+ 'IE',
19
+ 'IS',
20
+ 'IT',
21
+ 'LI',
22
+ 'LT',
23
+ 'LU',
24
+ 'LV',
25
+ 'MT',
26
+ 'NL',
27
+ 'NO',
28
+ 'PL',
29
+ 'PT',
30
+ 'RO',
31
+ 'SE',
32
+ 'SI',
33
+ 'SK'
34
+ ]);
35
+ // eslint-disable-next-line @typescript-eslint/require-await
36
+ export async function GET(request) {
37
+ const country = request.headers.get('x-vercel-ip-country')?.toUpperCase() || null;
38
+ const requiresConsent = country ? EEA_UK_CH_COUNTRY_CODES.has(country) : true;
39
+ return Response.json({
40
+ requiresConsent
41
+ });
42
+ }
43
+
44
+ //# sourceMappingURL=consent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/api/consent.ts"],"sourcesContent":["const EEA_UK_CH_COUNTRY_CODES = new Set([\n 'AT',\n 'BE',\n 'BG',\n 'CH',\n 'CY',\n 'CZ',\n 'DE',\n 'DK',\n 'EE',\n 'ES',\n 'FI',\n 'FR',\n 'GB',\n 'GR',\n 'HR',\n 'HU',\n 'IE',\n 'IS',\n 'IT',\n 'LI',\n 'LT',\n 'LU',\n 'LV',\n 'MT',\n 'NL',\n 'NO',\n 'PL',\n 'PT',\n 'RO',\n 'SE',\n 'SI',\n 'SK',\n])\n\n// eslint-disable-next-line @typescript-eslint/require-await\nexport async function GET(request: Request) {\n const country = request.headers.get('x-vercel-ip-country')?.toUpperCase() || null\n const requiresConsent = country ? EEA_UK_CH_COUNTRY_CODES.has(country) : true\n\n return Response.json({ requiresConsent })\n}\n"],"names":["EEA_UK_CH_COUNTRY_CODES","Set","GET","request","country","headers","get","toUpperCase","requiresConsent","has","Response","json"],"mappings":"AAAA,MAAMA,0BAA0B,IAAIC,IAAI;IACtC;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD;AAED,4DAA4D;AAC5D,OAAO,eAAeC,IAAIC,OAAgB;IACxC,MAAMC,UAAUD,QAAQE,OAAO,CAACC,GAAG,CAAC,wBAAwBC,iBAAiB;IAC7E,MAAMC,kBAAkBJ,UAAUJ,wBAAwBS,GAAG,CAACL,WAAW;IAEzE,OAAOM,SAASC,IAAI,CAAC;QAAEH;IAAgB;AACzC"}
@@ -0,0 +1,71 @@
1
+ # Question
2
+
3
+ Why can't I load the gtm script until consent has been granted? Isn't the point of the gtag('consent') call to disable the parts of GA that would track with cookies?
4
+
5
+ # Answer
6
+
7
+ In theory you **can** load Google Tag Manager (or the Google tag) immediately and rely on
8
+
9
+ ```js
10
+ gtag('consent','default',{…all denied…})
11
+ ```
12
+
13
+ to block cookies. That is Google’s **“Advanced Consent Mode”** implementation. But many privacy teams still choose the stricter **“Basic”** approach—holding the whole GTM payload until the visitor has opted-in—because the *practical* and *legal* differences between the two modes are bigger than the single line of code suggests.
14
+
15
+ ---
16
+
17
+ ### 1 What still happens when GTM loads in “default-denied” mode?
18
+
19
+ | | Advanced Consent Mode | Basic Consent Mode |
20
+ | ------------------------ | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
21
+ | Tag loading | Runs as soon as the page loads (cookies start **denied**) | Blocked until the user interacts with the banner |
22
+ | Data sent before consent | **Cookieless pings** that include the consent state plus network-level data (IP address, UA string, referrer, screen size…) | **Nothing at all**—not even the consent state |
23
+ | Regulatory posture | You must justify why sending those pings (and the cross-border transfer) is lawful without prior consent | No processing ⇒ no lawful-basis debate up-front |
24
+
25
+ Google’s own help page spells this out: the advanced mode “loads with defaults set to *denied* … **While consent is denied, the Google tags send cookieless pings**.” ([Google Help][1])
26
+
27
+ Those pings are small and Google deletes the IP immediately, but they **do travel to Google’s servers and necessarily contain the visitor’s IP address** (still personal data under GDPR Recital 30) together with user-agent & screen info. ([Google Help][2])
28
+
29
+ ---
30
+
31
+ ### 2 Why some DPOs insist on deferring the script
32
+
33
+ 1. **Zero-data guarantee** – If the tags never load, you can state truthfully that *no personal data leaves the browser until consent*, eliminating legitimate-interest debates.
34
+
35
+ 2. **Cross-border transfer risk** – Several EU DPAs (Austria, France, Italy, Finland …) have already ruled that sending even *anonymised* GA data to the US violates GDPR because the IP address is still transmitted. ([noyb.eu][3]) Blocking the whole script sidesteps this argument.
36
+
37
+ 3. **Mis-fire protection** – GTM can host any third-party or custom HTML tag. If one of those tags ignores Consent Mode (or you forget to flag it), it can drop cookies before you notice. Loading GTM only after an explicit “Accept” keeps that surface area at zero.
38
+
39
+ 4. **Simpler audits** – Regulators (and your own QA) only have to confirm that the banner works. They don’t need to inspect every tag template or network request for possible leaks.
40
+
41
+ 5. **Easier banner copy** – Most consent banners promise “no tracking before consent.” Basic mode makes that statement literally true; advanced mode makes it… *mostly* true and therefore potentially misleading.
42
+
43
+ ---
44
+
45
+ ### 3 When is Advanced mode worth the trade-off?
46
+
47
+ * **You really need Google’s modelling uplift.**
48
+ Cookieless pings feed GA4 and Ads’ conversion-modelling, so you lose some reporting accuracy in Basic mode.
49
+
50
+ * **You’re satisfied your international-transfer safeguards are rock-solid** (e.g., Google Analytics 4 with EU Data Boundary + new adequacy decision, plus your own SCCs and DPIA).
51
+
52
+ * **Your legal basis is clear.** Some controllers rely on *legitimate interest* for those minimal pings and reserve *consent* exclusively for cookies and personalised ads. That position must be documented.
53
+
54
+ If you go the “Advanced” route, make sure you also:
55
+
56
+ 1. **Enable `ads_data_redaction`** so ad-click IDs are stripped while consent is denied.
57
+ 2. **Double-check every custom tag’s consent settings in GTM.**
58
+ 3. **Document the residual personal data (IP, UA, etc.)** in your Record of Processing Activities and privacy notice.
59
+
60
+ ---
61
+
62
+ ### 4 Bottom line
63
+
64
+ *`gtag('consent','default',{…denied…})`* only prevents **cookies**; it does **not** prevent **all data** leaving the browser.
65
+ If you want the cleanest, least-arguable GDPR story—*“no personal data is processed until the user opts-in”*—keep GTM blocked until the banner returns **“granted.”**
66
+
67
+ If you need Google’s modelling and are comfortable defending the minimal data transfer, load GTM up-front with **Advanced Consent Mode**—but be very explicit about that in your privacy documentation and banner wording.
68
+
69
+ [1]: https://support.google.com/tagmanager/answer/14009635?hl=en "Set up consent mode - Tag Manager Help"
70
+ [2]: https://support.google.com/analytics/answer/13802165?hl=en "Consent mode reference - Analytics Help"
71
+ [3]: https://noyb.eu/en/update-further-eu-dpa-orders-stop-google-analytics "UPDATE: Further EU DPA orders stop of Google Analytics"
@@ -0,0 +1,8 @@
1
+ export { default as Analytics } from './Analytics.js';
2
+ export { default as CookieBanner } from './CookieBanner.js';
3
+ export { CookieBannerPortal } from './CookieBannerPortal.js';
4
+ export { type ConsentStrategy, CookieBannerProvider, useCookieBanner, } from './CookieBannerProvider.js';
5
+ export { default as FacebookPixel } from './FacebookPixel.js';
6
+ export { default as GoogleAnalytics } from './GoogleAnalytics.js';
7
+ export { default as GoogleTagManager } from './GoogleTagManager.js';
8
+ export { default as MicrosoftClarity } from './MicrosoftClarity.js';
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export { default as Analytics } from './Analytics.js';
2
+ export { default as CookieBanner } from './CookieBanner.js';
3
+ export { CookieBannerPortal } from './CookieBannerPortal.js';
4
+ export { CookieBannerProvider, useCookieBanner } from './CookieBannerProvider.js';
5
+ export { default as FacebookPixel } from './FacebookPixel.js';
6
+ export { default as GoogleAnalytics } from './GoogleAnalytics.js';
7
+ export { default as GoogleTagManager } from './GoogleTagManager.js';
8
+ export { default as MicrosoftClarity } from './MicrosoftClarity.js';
9
+
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { default as Analytics } from './Analytics.js'\nexport { default as CookieBanner } from './CookieBanner.js'\nexport { CookieBannerPortal } from './CookieBannerPortal.js'\nexport {\n type ConsentStrategy,\n CookieBannerProvider,\n useCookieBanner,\n} from './CookieBannerProvider.js'\nexport { default as FacebookPixel } from './FacebookPixel.js'\nexport { default as GoogleAnalytics } from './GoogleAnalytics.js'\nexport { default as GoogleTagManager } from './GoogleTagManager.js'\nexport { default as MicrosoftClarity } from './MicrosoftClarity.js'\n"],"names":["default","Analytics","CookieBanner","CookieBannerPortal","CookieBannerProvider","useCookieBanner","FacebookPixel","GoogleAnalytics","GoogleTagManager","MicrosoftClarity"],"mappings":"AAAA,SAASA,WAAWC,SAAS,QAAQ,iBAAgB;AACrD,SAASD,WAAWE,YAAY,QAAQ,oBAAmB;AAC3D,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAEEC,oBAAoB,EACpBC,eAAe,QACV,4BAA2B;AAClC,SAASL,WAAWM,aAAa,QAAQ,qBAAoB;AAC7D,SAASN,WAAWO,eAAe,QAAQ,uBAAsB;AACjE,SAASP,WAAWQ,gBAAgB,QAAQ,wBAAuB;AACnE,SAASR,WAAWS,gBAAgB,QAAQ,wBAAuB"}
@@ -0,0 +1 @@
1
+ .container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.ww .fixed{position:fixed}.ww .inset-x-4{left:1rem;right:1rem}.ww .bottom-4{bottom:1rem}.ww .z-50{z-index:50}.ww .mx-auto{margin-left:auto;margin-right:auto}.ww .flex{display:flex}.ww .shrink-0{flex-shrink:0}.ww .flex-col{flex-direction:column}.ww .items-center{align-items:center}.ww .justify-between{justify-content:space-between}.ww .gap-4{gap:1rem}.ww :is(.space-y-2>:not([hidden])~:not([hidden])){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.ww .rounded-lg{border-radius:.5rem}.ww .rounded-md{border-radius:.375rem}.ww .border{border-width:1px}.ww .border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity,1))}.ww .bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity,1))}.ww .bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.ww .px-4{padding-left:1rem;padding-right:1rem}.ww .px-6{padding-left:1.5rem;padding-right:1.5rem}.ww .py-3{padding-top:.75rem;padding-bottom:.75rem}.ww .py-8{padding-top:2rem;padding-bottom:2rem}.ww .text-base{font-size:1rem;line-height:1.5rem}.ww .text-sm{font-size:.875rem;line-height:1.25rem}.ww .font-medium{font-weight:500}.ww .text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity,1))}.ww .text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.ww .underline{text-decoration-line:underline}.ww .shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.ww .hover\:bg-stone-800:hover{--tw-bg-opacity:1;background-color:rgb(41 37 36/var(--tw-bg-opacity,1))}@media (min-width:640px){.ww .sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width:768px){.ww .md\:flex-row{flex-direction:row}.ww .md\:items-center{align-items:center}}@media (min-width:1024px){.ww .lg\:px-8{padding-left:2rem;padding-right:2rem}}
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@whatworks/analytics",
3
+ "version": "1.0.0",
4
+ "description": "Analytics components for Next.js with cookie consent.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./api/consent": {
14
+ "import": "./dist/api/consent.js",
15
+ "types": "./dist/api/consent.d.ts",
16
+ "default": "./dist/api/consent.js"
17
+ }
18
+ },
19
+ "main": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "sideEffects": false,
25
+ "devDependencies": {
26
+ "@eslint/eslintrc": "^3.2.0",
27
+ "@swc-node/register": "1.10.9",
28
+ "@swc/cli": "0.6.0",
29
+ "@types/node": "^22.5.4",
30
+ "@types/react": "19.1.8",
31
+ "@types/react-dom": "19.1.6",
32
+ "copyfiles": "2.4.1",
33
+ "eslint": "^9.23.0",
34
+ "eslint-config-next": "15.4.4",
35
+ "next": "15.4.4",
36
+ "prettier": "^3.4.2",
37
+ "postcss": "^8.4.49",
38
+ "autoprefixer": "^10.4.20",
39
+ "react": "19.1.0",
40
+ "react-dom": "19.1.0",
41
+ "rimraf": "3.0.2",
42
+ "sort-package-json": "^2.10.0",
43
+ "tailwindcss": "^3.4.17",
44
+ "typescript": "5.7.3",
45
+ "vite-tsconfig-paths": "^5.1.4"
46
+ },
47
+ "peerDependencies": {
48
+ "next": "^15.4.4",
49
+ "react": "^19.1.0",
50
+ "react-dom": "^19.1.0"
51
+ },
52
+ "engines": {
53
+ "node": "^18.20.2 || >=20.9.0",
54
+ "pnpm": "^9 || ^10"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "registry": "https://registry.npmjs.org/",
60
+ "dependencies": {},
61
+ "scripts": {
62
+ "build": "pnpm copyfiles && pnpm build:css && pnpm build:types && pnpm build:swc",
63
+ "build:css": "tailwindcss -c tailwind.config.js -i ./src/styles.css -o ./dist/styles.css --minify",
64
+ "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
65
+ "build:types": "tsc --outDir dist --rootDir ./src",
66
+ "clean": "rimraf {dist,*.tsbuildinfo}",
67
+ "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json,md}\" dist/",
68
+ "dev": "next dev dev",
69
+ "lint": "eslint",
70
+ "lint:fix": "eslint ./src --fix"
71
+ }
72
+ }