feat(website): implement PDPA cookie consent banner and script manager (#14)
### Overview Implement PDPA & GDPR compliant Cookie Consent Management Platform (CMP) for pps/website: - **Zustand Store (cookieConsentStore.ts):** Stores consent record ( ecessary, nalytics, marketing) in localStorage with skipHydration: true for Next.js SSR safety. - **Cookie Banner (CookieBanner.tsx):** Fixed bottom floating banner with Quiet Luxury styling, providing Accept All, Reject Non-Essential, and Customize buttons. - **Preferences Modal (CookiePreferencesModal.tsx):** Detailed modal dialog allowing users to granularly toggle Analytics and Marketing cookies (Strictly Necessary is locked). - **Script Manager (ScriptManager.tsx):** Coordinates Google Consent Mode v2 (nalytics_storage, d_storage), conditionally injects GA4, Meta Pixel, and TikTok Pixel, and dispatches homaroma:consent-change DOM events. - **Footer Revocation Link:** Added Cookie Preferences link in the Support column of Footer.tsx so users can modify/revoke consent anytime. - **Multilingual Support:** Added localized text for TH, EN, JA, and ZH. --------- Co-authored-by: Your Name <you@example.com> Reviewed-on: #14 Co-authored-by: antigravity-bot <bot@kittisak.com>
This commit was merged in pull request #14.
This commit is contained in:
1 parent
9a18463bda
commit
b27afb4561
11 files changed
+692
-7
No files matched your search
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import { useCookieConsentStore } from '../../store/cookieConsentStore';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Cookie, Settings2, ShieldCheck } from 'lucide-react';
|
||||
|
||||
export default function CookieBanner() {
|
||||
const { t } = useTranslation();
|
||||
const { record, isHydrated, acceptAll, rejectAll, openPreferences } = useCookieConsentStore();
|
||||
|
||||
// Do not render on server or before rehydration, or if user has already made a choice
|
||||
if (!isHydrated || record.hasResponded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<aside
|
||||
aria-label={t('cookie.bannerTitle', 'การตั้งค่าคุกกี้และความเป็นส่วนตัว')}
|
||||
className="fixed bottom-0 inset-x-0 z-50 p-4 sm:p-6 pointer-events-none animate-in fade-in slide-in-from-bottom-6 duration-500"
|
||||
>
|
||||
<div className="max-w-5xl mx-auto pointer-events-auto bg-surface-card/95 backdrop-blur-md border border-border-color shadow-2xl rounded-2xl p-5 sm:p-6 text-text-main transition-all">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-5">
|
||||
{/* Icon & Description */}
|
||||
<div className="flex items-start gap-3.5 flex-1 pr-0 md:pr-4">
|
||||
<div className="p-2.5 rounded-xl bg-brand-cool/60 text-brand-primary shrink-0 mt-0.5">
|
||||
<Cookie className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-sm sm:text-base text-text-main tracking-wide">
|
||||
{t('cookie.bannerTitle', 'เราให้ความสำคัญกับความเป็นส่วนตัวของคุณ')}
|
||||
</h3>
|
||||
<span className="inline-flex items-center gap-1 text-[11px] font-medium px-2 py-0.5 rounded-full bg-brand-cool/50 text-muted-text">
|
||||
<ShieldCheck className="w-3 h-3 text-brand-primary" />
|
||||
PDPA Compliant
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs sm:text-sm text-muted-text leading-relaxed">
|
||||
{t(
|
||||
'cookie.bannerDesc',
|
||||
'เราใช้คุกกี้เพื่อเพิ่มประสิทธิภาพ ประสบการณ์ที่ดีในการใช้งานเว็บไซต์ และเพื่อการวิเคราะห์ปรับปรุงบริการ ท่านสามารถเลือกตั้งค่าความยินยอมตามประเภทคุกกี้ได้'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-wrap sm:flex-nowrap items-center gap-2.5 w-full md:w-auto shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={openPreferences}
|
||||
className="flex-1 sm:flex-none inline-flex items-center justify-center gap-1.5 px-3.5 py-2 text-xs sm:text-sm font-medium rounded-lg text-muted-text hover:text-text-main hover:bg-surface-muted transition-colors border border-transparent"
|
||||
>
|
||||
<Settings2 className="w-3.5 h-3.5" />
|
||||
<span>{t('cookie.btnCustomize', 'ตั้งค่าคุกกี้')}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={rejectAll}
|
||||
className="flex-1 sm:flex-none px-4 py-2 text-xs sm:text-sm font-medium rounded-lg border border-border-color text-text-main hover:bg-surface-muted hover:border-text-main/20 transition-all text-center"
|
||||
>
|
||||
{t('cookie.btnRejectAll', 'ปฏิเสธทั้งหมด')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={acceptAll}
|
||||
className="w-full sm:w-auto px-5 py-2 text-xs sm:text-sm font-semibold rounded-lg bg-brand-primary text-white hover:bg-brand-primary-hover shadow-sm hover:shadow transition-all text-center tracking-wide"
|
||||
>
|
||||
{t('cookie.btnAcceptAll', 'ยอมรับทั้งหมด')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useCookieConsentStore, type CookiePreferences } from '../../store/cookieConsentStore';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { X, ShieldCheck, BarChart3, Target, Lock, Check } from 'lucide-react';
|
||||
|
||||
export default function CookiePreferencesModal() {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
record,
|
||||
isPreferencesOpen,
|
||||
closePreferences,
|
||||
savePreferences,
|
||||
acceptAll,
|
||||
rejectAll,
|
||||
} = useCookieConsentStore();
|
||||
|
||||
// Local state for interactive toggling inside the modal
|
||||
const [localPrefs, setLocalPrefs] = useState<CookiePreferences>({
|
||||
necessary: true,
|
||||
analytics: false,
|
||||
marketing: false,
|
||||
});
|
||||
|
||||
// Sync local state when modal opens
|
||||
useEffect(() => {
|
||||
if (isPreferencesOpen) {
|
||||
setLocalPrefs({
|
||||
necessary: true,
|
||||
analytics: Boolean(record.preferences.analytics),
|
||||
marketing: Boolean(record.preferences.marketing),
|
||||
});
|
||||
}
|
||||
}, [isPreferencesOpen, record.preferences]);
|
||||
|
||||
// Handle ESC key to close
|
||||
useEffect(() => {
|
||||
if (!isPreferencesOpen) return;
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') closePreferences();
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isPreferencesOpen, closePreferences]);
|
||||
|
||||
if (!isPreferencesOpen) return null;
|
||||
|
||||
const handleSave = () => {
|
||||
savePreferences(localPrefs);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="cookie-modal-title"
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 bg-black/50 backdrop-blur-sm animate-in fade-in duration-200"
|
||||
>
|
||||
<div
|
||||
className="w-full max-w-2xl bg-surface-card rounded-2xl shadow-2xl border border-border-color overflow-hidden flex flex-col max-h-[90vh] animate-in zoom-in-95 duration-200"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-border-color bg-surface-nav-rest">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="p-2 rounded-lg bg-brand-cool/70 text-brand-primary">
|
||||
<ShieldCheck className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 id="cookie-modal-title" className="text-base sm:text-lg font-bold text-text-main">
|
||||
{t('cookie.prefTitle', 'ศูนย์การตั้งค่าความเป็นส่วนตัว (Cookie Preferences)')}
|
||||
</h2>
|
||||
<p className="text-xs text-muted-text">
|
||||
{t('cookie.prefSubtitle', 'จัดการความยินยอมในการจัดเก็บและใช้งานคุกกี้ตาม พ.ร.บ. คุ้มครองข้อมูลส่วนบุคคล (PDPA)')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closePreferences}
|
||||
className="p-2 text-muted-text hover:text-text-main rounded-lg hover:bg-surface-muted transition-colors"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body / Cookie Categories */}
|
||||
<div className="p-6 space-y-4 overflow-y-auto divide-y divide-border-color/60">
|
||||
{/* 1. Necessary Cookies */}
|
||||
<div className="pt-2 first:pt-0">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
<div className="p-2 rounded-md bg-surface-muted text-muted-text mt-0.5">
|
||||
<Lock className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-sm font-semibold text-text-main">
|
||||
{t('cookie.catNecessaryTitle', 'คุกกี้ที่จำเป็นอย่างยิ่ง (Strictly Necessary)')}
|
||||
</h4>
|
||||
<span className="text-[10px] font-medium px-2 py-0.5 rounded-full bg-emerald-50 text-emerald-700 border border-emerald-200">
|
||||
{t('cookie.alwaysActive', 'เปิดใช้งานเสมอ')}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-text mt-1 leading-relaxed">
|
||||
{t(
|
||||
'cookie.catNecessaryDesc',
|
||||
'คุกกี้เหล่านี้มีความจำเป็นต่อการทำงานพื้นฐานของเว็บไซต์ เช่น การเข้าสู่ระบบ ความปลอดภัย การจดจำสินค้าในตะกร้า และการตั้งค่าภาษา เว็บไซต์ไม่สามารถทำงานได้อย่างถูกต้องหากไม่มีคุกกี้เหล่านี้'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative inline-flex items-center cursor-not-allowed opacity-80 shrink-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={true}
|
||||
disabled
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-brand-primary rounded-full peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2. Analytics Cookies */}
|
||||
<div className="pt-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
<div className="p-2 rounded-md bg-brand-cool/50 text-brand-primary mt-0.5">
|
||||
<BarChart3 className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-text-main">
|
||||
{t('cookie.catAnalyticsTitle', 'คุกกี้เพื่อการวิเคราะห์และวัดผล (Analytics & Performance)')}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-text mt-1 leading-relaxed">
|
||||
{t(
|
||||
'cookie.catAnalyticsDesc',
|
||||
'ช่วยให้เราทราบสถิติการเข้าชม พฤติกรรมหน้าเว็บยอดนิยม และเส้นทางการใช้งานของผู้ใช้ เพื่อนำข้อมูลไปปรับปรุงประสิทธิภาพและบริการ (เช่น Google Analytics 4) ข้อมูลจะถูกจัดเก็บแบบไม่ระบุตัวตนโดยตรง'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer shrink-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={localPrefs.analytics}
|
||||
onChange={(e) =>
|
||||
setLocalPrefs((prev) => ({ ...prev, analytics: e.target.checked }))
|
||||
}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-border-color peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand-primary"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 3. Marketing Cookies */}
|
||||
<div className="pt-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
<div className="p-2 rounded-md bg-brand-cool/50 text-brand-primary mt-0.5">
|
||||
<Target className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-text-main">
|
||||
{t('cookie.catMarketingTitle', 'คุกกี้เพื่อการตลาดและโฆษณา (Marketing & Targeting)')}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-text mt-1 leading-relaxed">
|
||||
{t(
|
||||
'cookie.catMarketingDesc',
|
||||
'ใช้เพื่อนำเสนอโปรโมชัน โฆษณา และเนื้อหาที่สอดคล้องกับความสนใจของท่านผ่านแพลตฟอร์มพันธมิตร (เช่น Meta Pixel, TikTok Pixel, Google Ads) รวมถึงวัดประสิทธิผลของแคมเปญ'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer shrink-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={localPrefs.marketing}
|
||||
onChange={(e) =>
|
||||
setLocalPrefs((prev) => ({ ...prev, marketing: e.target.checked }))
|
||||
}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-border-color peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand-primary"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer / Buttons */}
|
||||
<div className="flex flex-col-reverse sm:flex-row items-center justify-between gap-3 p-4 sm:p-5 border-t border-border-color bg-surface-nav-rest">
|
||||
<button
|
||||
type="button"
|
||||
onClick={rejectAll}
|
||||
className="w-full sm:w-auto px-4 py-2 text-xs sm:text-sm font-medium rounded-lg text-muted-text hover:text-text-main hover:bg-surface-muted transition-colors text-center"
|
||||
>
|
||||
{t('cookie.btnRejectAll', 'ปฏิเสธทั้งหมด')}
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2.5 w-full sm:w-auto">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
className="flex-1 sm:flex-none inline-flex items-center justify-center gap-1.5 px-4 py-2 text-xs sm:text-sm font-medium rounded-lg border border-border-color text-text-main hover:bg-surface-muted hover:border-text-main/20 transition-all text-center"
|
||||
>
|
||||
<Check className="w-4 h-4 text-brand-primary" />
|
||||
<span>{t('cookie.btnSavePreferences', 'บันทึกการตั้งค่า')}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={acceptAll}
|
||||
className="flex-1 sm:flex-none px-5 py-2 text-xs sm:text-sm font-semibold rounded-lg bg-brand-primary text-white hover:bg-brand-primary-hover shadow-sm transition-all text-center"
|
||||
>
|
||||
{t('cookie.btnAcceptAll', 'ยอมรับทั้งหมด')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useCookieConsentStore } from '../../store/cookieConsentStore';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
dataLayer?: any[];
|
||||
gtag?: (...args: any[]) => void;
|
||||
fbq?: any;
|
||||
_fbq?: any;
|
||||
ttq?: any;
|
||||
}
|
||||
}
|
||||
|
||||
export default function ScriptManager() {
|
||||
const { record, isHydrated } = useCookieConsentStore();
|
||||
const consentInitializedRef = useRef(false);
|
||||
const gaInjectedRef = useRef(false);
|
||||
const metaInjectedRef = useRef(false);
|
||||
const tiktokInjectedRef = useRef(false);
|
||||
|
||||
// 1. Initialize Google Consent Mode v2 Defaults
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined' || consentInitializedRef.current) return;
|
||||
consentInitializedRef.current = true;
|
||||
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
if (!window.gtag) {
|
||||
window.gtag = function () {
|
||||
window.dataLayer?.push(arguments);
|
||||
};
|
||||
}
|
||||
|
||||
// Default to 'denied' as required by GDPR/PDPA prior to user choice
|
||||
window.gtag('consent', 'default', {
|
||||
analytics_storage: 'denied',
|
||||
ad_storage: 'denied',
|
||||
ad_user_data: 'denied',
|
||||
ad_personalization: 'denied',
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 2. Sync consent updates whenever state changes
|
||||
useEffect(() => {
|
||||
if (!isHydrated || typeof window === 'undefined') return;
|
||||
|
||||
const { preferences, hasResponded } = record;
|
||||
|
||||
if (window.gtag) {
|
||||
window.gtag('consent', 'update', {
|
||||
analytics_storage: hasResponded && preferences.analytics ? 'granted' : 'denied',
|
||||
ad_storage: hasResponded && preferences.marketing ? 'granted' : 'denied',
|
||||
ad_user_data: hasResponded && preferences.marketing ? 'granted' : 'denied',
|
||||
ad_personalization: hasResponded && preferences.marketing ? 'granted' : 'denied',
|
||||
});
|
||||
}
|
||||
|
||||
// Dispatch custom browser event for any other custom tags/scripts
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('homaroma:consent-change', {
|
||||
detail: {
|
||||
hasResponded,
|
||||
preferences,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// 3. Inject GA4 dynamically if enabled and configured
|
||||
const gaId = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID;
|
||||
if (gaId && hasResponded && preferences.analytics && !gaInjectedRef.current) {
|
||||
gaInjectedRef.current = true;
|
||||
const script = document.createElement('script');
|
||||
script.src = `https://www.googletagmanager.com/gtag/js?id=${gaId}`;
|
||||
script.async = true;
|
||||
document.head.appendChild(script);
|
||||
|
||||
window.gtag?.('js', new Date());
|
||||
window.gtag?.('config', gaId, {
|
||||
anonymize_ip: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Inject Meta Pixel dynamically if enabled and configured
|
||||
const metaPixelId = process.env.NEXT_PUBLIC_META_PIXEL_ID;
|
||||
if (metaPixelId && hasResponded && preferences.marketing && !metaInjectedRef.current) {
|
||||
metaInjectedRef.current = true;
|
||||
(function (f: any, b: any, e: any, v: any, n?: any, t?: any, s?: any) {
|
||||
if (f.fbq) return;
|
||||
n = f.fbq = function () {
|
||||
n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments);
|
||||
};
|
||||
if (!f._fbq) f._fbq = n;
|
||||
n.push = n;
|
||||
n.loaded = true;
|
||||
n.version = '2.0';
|
||||
n.queue = [];
|
||||
t = b.createElement(e);
|
||||
t.async = true;
|
||||
t.src = v;
|
||||
s = b.getElementsByTagName(e)[0];
|
||||
s.parentNode.insertBefore(t, s);
|
||||
})(window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js');
|
||||
|
||||
window.fbq('init', metaPixelId);
|
||||
window.fbq('track', 'PageView');
|
||||
}
|
||||
|
||||
// 5. Inject TikTok Pixel dynamically if enabled and configured
|
||||
const tiktokPixelId = process.env.NEXT_PUBLIC_TIKTOK_PIXEL_ID;
|
||||
if (tiktokPixelId && hasResponded && preferences.marketing && !tiktokInjectedRef.current) {
|
||||
tiktokInjectedRef.current = true;
|
||||
(function (w: any, d: any, t: any) {
|
||||
w.TiktokAnalyticsObject = t;
|
||||
const ttq = (w[t] = w[t] || []);
|
||||
ttq.methods = [
|
||||
'page',
|
||||
'track',
|
||||
'identify',
|
||||
'instances',
|
||||
'debug',
|
||||
'on',
|
||||
'off',
|
||||
'once',
|
||||
'ready',
|
||||
'alias',
|
||||
'group',
|
||||
'enableCookie',
|
||||
'disableCookie',
|
||||
];
|
||||
ttq.setAndDefer = function (t: any, e: any) {
|
||||
t[e] = function () {
|
||||
t.push([e].concat(Array.prototype.slice.call(arguments, 0)));
|
||||
};
|
||||
};
|
||||
for (let i = 0; i < ttq.methods.length; i++) {
|
||||
ttq.setAndDefer(ttq, ttq.methods[i]);
|
||||
}
|
||||
ttq.instance = function (t: any) {
|
||||
const e = ttq._i[t] || [];
|
||||
for (let n = 0; n < ttq.methods.length; n++) {
|
||||
ttq.setAndDefer(e, ttq.methods[n]);
|
||||
}
|
||||
return e;
|
||||
};
|
||||
ttq.load = function (e: any, n: any) {
|
||||
const i = 'https://analytics.tiktok.com/i18n/pixel/events.js';
|
||||
ttq._i = ttq._i || {};
|
||||
ttq._i[e] = [];
|
||||
ttq._i[e]._u = i;
|
||||
ttq._t = ttq._t || {};
|
||||
ttq._t[e] = +new Date();
|
||||
ttq._o = ttq._o || {};
|
||||
ttq._o[e] = n || {};
|
||||
const o = d.createElement('script');
|
||||
o.type = 'text/javascript';
|
||||
o.async = true;
|
||||
o.src = `${i}?sdkid=${e}&lib=${t}`;
|
||||
const a = d.getElementsByTagName('script')[0];
|
||||
a.parentNode.insertBefore(o, a);
|
||||
};
|
||||
})(window, document, 'ttq');
|
||||
|
||||
window.ttq?.load(tiktokPixelId);
|
||||
window.ttq?.page();
|
||||
}
|
||||
}, [record, isHydrated]);
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in new issue
Block a user